reqscan 1.0.2 → 1.2.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.
- package/README.md +29 -14
- package/package.json +3 -3
- package/src/api.js +13 -0
- package/src/colors.js +23 -14
- package/src/commands.js +147 -45
- package/src/index.js +44 -18
- package/src/scanner.js +55 -46
package/README.md
CHANGED
|
@@ -96,6 +96,17 @@ reqscan list
|
|
|
96
96
|
|
|
97
97
|
---
|
|
98
98
|
|
|
99
|
+
## 🚩 Available Flags
|
|
100
|
+
|
|
101
|
+
| Flag | Description | Commands |
|
|
102
|
+
|------|-------------|----------|
|
|
103
|
+
| `--json` | Output machine-readable JSON | `check`, `audit`, `list` |
|
|
104
|
+
| `--save-dev` | Install missing packages as `devDependencies` | `install`, `fix` |
|
|
105
|
+
| `--dry-run` | Preview changes without executing | `install`, `clean`, `fix` |
|
|
106
|
+
| `--force` | Skip confirmation prompt | `clean`, `fix` |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
99
110
|
## 📤 Example Outputs
|
|
100
111
|
|
|
101
112
|
### `reqscan check`
|
|
@@ -175,12 +186,15 @@ Legend: ✓ present ✗ missing ~ declared but unused
|
|
|
175
186
|
2. **Extracts** package names from all import styles:
|
|
176
187
|
- `require('pkg')`
|
|
177
188
|
- `import x from 'pkg'`
|
|
178
|
-
- `import
|
|
189
|
+
- `import type { T } from 'pkg'`
|
|
190
|
+
- `import('pkg')` (dynamic)
|
|
179
191
|
- `require.resolve('pkg')`
|
|
180
192
|
- `export { x } from 'pkg'`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
- `import 'pkg'` (side-effect)
|
|
194
|
+
3. **Strips comments** before scanning so commented-out imports are ignored
|
|
195
|
+
4. **Filters out** Node.js built-ins (`fs`, `path`, `http`, `node:*`, etc.)
|
|
196
|
+
5. **Compares** against `package.json` (all dependency types)
|
|
197
|
+
6. **Reports** and optionally acts on findings
|
|
184
198
|
|
|
185
199
|
---
|
|
186
200
|
|
|
@@ -194,22 +208,25 @@ const result = await scanProject('/path/to/project');
|
|
|
194
208
|
console.log(result.missing); // ['axios', 'uuid']
|
|
195
209
|
console.log(result.present); // ['express', 'mongoose']
|
|
196
210
|
console.log(result.unused); // ['jest']
|
|
197
|
-
console.log(result.allImports); // all detected package names
|
|
211
|
+
console.log(result.allImports); // all detected package names (sorted)
|
|
212
|
+
console.log(result.declared); // all declared package names (sorted)
|
|
198
213
|
console.log(result.scannedFiles); // list of files scanned
|
|
214
|
+
console.log(result.targetDir); // resolved project directory
|
|
199
215
|
```
|
|
200
216
|
|
|
201
217
|
### Return Value
|
|
202
218
|
|
|
203
219
|
| Field | Type | Description |
|
|
204
220
|
|-------|------|-------------|
|
|
205
|
-
| `projectName` | `string` | Name from package.json |
|
|
206
|
-
| `allImports` | `string[]` | All unique packages found in source |
|
|
207
|
-
| `declared` | `string[]` | All packages in package.json |
|
|
221
|
+
| `projectName` | `string` | Name from `package.json` |
|
|
222
|
+
| `allImports` | `string[]` | All unique packages found in source (sorted) |
|
|
223
|
+
| `declared` | `string[]` | All packages in `package.json` (sorted) |
|
|
208
224
|
| `missing` | `string[]` | Imported but not declared |
|
|
209
225
|
| `present` | `string[]` | Imported AND declared |
|
|
210
226
|
| `unused` | `string[]` | Declared but never imported |
|
|
211
227
|
| `scannedFiles` | `string[]` | All files that were scanned |
|
|
212
|
-
| `packageJson` | `object\|null` | Parsed package.json content |
|
|
228
|
+
| `packageJson` | `object\|null` | Parsed `package.json` content |
|
|
229
|
+
| `targetDir` | `string` | Resolved absolute path to the project root |
|
|
213
230
|
|
|
214
231
|
---
|
|
215
232
|
|
|
@@ -225,14 +242,12 @@ node test/test.js
|
|
|
225
242
|
|
|
226
243
|
- [ ] `reqscan outdated` — Check for newer package versions
|
|
227
244
|
- [ ] `reqscan upgrade` — Update all outdated packages
|
|
228
|
-
- [ ] `--save-dev` flag for install command
|
|
229
|
-
- [ ] `--dry-run` flag for previewing changes
|
|
230
245
|
- [ ] Configuration file (`.reqscanrc`)
|
|
231
|
-
- [ ] Monorepo/workspace support
|
|
232
|
-
- [
|
|
246
|
+
- [ ] Monorepo / workspace support
|
|
247
|
+
- [x] `--save-dev`, `--dry-run`, `--force`, `--json` flags *(shipped in v1.1.0)*
|
|
233
248
|
|
|
234
249
|
---
|
|
235
250
|
|
|
236
251
|
## 📄 License
|
|
237
252
|
|
|
238
|
-
MIT
|
|
253
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reqscan",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Dependency Manager for Node.js — scan, install, clean, and audit your project dependencies",
|
|
5
|
-
"main": "src/
|
|
5
|
+
"main": "src/api.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"reqscan": "./src/index.js"
|
|
8
8
|
},
|
|
@@ -29,4 +29,4 @@
|
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=14.0.0"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* reqscan — Public Programmatic API
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { scanProject } = require('reqscan');
|
|
8
|
+
* const result = await scanProject('/path/to/project');
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { scanProject, readPackageJson } = require('./scanner');
|
|
12
|
+
|
|
13
|
+
module.exports = { scanProject, readPackageJson };
|
package/src/colors.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Respect NO_COLOR env (https://no-color.org) and non-TTY output
|
|
4
|
+
const ENABLED = !process.env.NO_COLOR && process.stdout.isTTY !== false;
|
|
5
|
+
|
|
6
|
+
const raw = {
|
|
4
7
|
reset : '\x1b[0m',
|
|
5
8
|
bold : '\x1b[1m',
|
|
6
9
|
dim : '\x1b[2m',
|
|
@@ -11,20 +14,26 @@ const c = {
|
|
|
11
14
|
white : '\x1b[37m',
|
|
12
15
|
};
|
|
13
16
|
|
|
14
|
-
const paint = (
|
|
17
|
+
const paint = (code, text) => ENABLED ? `${code}${text}${raw.reset}` : text;
|
|
15
18
|
|
|
16
19
|
module.exports = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
// Raw codes (for constructing multi-codes manually)
|
|
21
|
+
...raw,
|
|
22
|
+
|
|
23
|
+
// Single-color helpers
|
|
24
|
+
bold : (t) => paint(raw.bold, t),
|
|
25
|
+
dim : (t) => paint(raw.dim, t),
|
|
26
|
+
red : (t) => paint(raw.red, t),
|
|
27
|
+
green : (t) => paint(raw.green, t),
|
|
28
|
+
yellow : (t) => paint(raw.yellow, t),
|
|
29
|
+
cyan : (t) => paint(raw.cyan, t),
|
|
30
|
+
white : (t) => paint(raw.white, t),
|
|
31
|
+
|
|
32
|
+
// Combined helpers
|
|
33
|
+
boldCyan : (t) => ENABLED ? `${raw.bold}${raw.cyan}${t}${raw.reset}` : t,
|
|
34
|
+
boldGreen : (t) => ENABLED ? `${raw.bold}${raw.green}${t}${raw.reset}` : t,
|
|
35
|
+
boldRed : (t) => ENABLED ? `${raw.bold}${raw.red}${t}${raw.reset}` : t,
|
|
36
|
+
boldYellow : (t) => ENABLED ? `${raw.bold}${raw.yellow}${t}${raw.reset}` : t,
|
|
37
|
+
|
|
29
38
|
LINE : '─'.repeat(50),
|
|
30
39
|
};
|
package/src/commands.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { execSync }
|
|
4
|
-
const readline
|
|
5
|
-
const { scanProject} = require('./scanner');
|
|
6
|
-
const col
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const { scanProject, readPackageJson } = require('./scanner');
|
|
6
|
+
const col = require('./colors');
|
|
7
7
|
|
|
8
8
|
// ─── Shared helpers ───────────────────────────────────────────────────────────
|
|
9
9
|
|
|
@@ -29,10 +29,45 @@ function runNpm(args, cwd) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function getVersion(pkgJson, pkg) {
|
|
33
|
+
return (
|
|
34
|
+
pkgJson?.dependencies?.[pkg] ||
|
|
35
|
+
pkgJson?.devDependencies?.[pkg] ||
|
|
36
|
+
pkgJson?.peerDependencies?.[pkg] ||
|
|
37
|
+
pkgJson?.optionalDependencies?.[pkg] || ''
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getDepType(pkgJson, pkg) {
|
|
42
|
+
if (pkgJson?.dependencies?.[pkg]) return 'dependencies';
|
|
43
|
+
if (pkgJson?.devDependencies?.[pkg]) return 'devDependencies';
|
|
44
|
+
if (pkgJson?.peerDependencies?.[pkg]) return 'peerDependencies';
|
|
45
|
+
if (pkgJson?.optionalDependencies?.[pkg]) return 'optionalDependencies';
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
// ─── check ────────────────────────────────────────────────────────────────────
|
|
33
50
|
|
|
34
|
-
async function cmdCheck(targetDir) {
|
|
51
|
+
async function cmdCheck(targetDir, { json = false } = {}) {
|
|
35
52
|
const r = await scanProject(targetDir);
|
|
53
|
+
|
|
54
|
+
if (json) {
|
|
55
|
+
console.log(JSON.stringify({
|
|
56
|
+
project : r.projectName,
|
|
57
|
+
missing : r.missing,
|
|
58
|
+
present : r.present,
|
|
59
|
+
unused : r.unused,
|
|
60
|
+
summary : {
|
|
61
|
+
totalImports : r.allImports.length,
|
|
62
|
+
declared : r.declared.length,
|
|
63
|
+
present : r.present.length,
|
|
64
|
+
missing : r.missing.length,
|
|
65
|
+
unused : r.unused.length,
|
|
66
|
+
},
|
|
67
|
+
}, null, 2));
|
|
68
|
+
return r;
|
|
69
|
+
}
|
|
70
|
+
|
|
36
71
|
header(r.projectName);
|
|
37
72
|
|
|
38
73
|
// Summary
|
|
@@ -67,12 +102,10 @@ async function cmdCheck(targetDir) {
|
|
|
67
102
|
console.log(col.boldGreen('✅ Already Declared Packages'));
|
|
68
103
|
console.log(col.dim(col.LINE));
|
|
69
104
|
r.present.forEach(p => {
|
|
70
|
-
const ver
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
r.packageJson?.optionalDependencies?.[p] || '';
|
|
75
|
-
console.log(` ${col.green('✓')} ${p} ${col.dim(ver)}`);
|
|
105
|
+
const ver = getVersion(r.packageJson, p);
|
|
106
|
+
const type = getDepType(r.packageJson, p);
|
|
107
|
+
const typeLabel = type === 'devDependencies' ? col.dim(' [dev]') : '';
|
|
108
|
+
console.log(` ${col.green('✓')} ${p} ${col.dim(ver)}${typeLabel}`);
|
|
76
109
|
});
|
|
77
110
|
console.log();
|
|
78
111
|
}
|
|
@@ -81,9 +114,14 @@ async function cmdCheck(targetDir) {
|
|
|
81
114
|
if (r.unused.length > 0) {
|
|
82
115
|
console.log(col.boldYellow('⚠️ Declared but NOT imported in source (possibly unused)'));
|
|
83
116
|
console.log(col.dim(col.LINE));
|
|
84
|
-
r.unused.forEach(p =>
|
|
117
|
+
r.unused.forEach(p => {
|
|
118
|
+
const type = getDepType(r.packageJson, p);
|
|
119
|
+
console.log(` ${col.yellow('~')} ${p} ${col.dim(`(${type})`)}`);
|
|
120
|
+
});
|
|
85
121
|
console.log();
|
|
86
|
-
console.log(col.dim('
|
|
122
|
+
console.log(col.dim('Note: Some packages may be used in config files, build scripts, or'));
|
|
123
|
+
console.log(col.dim(' CLI tools not directly imported in source. Review before removing.'));
|
|
124
|
+
console.log(col.dim('Tip: Run `reqscan clean` to interactively remove unused packages.'));
|
|
87
125
|
console.log();
|
|
88
126
|
}
|
|
89
127
|
|
|
@@ -96,7 +134,7 @@ async function cmdCheck(targetDir) {
|
|
|
96
134
|
|
|
97
135
|
// ─── install ──────────────────────────────────────────────────────────────────
|
|
98
136
|
|
|
99
|
-
async function cmdInstall(targetDir) {
|
|
137
|
+
async function cmdInstall(targetDir, { saveDev = false, dryRun = false } = {}) {
|
|
100
138
|
console.log();
|
|
101
139
|
console.log(col.boldCyan('📦 Installing missing dependencies...'));
|
|
102
140
|
console.log();
|
|
@@ -112,10 +150,21 @@ async function cmdInstall(targetDir) {
|
|
|
112
150
|
console.log(`Found ${col.boldRed(String(r.missing.length))} missing package(s):`);
|
|
113
151
|
r.missing.forEach(p => console.log(` • ${p}`));
|
|
114
152
|
console.log();
|
|
153
|
+
|
|
154
|
+
const flag = saveDev ? '--save-dev ' : '';
|
|
155
|
+
const cmd = `install ${flag}${r.missing.join(' ')}`;
|
|
156
|
+
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
console.log(col.boldYellow('🔍 Dry run — would execute:'));
|
|
159
|
+
console.log(col.boldCyan(` npm ${cmd}`));
|
|
160
|
+
console.log();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
115
164
|
console.log('Installing... ⚙️');
|
|
116
165
|
console.log();
|
|
117
166
|
|
|
118
|
-
const ok = runNpm(
|
|
167
|
+
const ok = runNpm(cmd, targetDir);
|
|
119
168
|
|
|
120
169
|
if (ok) {
|
|
121
170
|
console.log();
|
|
@@ -131,7 +180,7 @@ async function cmdInstall(targetDir) {
|
|
|
131
180
|
|
|
132
181
|
// ─── clean ────────────────────────────────────────────────────────────────────
|
|
133
182
|
|
|
134
|
-
async function cmdClean(targetDir, { force = false } = {}) {
|
|
183
|
+
async function cmdClean(targetDir, { force = false, dryRun = false } = {}) {
|
|
135
184
|
console.log();
|
|
136
185
|
console.log(col.boldCyan('🧹 Scanning for unused dependencies...'));
|
|
137
186
|
console.log();
|
|
@@ -146,11 +195,20 @@ async function cmdClean(targetDir, { force = false } = {}) {
|
|
|
146
195
|
|
|
147
196
|
console.log(`Found ${col.boldYellow(String(r.unused.length))} unused package(s):`);
|
|
148
197
|
r.unused.forEach(p => {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
console.log(` • ${p} ${col.dim(`(${kind})`)}`);
|
|
198
|
+
const type = getDepType(r.packageJson, p);
|
|
199
|
+
console.log(` • ${p} ${col.dim(`(${type})`)}`);
|
|
152
200
|
});
|
|
153
201
|
console.log();
|
|
202
|
+
console.log(col.dim('Note: Verify these are truly unused before removing.'));
|
|
203
|
+
console.log(col.dim(' Config files and build tools may use packages without importing them.'));
|
|
204
|
+
console.log();
|
|
205
|
+
|
|
206
|
+
if (dryRun) {
|
|
207
|
+
console.log(col.boldYellow('🔍 Dry run — would execute:'));
|
|
208
|
+
console.log(col.boldCyan(` npm uninstall ${r.unused.join(' ')}`));
|
|
209
|
+
console.log();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
154
212
|
|
|
155
213
|
let confirm = 'y';
|
|
156
214
|
if (!force) {
|
|
@@ -158,6 +216,7 @@ async function cmdClean(targetDir, { force = false } = {}) {
|
|
|
158
216
|
}
|
|
159
217
|
|
|
160
218
|
if (confirm === '' || confirm.toLowerCase() === 'y') {
|
|
219
|
+
// Snapshot counts BEFORE running uninstall
|
|
161
220
|
const depsBefore = Object.keys(r.packageJson?.dependencies || {}).length;
|
|
162
221
|
const devDepsBefore = Object.keys(r.packageJson?.devDependencies || {}).length;
|
|
163
222
|
|
|
@@ -165,9 +224,8 @@ async function cmdClean(targetDir, { force = false } = {}) {
|
|
|
165
224
|
const ok = runNpm(`uninstall ${r.unused.join(' ')}`, targetDir);
|
|
166
225
|
|
|
167
226
|
if (ok) {
|
|
168
|
-
// Re-read
|
|
169
|
-
const
|
|
170
|
-
const updated = readPackageJson(targetDir);
|
|
227
|
+
// Re-read package.json fresh after uninstall (not from require cache)
|
|
228
|
+
const updated = readPackageJson(targetDir);
|
|
171
229
|
const depsAfter = Object.keys(updated?.dependencies || {}).length;
|
|
172
230
|
const devDepsAfter = Object.keys(updated?.devDependencies || {}).length;
|
|
173
231
|
|
|
@@ -188,24 +246,58 @@ async function cmdClean(targetDir, { force = false } = {}) {
|
|
|
188
246
|
|
|
189
247
|
// ─── fix ──────────────────────────────────────────────────────────────────────
|
|
190
248
|
|
|
191
|
-
async function cmdFix(targetDir) {
|
|
249
|
+
async function cmdFix(targetDir, opts = {}) {
|
|
192
250
|
console.log();
|
|
193
251
|
console.log(col.boldCyan('🔧 Running reqscan fix (install missing + clean unused)...'));
|
|
194
252
|
console.log(col.dim(col.LINE));
|
|
195
|
-
await cmdInstall(targetDir);
|
|
253
|
+
await cmdInstall(targetDir, opts);
|
|
196
254
|
console.log(col.dim(col.LINE));
|
|
197
|
-
await cmdClean(targetDir, { force: false });
|
|
255
|
+
//await cmdClean(targetDir, { ...opts, force: false });
|
|
256
|
+
|
|
257
|
+
await cmdClean(targetDir, { ...opts, force: opts.dryRun || false });
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
198
261
|
}
|
|
199
262
|
|
|
200
263
|
// ─── audit ────────────────────────────────────────────────────────────────────
|
|
201
264
|
|
|
202
|
-
async function cmdAudit(targetDir) {
|
|
265
|
+
async function cmdAudit(targetDir, { json = false } = {}) {
|
|
203
266
|
const r = await scanProject(targetDir);
|
|
267
|
+
|
|
268
|
+
const total = r.allImports.length || 1;
|
|
269
|
+
const score = Math.round((r.present.length / total) * 100);
|
|
270
|
+
|
|
271
|
+
if (json) {
|
|
272
|
+
const deps = r.packageJson?.dependencies || {};
|
|
273
|
+
const devDeps = r.packageJson?.devDependencies || {};
|
|
274
|
+
const peer = r.packageJson?.peerDependencies || {};
|
|
275
|
+
const opt = r.packageJson?.optionalDependencies|| {};
|
|
276
|
+
console.log(JSON.stringify({
|
|
277
|
+
project : r.projectName,
|
|
278
|
+
healthScore : score,
|
|
279
|
+
filesScanned : r.scannedFiles.length,
|
|
280
|
+
summary: {
|
|
281
|
+
totalImports : r.allImports.length,
|
|
282
|
+
declared : r.declared.length,
|
|
283
|
+
present : r.present.length,
|
|
284
|
+
missing : r.missing.length,
|
|
285
|
+
unused : r.unused.length,
|
|
286
|
+
},
|
|
287
|
+
breakdown: {
|
|
288
|
+
dependencies : Object.keys(deps).length,
|
|
289
|
+
devDependencies : Object.keys(devDeps).length,
|
|
290
|
+
peerDependencies : Object.keys(peer).length,
|
|
291
|
+
optionalDependencies : Object.keys(opt).length,
|
|
292
|
+
},
|
|
293
|
+
missing : r.missing,
|
|
294
|
+
unused : r.unused,
|
|
295
|
+
}, null, 2));
|
|
296
|
+
return r;
|
|
297
|
+
}
|
|
298
|
+
|
|
204
299
|
header(r.projectName);
|
|
205
300
|
|
|
206
|
-
// Health score
|
|
207
|
-
const total = r.allImports.length || 1;
|
|
208
|
-
const score = Math.round((r.present.length / total) * 100);
|
|
209
301
|
const scoreColor = score === 100 ? col.boldGreen : score >= 70 ? col.boldYellow : col.boldRed;
|
|
210
302
|
|
|
211
303
|
console.log(col.bold('🏥 Dependency Health Report'));
|
|
@@ -219,7 +311,6 @@ async function cmdAudit(targetDir) {
|
|
|
219
311
|
console.log(` ~ Unused declared : ${r.unused.length > 0 ? col.boldYellow(String(r.unused.length)) : col.boldGreen('0')}`);
|
|
220
312
|
console.log();
|
|
221
313
|
|
|
222
|
-
// Dependency breakdown
|
|
223
314
|
const deps = r.packageJson?.dependencies || {};
|
|
224
315
|
const devDeps = r.packageJson?.devDependencies || {};
|
|
225
316
|
const peer = r.packageJson?.peerDependencies || {};
|
|
@@ -233,15 +324,14 @@ async function cmdAudit(targetDir) {
|
|
|
233
324
|
console.log(` optionalDependencies : ${Object.keys(opt).length}`);
|
|
234
325
|
console.log();
|
|
235
326
|
|
|
236
|
-
// Recommendations
|
|
237
327
|
const recs = [];
|
|
238
328
|
if (r.missing.length > 0) recs.push(`Run ${col.cyan('reqscan install')} to install ${r.missing.length} missing package(s).`);
|
|
239
|
-
if (r.unused.length > 0) recs.push(`Run ${col.cyan('reqscan clean')} to remove ${r.unused.length} unused package(s).`);
|
|
329
|
+
if (r.unused.length > 0) recs.push(`Run ${col.cyan('reqscan clean')} to remove ${r.unused.length} potentially unused package(s).`);
|
|
240
330
|
if (recs.length === 0) recs.push(col.green('Your project dependencies look great!'));
|
|
241
331
|
|
|
242
332
|
console.log(col.bold('💡 Recommendations'));
|
|
243
333
|
console.log(col.dim(col.LINE));
|
|
244
|
-
recs.forEach(
|
|
334
|
+
recs.forEach(rec => console.log(` → ${rec}`));
|
|
245
335
|
console.log();
|
|
246
336
|
console.log(col.dim(col.LINE));
|
|
247
337
|
console.log(col.dim('Audit complete.'));
|
|
@@ -252,39 +342,51 @@ async function cmdAudit(targetDir) {
|
|
|
252
342
|
|
|
253
343
|
// ─── list ─────────────────────────────────────────────────────────────────────
|
|
254
344
|
|
|
255
|
-
async function cmdList(targetDir) {
|
|
345
|
+
async function cmdList(targetDir, { json = false } = {}) {
|
|
256
346
|
const r = await scanProject(targetDir);
|
|
347
|
+
|
|
348
|
+
if (json) {
|
|
349
|
+
const all = [...new Set([...r.allImports, ...r.declared])].sort();
|
|
350
|
+
const list = all.map(pkg => ({
|
|
351
|
+
name : pkg,
|
|
352
|
+
status : r.allImports.includes(pkg) && r.declared.includes(pkg) ? 'present'
|
|
353
|
+
: r.allImports.includes(pkg) ? 'missing' : 'unused',
|
|
354
|
+
version : getVersion(r.packageJson, pkg),
|
|
355
|
+
type : getDepType(r.packageJson, pkg),
|
|
356
|
+
}));
|
|
357
|
+
console.log(JSON.stringify({ project: r.projectName, packages: list }, null, 2));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
257
361
|
header(r.projectName);
|
|
258
362
|
|
|
259
363
|
console.log(col.bold('📋 All Dependencies with Status'));
|
|
260
364
|
console.log(col.dim(col.LINE));
|
|
261
365
|
|
|
262
|
-
const all
|
|
366
|
+
const all = new Set([...r.allImports, ...r.declared]);
|
|
263
367
|
const sorted = [...all].sort();
|
|
264
368
|
|
|
265
369
|
sorted.forEach(pkg => {
|
|
266
|
-
const imported
|
|
267
|
-
const declared
|
|
268
|
-
const ver
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
r.packageJson?.peerDependencies?.[pkg] ||
|
|
272
|
-
r.packageJson?.optionalDependencies?.[pkg] || '';
|
|
370
|
+
const imported = r.allImports.includes(pkg);
|
|
371
|
+
const declared = r.declared.includes(pkg);
|
|
372
|
+
const ver = getVersion(r.packageJson, pkg);
|
|
373
|
+
const type = getDepType(r.packageJson, pkg);
|
|
374
|
+
const typeTag = type === 'devDependencies' ? col.dim(' [dev]') : '';
|
|
273
375
|
|
|
274
376
|
let status, icon;
|
|
275
377
|
if (imported && declared) { icon = col.green('✓'); status = col.dim('present'); }
|
|
276
378
|
else if (imported) { icon = col.red('✗'); status = col.red('missing'); }
|
|
277
379
|
else { icon = col.yellow('~'); status = col.yellow('unused'); }
|
|
278
380
|
|
|
279
|
-
console.log(` ${icon} ${pkg.padEnd(30)} ${col.dim(ver.padEnd(12))} ${status}`);
|
|
381
|
+
console.log(` ${icon} ${pkg.padEnd(30)} ${col.dim(ver.padEnd(12))} ${status}${typeTag}`);
|
|
280
382
|
});
|
|
281
383
|
|
|
282
384
|
console.log();
|
|
283
|
-
console.log(col.dim(`Legend: ${col.green('✓')} present ${col.red('✗')} missing ${col.yellow('~')} declared but unused`));
|
|
385
|
+
console.log(col.dim(`Legend: ${col.green('✓')} present ${col.red('✗')} missing ${col.yellow('~')} declared but unused ${col.dim('[dev]')} devDependency`));
|
|
284
386
|
console.log();
|
|
285
387
|
console.log(col.dim(col.LINE));
|
|
286
388
|
console.log(col.dim('List complete.'));
|
|
287
389
|
console.log();
|
|
288
390
|
}
|
|
289
391
|
|
|
290
|
-
module.exports = { cmdCheck, cmdInstall, cmdClean, cmdFix, cmdAudit, cmdList };
|
|
392
|
+
module.exports = { cmdCheck, cmdInstall, cmdClean, cmdFix, cmdAudit, cmdList };
|
package/src/index.js
CHANGED
|
@@ -7,9 +7,21 @@ const col = require('./colors');
|
|
|
7
7
|
const { cmdCheck, cmdInstall, cmdClean, cmdFix, cmdAudit, cmdList } = require('./commands');
|
|
8
8
|
|
|
9
9
|
// ─── Parse args ───────────────────────────────────────────────────────────────
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const argv = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const nonFlags = argv.filter(arg => !arg.startsWith('-'));
|
|
14
|
+
const command = nonFlags[0] ?? null;
|
|
15
|
+
const dirArg = nonFlags[1] ?? null;
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const flags = {
|
|
20
|
+
json : argv.includes('--json'),
|
|
21
|
+
saveDev : argv.includes('--save-dev'),
|
|
22
|
+
dryRun : argv.includes('--dry-run'),
|
|
23
|
+
force : argv.includes('--force'),
|
|
24
|
+
};
|
|
13
25
|
|
|
14
26
|
// ─── Help text ────────────────────────────────────────────────────────────────
|
|
15
27
|
function printHelp() {
|
|
@@ -17,7 +29,7 @@ function printHelp() {
|
|
|
17
29
|
console.log(col.boldCyan('reqscan') + col.bold(' — Dependency Manager for Node.js'));
|
|
18
30
|
console.log();
|
|
19
31
|
console.log(col.bold('Usage:'));
|
|
20
|
-
console.log(' reqscan <command> [project-dir]');
|
|
32
|
+
console.log(' reqscan <command> [project-dir] [flags]');
|
|
21
33
|
console.log();
|
|
22
34
|
console.log(col.bold('Commands:'));
|
|
23
35
|
const cmds = [
|
|
@@ -32,20 +44,32 @@ function printHelp() {
|
|
|
32
44
|
console.log(` ${col.cyan(cmd.padEnd(20))} ${desc}`);
|
|
33
45
|
});
|
|
34
46
|
console.log();
|
|
47
|
+
console.log(col.bold('Flags:'));
|
|
48
|
+
const flags_help = [
|
|
49
|
+
['--json', 'Output results as JSON (check, audit, list)'],
|
|
50
|
+
['--save-dev', 'Install missing packages as devDependencies'],
|
|
51
|
+
['--dry-run', 'Preview what would be installed/removed'],
|
|
52
|
+
['--force', 'Skip confirmation prompt (clean, fix)'],
|
|
53
|
+
];
|
|
54
|
+
flags_help.forEach(([flag, desc]) => {
|
|
55
|
+
console.log(` ${col.cyan(flag.padEnd(20))} ${desc}`);
|
|
56
|
+
});
|
|
57
|
+
console.log();
|
|
35
58
|
console.log(col.bold('Examples:'));
|
|
36
59
|
console.log(` ${col.dim('reqscan check')} # scan current directory`);
|
|
37
60
|
console.log(` ${col.dim('reqscan check ./my-app')} # scan specific folder`);
|
|
38
|
-
console.log(` ${col.dim('reqscan
|
|
39
|
-
console.log(` ${col.dim('reqscan
|
|
40
|
-
console.log(` ${col.dim('reqscan
|
|
41
|
-
console.log(` ${col.dim('reqscan
|
|
61
|
+
console.log(` ${col.dim('reqscan check --json')} # output as JSON`);
|
|
62
|
+
console.log(` ${col.dim('reqscan install --save-dev')} # install as devDependencies`);
|
|
63
|
+
console.log(` ${col.dim('reqscan install --dry-run')} # preview install`);
|
|
64
|
+
console.log(` ${col.dim('reqscan clean --force')} # remove unused without prompt`);
|
|
65
|
+
console.log(` ${col.dim('reqscan audit --json')} # full report as JSON`);
|
|
42
66
|
console.log(` ${col.dim('reqscan list')} # show all with status`);
|
|
43
67
|
console.log();
|
|
44
68
|
}
|
|
45
69
|
|
|
46
70
|
// ─── Resolve target directory ─────────────────────────────────────────────────
|
|
47
|
-
function resolveDir(
|
|
48
|
-
const target =
|
|
71
|
+
function resolveDir(arg) {
|
|
72
|
+
const target = arg ? path.resolve(arg) : process.cwd();
|
|
49
73
|
if (!fs.existsSync(target)) {
|
|
50
74
|
console.error(col.boldRed(`❌ Directory not found: ${target}`));
|
|
51
75
|
process.exit(1);
|
|
@@ -74,19 +98,21 @@ function resolveDir(dirArg) {
|
|
|
74
98
|
|
|
75
99
|
const targetDir = resolveDir(command === 'check' ? dirArg : (dirArg || undefined));
|
|
76
100
|
|
|
77
|
-
|
|
101
|
+
if (!flags.json) {
|
|
102
|
+
console.log(col.dim(`\n🔍 Scanning project at: ${targetDir}`));
|
|
103
|
+
}
|
|
78
104
|
|
|
79
105
|
try {
|
|
80
106
|
switch (command) {
|
|
81
|
-
case 'check' : await cmdCheck(targetDir);
|
|
82
|
-
case 'install': await cmdInstall(targetDir); break;
|
|
83
|
-
case 'clean' : await cmdClean(targetDir);
|
|
84
|
-
case 'fix' : await cmdFix(targetDir);
|
|
85
|
-
case 'audit' : await cmdAudit(targetDir);
|
|
86
|
-
case 'list' : await cmdList(targetDir);
|
|
107
|
+
case 'check' : await cmdCheck(targetDir, flags); break;
|
|
108
|
+
case 'install': await cmdInstall(targetDir, flags); break;
|
|
109
|
+
case 'clean' : await cmdClean(targetDir, flags); break;
|
|
110
|
+
case 'fix' : await cmdFix(targetDir, flags); break;
|
|
111
|
+
case 'audit' : await cmdAudit(targetDir, flags); break;
|
|
112
|
+
case 'list' : await cmdList(targetDir, flags); break;
|
|
87
113
|
}
|
|
88
114
|
} catch (err) {
|
|
89
115
|
console.error(col.boldRed(`\n❌ Error: ${err.message}`));
|
|
90
116
|
process.exit(1);
|
|
91
117
|
}
|
|
92
|
-
})();
|
|
118
|
+
})();
|
package/src/scanner.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
-
// ─── Node.js built-in modules
|
|
6
|
+
// ─── Node.js built-in modules ─────────────────────────────────────────────────
|
|
7
7
|
const BUILTIN_MODULES = new Set([
|
|
8
8
|
'assert','async_hooks','buffer','child_process','cluster','console',
|
|
9
9
|
'constants','crypto','dgram','diagnostics_channel','dns','domain',
|
|
@@ -13,7 +13,6 @@ const BUILTIN_MODULES = new Set([
|
|
|
13
13
|
'stream/consumers','stream/promises','stream/web','string_decoder',
|
|
14
14
|
'sys','timers','timers/promises','tls','trace_events','tty','url',
|
|
15
15
|
'util','util/types','v8','vm','wasi','worker_threads','zlib',
|
|
16
|
-
// node: prefix variants
|
|
17
16
|
'node:assert','node:buffer','node:child_process','node:cluster',
|
|
18
17
|
'node:crypto','node:dgram','node:dns','node:domain','node:events',
|
|
19
18
|
'node:fs','node:http','node:http2','node:https','node:inspector',
|
|
@@ -23,57 +22,74 @@ const BUILTIN_MODULES = new Set([
|
|
|
23
22
|
'node:vm','node:worker_threads','node:zlib'
|
|
24
23
|
]);
|
|
25
24
|
|
|
26
|
-
// ─── Directories to skip
|
|
25
|
+
// ─── Directories to skip ──────────────────────────────────────────────────────
|
|
27
26
|
const IGNORE_DIRS = new Set([
|
|
28
27
|
'node_modules','.git','.next','.nuxt','dist','build',
|
|
29
28
|
'coverage','.cache','.turbo','out','.svelte-kit','.vercel',
|
|
30
|
-
'.output','public','static','assets'
|
|
29
|
+
'.output','public','static','assets','.idea','.vscode'
|
|
31
30
|
]);
|
|
32
31
|
|
|
33
|
-
// ─── File extensions to scan
|
|
32
|
+
// ─── File extensions to scan ──────────────────────────────────────────────────
|
|
34
33
|
const JS_EXTENSIONS = new Set(['.js','.jsx','.mjs','.cjs','.ts','.tsx','.mts','.cts']);
|
|
35
34
|
|
|
36
|
-
// ─── Import/require patterns
|
|
35
|
+
// ─── Import/require patterns ──────────────────────────────────────────────────
|
|
37
36
|
const IMPORT_PATTERNS = [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
// require('pkg')
|
|
38
|
+
/\brequire\s*\(\s*['"`]([^'"`\s]+)['"`]\s*\)/g,
|
|
39
|
+
// require.resolve('pkg')
|
|
40
|
+
/\brequire\.resolve\s*\(\s*['"`]([^'"`\s]+)['"`]\s*\)/g,
|
|
41
|
+
// import 'pkg' (side-effect)
|
|
42
|
+
/\bimport\s+['"`]([^'"`\s]+)['"`]/g,
|
|
43
|
+
// import [type] [...stuff...] from 'pkg'
|
|
44
|
+
/\bimport\s+(?:type\s+)?(?:[\w$*{},\s]+\s+from\s+)['"`]([^'"`\s]+)['"`]/g,
|
|
45
|
+
// import('pkg') dynamic
|
|
46
|
+
/\bimport\s*\(\s*['"`]([^'"`\s]+)['"`]\s*\)/g,
|
|
47
|
+
// export [type] [...stuff...] from 'pkg'
|
|
48
|
+
/\bexport\s+(?:type\s+)?(?:[\w$*{},\s]+\s+from\s+)['"`]([^'"`\s]+)['"`]/g,
|
|
43
49
|
];
|
|
44
50
|
|
|
45
51
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
46
52
|
|
|
47
53
|
function extractPackageName(raw) {
|
|
48
54
|
if (!raw) return null;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
if (
|
|
56
|
+
raw.startsWith('.') || raw.startsWith('/') ||
|
|
57
|
+
/^https?:\/\//.test(raw) ||
|
|
58
|
+
raw.startsWith('data:') || raw.startsWith('blob:')
|
|
59
|
+
) return null;
|
|
52
60
|
if (raw.startsWith('@')) {
|
|
53
61
|
const parts = raw.split('/');
|
|
54
62
|
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
55
63
|
}
|
|
56
|
-
|
|
57
|
-
return raw.split('/')[0];
|
|
64
|
+
return raw.split('/')[0] || null;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
function extractImportsFromFile(filePath) {
|
|
61
68
|
const found = new Set();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
let src;
|
|
70
|
+
try { src = fs.readFileSync(filePath, 'utf8'); }
|
|
71
|
+
catch (_) { return found; }
|
|
72
|
+
|
|
73
|
+
// Strip comments to avoid matching commented-out imports
|
|
74
|
+
const stripped = src
|
|
75
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
76
|
+
.replace(/\/\/[^\n]*/g, ' ');
|
|
77
|
+
|
|
78
|
+
for (const re of IMPORT_PATTERNS) {
|
|
79
|
+
re.lastIndex = 0;
|
|
80
|
+
let m;
|
|
81
|
+
while ((m = re.exec(stripped)) !== null) {
|
|
82
|
+
const pkg = extractPackageName(m[1]);
|
|
83
|
+
if (pkg && !BUILTIN_MODULES.has(pkg)) found.add(pkg);
|
|
71
84
|
}
|
|
72
|
-
}
|
|
85
|
+
}
|
|
73
86
|
return found;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
|
|
89
|
+
const MAX_DEPTH = 20;
|
|
90
|
+
|
|
91
|
+
function walkDir(dir, allImports = new Set(), scannedFiles = [], depth = 0) {
|
|
92
|
+
if (depth > MAX_DEPTH) return { allImports, scannedFiles };
|
|
77
93
|
let entries;
|
|
78
94
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
79
95
|
catch (_) { return { allImports, scannedFiles }; }
|
|
@@ -82,7 +98,7 @@ function walkDir(dir, allImports = new Set(), scannedFiles = []) {
|
|
|
82
98
|
if (IGNORE_DIRS.has(e.name)) continue;
|
|
83
99
|
const full = path.join(dir, e.name);
|
|
84
100
|
if (e.isDirectory()) {
|
|
85
|
-
walkDir(full, allImports, scannedFiles);
|
|
101
|
+
walkDir(full, allImports, scannedFiles, depth + 1);
|
|
86
102
|
} else if (e.isFile() && JS_EXTENSIONS.has(path.extname(e.name))) {
|
|
87
103
|
scannedFiles.push(full);
|
|
88
104
|
for (const pkg of extractImportsFromFile(full)) allImports.add(pkg);
|
|
@@ -98,8 +114,6 @@ function readPackageJson(dir) {
|
|
|
98
114
|
catch (_) { return null; }
|
|
99
115
|
}
|
|
100
116
|
|
|
101
|
-
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
117
|
async function scanProject(targetDir) {
|
|
104
118
|
const pkgJson = readPackageJson(targetDir);
|
|
105
119
|
const { allImports, scannedFiles } = walkDir(targetDir);
|
|
@@ -111,24 +125,19 @@ async function scanProject(targetDir) {
|
|
|
111
125
|
...Object.keys(pkgJson?.optionalDependencies|| {}),
|
|
112
126
|
]);
|
|
113
127
|
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const
|
|
128
|
+
// Sort once, then derive all three arrays in a single pass each
|
|
129
|
+
const allImportsSorted = [...allImports].sort();
|
|
130
|
+
const declaredSorted = [...declared].sort();
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
for (const pkg of [...declared].sort()) {
|
|
124
|
-
if (!allImports.has(pkg)) unused.push(pkg);
|
|
125
|
-
}
|
|
132
|
+
const missing = allImportsSorted.filter(p => !declared.has(p));
|
|
133
|
+
const present = allImportsSorted.filter(p => declared.has(p));
|
|
134
|
+
const unused = declaredSorted.filter(p => !allImports.has(p));
|
|
126
135
|
|
|
127
136
|
return {
|
|
128
|
-
projectName
|
|
129
|
-
packageJson
|
|
130
|
-
allImports
|
|
131
|
-
declared
|
|
137
|
+
projectName : pkgJson?.name || path.basename(targetDir),
|
|
138
|
+
packageJson : pkgJson,
|
|
139
|
+
allImports : allImportsSorted,
|
|
140
|
+
declared : declaredSorted,
|
|
132
141
|
missing,
|
|
133
142
|
present,
|
|
134
143
|
unused,
|
|
@@ -137,4 +146,4 @@ async function scanProject(targetDir) {
|
|
|
137
146
|
};
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
module.exports = { scanProject, readPackageJson };
|
|
149
|
+
module.exports = { scanProject, readPackageJson };
|