helpforbegs 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +146 -2
- package/bin/cli.js +108 -0
- package/package.json +43 -4
- package/src/analyzer.js +348 -0
- package/src/args.js +116 -0
- package/src/astParser.js +228 -0
- package/src/binMapper.js +167 -0
- package/src/configReader.js +351 -0
- package/src/fallbackParser.js +130 -0
- package/src/fileFinder.js +196 -0
- package/src/messages.js +82 -0
- package/src/remover.js +184 -0
- package/src/reporter.js +186 -0
- package/src/safeList.js +91 -0
- package/src/scorer.js +150 -0
- package/src/why.js +103 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 helpforbegs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,147 @@
|
|
|
1
|
-
#
|
|
1
|
+
# helpforbegs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Dependency help for beginners. No jargon, just plain English.**
|
|
4
|
+
|
|
5
|
+
A gentle, accurate command-line assistant that scans your Node.js project and tells you:
|
|
6
|
+
- Which packages you installed but never use.
|
|
7
|
+
- Which packages you use in your code but forgot to install.
|
|
8
|
+
- How much lighter and cleaner your project will be after cleaning up.
|
|
9
|
+
|
|
10
|
+
Zero build steps. Zero TypeScript compilation needed. Safe by default.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 📸 Demo Output
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
🔍 Scanning your project... (42 files read)
|
|
18
|
+
|
|
19
|
+
❌ Installed but never used (2)
|
|
20
|
+
• lodash (1.4 MB) You installed lodash but never used it.
|
|
21
|
+
• moment (4.2 MB) You installed moment but never used it.
|
|
22
|
+
|
|
23
|
+
🤔 Probably unused (1)
|
|
24
|
+
• cors Known development tool, but no active script or config reference was detected
|
|
25
|
+
|
|
26
|
+
⚠️ Used but not installed (1)
|
|
27
|
+
• axios Used in src/api.js:3 but missing from package.json.
|
|
28
|
+
|
|
29
|
+
✅ 9 packages are used correctly
|
|
30
|
+
|
|
31
|
+
📊 Health score: 78/100 (B) "Pretty tidy, a few dusty corners."
|
|
32
|
+
💾 You could free about 5.6 MB.
|
|
33
|
+
|
|
34
|
+
💡 Run `npx helpforbegs remove` to clean up safely.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ⚡ Quick Start
|
|
40
|
+
|
|
41
|
+
You don't even need to install it permanently! Just run `npx` inside any Node.js project:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx helpforbegs
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or install it globally if you want to use it anytime:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g helpforbegs
|
|
51
|
+
helpforbegs
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🛠️ Commands
|
|
57
|
+
|
|
58
|
+
| Command | What it does |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `helpforbegs` or `helpforbegs scan` | Runs a complete project scan and prints the friendly health report. |
|
|
61
|
+
| `helpforbegs remove` | Displays unused packages, prompts for confirmation (`y/N`), creates a timestamped backup of `package.json`, and safely uninstalls them. |
|
|
62
|
+
| `helpforbegs why <package>` | Explains in plain English with exact file paths and line numbers where a package is used, or why it is considered unused. |
|
|
63
|
+
| `helpforbegs score` | Prints only the project health score, letter grade, and summary comment (great for CI or quick checks). |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🚩 Flags & Options
|
|
68
|
+
|
|
69
|
+
- `--path <dir>`: Scan another folder instead of the current directory (e.g. `npx helpforbegs --path ../my-express-app`).
|
|
70
|
+
- `--dry-run`: Test `helpforbegs remove` safely without touching or deleting any files.
|
|
71
|
+
- `--yes` or `-y`: Automatically confirm uninstallation in `remove` mode (required in CI or non-interactive terminals).
|
|
72
|
+
- `--roast`: Enable humorous, playfully sarcastic feedback on your dependency habits.
|
|
73
|
+
- `--json`: Output raw JSON data for CI pipelines, scripts, or custom dashboards.
|
|
74
|
+
- `--no-color`: Disable all ANSI terminal colors (automatically respected if `NO_COLOR` is set or when output is redirected to a file).
|
|
75
|
+
- `--help` (`-h`): Display the CLI help screen.
|
|
76
|
+
- `--version` (`-v`): Show the installed version.
|
|
77
|
+
- `--debug`: Display full technical stack traces on unexpected errors.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🧠 How Detection Works (In Plain English)
|
|
82
|
+
|
|
83
|
+
Many tools use simple text searches or regular expressions to find package names, which often leads to mistakes. `helpforbegs` works differently:
|
|
84
|
+
|
|
85
|
+
1. **Real AST Code Parsing**: We parse your JavaScript and TypeScript files using `@babel/parser`. This means we inspect the actual syntax tree of your code. We recognize:
|
|
86
|
+
- CommonJS `require('pkg')` and `require.resolve('pkg')`
|
|
87
|
+
- ES Module `import ... from 'pkg'`, `import 'pkg'`, and `export ... from 'pkg'`
|
|
88
|
+
- Dynamic `import('pkg')`
|
|
89
|
+
- TypeScript `import type { ... } from 'pkg'`
|
|
90
|
+
2. **Package Name Normalization**: We strip subpaths (`lodash/fp` $\rightarrow$ `lodash`, `@babel/parser/lib` $\rightarrow$ `@babel/parser`), ignore relative paths (`./utils`), and filter out Node.js built-ins (`fs`, `path`, `node:test`, `crypto`).
|
|
91
|
+
3. **Path Alias Intelligence**: We read path aliases in `tsconfig.json`, `jsconfig.json` (`compilerOptions.paths`), and `package.json` (`imports`). Imports like `@/components/Button` or `~/helpers` are recognized as internal aliases and are never flagged as missing npm packages.
|
|
92
|
+
4. **Script Execution (`bin` commands)**: When you install CLI packages like `nodemon` or `rimraf`, you don't `require` them in code—you run them in `package.json` scripts. We read the `bin` field of packages in `node_modules` and match them against your script commands.
|
|
93
|
+
5. **Config Files**: We check ESLint configs, Prettier plugins, Babel presets, Tailwind, PostCSS, and `tsconfig.json` compiler types.
|
|
94
|
+
6. **Peer Dependencies**: If another installed package requires a dependency as a `peerDependency`, we will never mark it as unused.
|
|
95
|
+
7. **Safe-List for Dev Tools**: Common development tools (`typescript`, `eslint`, `prettier`, `jest`, `nodemon`, `husky`, etc.) are recognized so you aren't pressured to delete your build pipeline.
|
|
96
|
+
8. **Special Dotenv Rule**: `dotenv` is only flagged as unused if it is neither imported in code nor passed as a `-r dotenv/config` flag in scripts.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🛡️ Why You Can Trust the Results
|
|
101
|
+
|
|
102
|
+
False positives are dangerous: telling a beginner to uninstall a required package could break their project.
|
|
103
|
+
|
|
104
|
+
`helpforbegs` follows strict safety rules:
|
|
105
|
+
1. **Never guess when unsure**: When confidence is not 100%, packages are marked `"Probably unused"` with an explanation to double-check, and `helpforbegs remove` will never delete them automatically.
|
|
106
|
+
2. **Timestamped backups**: Before modifying `package.json`, a backup named `package.json.helpforbegs.<timestamp>.bak` is created.
|
|
107
|
+
3. **Respects your package manager**: Automatically detects whether you use `npm`, `yarn`, `pnpm`, or `bun` by checking lockfiles, so your lockfile stays consistent.
|
|
108
|
+
4. **Large files protection**: Skips files larger than 1 MB to prevent memory bloat, and informs you in the report.
|
|
109
|
+
5. **Graceful syntax recovery**: If a file contains syntax errors that fail AST parsing, it falls back to a regex scanner and notes: `"Could not fully read src/x.js"`.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 🙈 Ignoring Packages (`.helpforbegsignore`)
|
|
114
|
+
|
|
115
|
+
If you have a package that is used in a way automated tools cannot see, create a `.helpforbegsignore` file in your project root:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
# .helpforbegsignore
|
|
119
|
+
# Packages listed here will never be flagged as unused
|
|
120
|
+
|
|
121
|
+
cors
|
|
122
|
+
dotenv
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## ⚠️ Known Limitations
|
|
128
|
+
|
|
129
|
+
1. **Dynamic require variables**: Expressions like `require(myVariableName)` cannot be statically analyzed because their value is only known while your app is running.
|
|
130
|
+
2. **Shell scripts outside package.json**: Tools invoked exclusively inside custom `.sh` or `.bat` files outside `package.json` scripts won't be seen unless listed in `.helpforbegsignore`.
|
|
131
|
+
3. **Code generated at runtime**: Dynamically evaluated code (e.g. `eval()` or code fetched over a network) cannot be scanned statically.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 🤝 Contributing
|
|
136
|
+
|
|
137
|
+
Contributions are welcome!
|
|
138
|
+
1. Fork the repository.
|
|
139
|
+
2. Clone your fork and run `npm install`.
|
|
140
|
+
3. Run tests with `npm test`.
|
|
141
|
+
4. Submit a Pull Request.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 📄 License
|
|
146
|
+
|
|
147
|
+
MIT © helpforbegs contributors
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/cli.js
|
|
4
|
+
// WHY: This is the executable entry point when someone runs `helpforbegs`
|
|
5
|
+
// or `npx helpforbegs` in their terminal.
|
|
6
|
+
// It coordinates argument parsing, routing to commands, and clean exit codes.
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { parseArgs } = require('../src/args');
|
|
10
|
+
const { analyzeProject } = require('../src/analyzer');
|
|
11
|
+
const { calculateScoreAndSavings } = require('../src/scorer');
|
|
12
|
+
const { reportResults } = require('../src/reporter');
|
|
13
|
+
const { explainPackage } = require('../src/why');
|
|
14
|
+
const { removeUnused } = require('../src/remover');
|
|
15
|
+
const pkgInfo = require('../package.json');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prints user-friendly help documentation.
|
|
19
|
+
*/
|
|
20
|
+
function printHelp() {
|
|
21
|
+
console.log(`
|
|
22
|
+
helpforbegs - ${pkgInfo.description}
|
|
23
|
+
|
|
24
|
+
USAGE:
|
|
25
|
+
$ npx helpforbegs [command] [options]
|
|
26
|
+
|
|
27
|
+
COMMANDS:
|
|
28
|
+
scan (default) Scans your project and prints the health report
|
|
29
|
+
remove Safely uninstalls unused packages after confirmation
|
|
30
|
+
why <package> Explains exactly where a package is used, or why it is unused
|
|
31
|
+
score Prints only the health score and letter grade
|
|
32
|
+
|
|
33
|
+
FLAGS:
|
|
34
|
+
--path <dir> Scan another directory (default: current directory)
|
|
35
|
+
--dry-run Preview what \`remove\` would uninstall without changing files
|
|
36
|
+
--yes, -y Confirm \`remove\` automatically (required in CI / non-TTY)
|
|
37
|
+
--roast Funny, gently sarcastic commentary
|
|
38
|
+
--json Output raw JSON data for scripts and CI
|
|
39
|
+
--no-color Disable ANSI terminal colors
|
|
40
|
+
--help, -h Show this help screen
|
|
41
|
+
--version, -v Show the version of helpforbegs
|
|
42
|
+
--debug Show technical stack traces on unexpected errors
|
|
43
|
+
|
|
44
|
+
EXAMPLES:
|
|
45
|
+
$ npx helpforbegs
|
|
46
|
+
$ npx helpforbegs --roast
|
|
47
|
+
$ npx helpforbegs why lodash
|
|
48
|
+
$ npx helpforbegs remove --dry-run
|
|
49
|
+
$ npx helpforbegs --path ./my-app
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
const { command, targetPkg, flags } = parseArgs(process.argv.slice(2));
|
|
55
|
+
|
|
56
|
+
// 1. --help / -h
|
|
57
|
+
if (flags.help) {
|
|
58
|
+
printHelp();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. --version / -v
|
|
63
|
+
if (flags.version) {
|
|
64
|
+
console.log(pkgInfo.version);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const targetDir = flags.targetDir;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (command === 'why') {
|
|
72
|
+
const exitCode = explainPackage(targetDir, targetPkg, flags);
|
|
73
|
+
process.exit(exitCode);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (command === 'remove') {
|
|
77
|
+
const exitCode = await removeUnused(targetDir, flags);
|
|
78
|
+
process.exit(exitCode);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default: 'scan' or 'score'
|
|
82
|
+
const analysis = analyzeProject(targetDir);
|
|
83
|
+
const scoreData = calculateScoreAndSavings(analysis, { roast: flags.roast });
|
|
84
|
+
|
|
85
|
+
const exitCode = reportResults(analysis, scoreData, {
|
|
86
|
+
...flags,
|
|
87
|
+
scoreOnly: command === 'score',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
process.exit(exitCode);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Exit code 2 for internal/input errors (no package.json, invalid JSON, etc.)
|
|
93
|
+
if (err.code === 'NO_PKG_JSON' || err.code === 'INVALID_PKG_JSON') {
|
|
94
|
+
console.error(`\n❌ ${err.message}\n`);
|
|
95
|
+
process.exit(2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.error(`\n❌ An unexpected error occurred: ${err.message}`);
|
|
99
|
+
if (flags.debug) {
|
|
100
|
+
console.error(err.stack);
|
|
101
|
+
} else {
|
|
102
|
+
console.error(' Run with `--debug` to view the full technical stack trace.\n');
|
|
103
|
+
}
|
|
104
|
+
process.exit(2);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helpforbegs",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Dependency help for beginners. No jargon, just plain English.",
|
|
5
|
+
"main": "src/analyzer.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"helpforbegs": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "jest"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/your-username/helpforbegs.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/your-username/helpforbegs/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/your-username/helpforbegs#readme",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"dependencies",
|
|
26
|
+
"unused",
|
|
27
|
+
"cleanup",
|
|
28
|
+
"depcheck",
|
|
29
|
+
"beginner",
|
|
30
|
+
"cli",
|
|
31
|
+
"package-json"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"author": "Shriyan Kogta",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@babel/parser": "^7.24.0",
|
|
40
|
+
"chalk": "^4.1.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"jest": "^29.7.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/analyzer.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// src/analyzer.js
|
|
2
|
+
// WHY: This is the brain of helpforbegs. It collects findings from all our scanners
|
|
3
|
+
// (AST parser, config reader, scripts, bin mapper, peer dependencies, and safe-list)
|
|
4
|
+
// and sorts every package into one of four clear verdict buckets:
|
|
5
|
+
// 1. Used
|
|
6
|
+
// 2. Unused (high confidence)
|
|
7
|
+
// 3. Probably unused (tooling with uncertainty)
|
|
8
|
+
// 4. Missing (imported in code but forgotten in package.json)
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { findFiles } = require('./fileFinder');
|
|
13
|
+
const { parseImports } = require('./astParser');
|
|
14
|
+
const {
|
|
15
|
+
extractPathAliases,
|
|
16
|
+
readIgnoreFile,
|
|
17
|
+
collectPeerDependencies,
|
|
18
|
+
scanConfigFiles,
|
|
19
|
+
checkDotenvInScripts,
|
|
20
|
+
} = require('./configReader');
|
|
21
|
+
const { mapInstalledBins, findBinUsageInScripts } = require('./binMapper');
|
|
22
|
+
const { isSafeListed } = require('./safeList');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolves monorepo workspace directories if "workspaces" is defined in package.json.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} projectDir
|
|
28
|
+
* @param {string[]|object} workspacesConfig
|
|
29
|
+
* @returns {string[]} List of absolute paths to workspace packages
|
|
30
|
+
*/
|
|
31
|
+
function resolveWorkspaces(projectDir, workspacesConfig) {
|
|
32
|
+
const patterns = Array.isArray(workspacesConfig)
|
|
33
|
+
? workspacesConfig
|
|
34
|
+
: (workspacesConfig && Array.isArray(workspacesConfig.packages) ? workspacesConfig.packages : []);
|
|
35
|
+
|
|
36
|
+
const workspaceDirs = [];
|
|
37
|
+
|
|
38
|
+
for (const pattern of patterns) {
|
|
39
|
+
if (pattern.endsWith('/*')) {
|
|
40
|
+
// e.g. "packages/*"
|
|
41
|
+
const parentDir = path.join(projectDir, pattern.slice(0, -2));
|
|
42
|
+
if (fs.existsSync(parentDir)) {
|
|
43
|
+
try {
|
|
44
|
+
const entries = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
const subDir = path.join(parentDir, entry.name);
|
|
48
|
+
if (fs.existsSync(path.join(subDir, 'package.json'))) {
|
|
49
|
+
workspaceDirs.push(subDir);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignore directory read errors
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// Direct directory path, e.g. "apps/web"
|
|
59
|
+
const directDir = path.join(projectDir, pattern);
|
|
60
|
+
if (fs.existsSync(path.join(directDir, 'package.json'))) {
|
|
61
|
+
workspaceDirs.push(directDir);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return workspaceDirs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Main analysis function that scans a project folder and categorizes all dependencies.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
73
|
+
* @param {object} [options]
|
|
74
|
+
* @returns {object} Full analysis results
|
|
75
|
+
*/
|
|
76
|
+
function analyzeProject(projectDir, options = {}) {
|
|
77
|
+
const pkgJsonPath = path.join(projectDir, 'package.json');
|
|
78
|
+
|
|
79
|
+
// Verify that package.json exists
|
|
80
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
81
|
+
const error = new Error(`No package.json found in ${projectDir}. Make sure you are in a Node.js project folder.`);
|
|
82
|
+
error.code = 'NO_PKG_JSON';
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse package.json
|
|
87
|
+
let rootPkgJson;
|
|
88
|
+
try {
|
|
89
|
+
const content = fs.readFileSync(pkgJsonPath, 'utf8');
|
|
90
|
+
rootPkgJson = JSON.parse(content);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const error = new Error(`Could not read package.json in ${projectDir}. It contains invalid JSON syntax.`);
|
|
93
|
+
error.code = 'INVALID_PKG_JSON';
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for monorepo workspaces
|
|
98
|
+
const workspaceDirs = rootPkgJson.workspaces ? resolveWorkspaces(projectDir, rootPkgJson.workspaces) : [];
|
|
99
|
+
const isMonorepo = workspaceDirs.length > 0;
|
|
100
|
+
|
|
101
|
+
// Aggregate all declared dependencies across root and workspaces
|
|
102
|
+
const declaredDeps = new Map(); // pkgName -> { isDev: boolean, definedIn: string }
|
|
103
|
+
const workspacePkgNames = new Set();
|
|
104
|
+
|
|
105
|
+
function registerDeps(pkgData, sourcePath) {
|
|
106
|
+
if (pkgData.name) {
|
|
107
|
+
workspacePkgNames.add(pkgData.name);
|
|
108
|
+
}
|
|
109
|
+
const regularDeps = pkgData.dependencies || {};
|
|
110
|
+
const devDeps = pkgData.devDependencies || {};
|
|
111
|
+
|
|
112
|
+
for (const dep of Object.keys(regularDeps)) {
|
|
113
|
+
if (!declaredDeps.has(dep)) {
|
|
114
|
+
declaredDeps.set(dep, { isDev: false, definedIn: sourcePath });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const dep of Object.keys(devDeps)) {
|
|
118
|
+
// If already in dependencies, regular dependency takes precedence
|
|
119
|
+
if (!declaredDeps.has(dep)) {
|
|
120
|
+
declaredDeps.set(dep, { isDev: true, definedIn: sourcePath });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
registerDeps(rootPkgJson, 'package.json');
|
|
126
|
+
|
|
127
|
+
for (const wsDir of workspaceDirs) {
|
|
128
|
+
try {
|
|
129
|
+
const wsPkgJson = JSON.parse(fs.readFileSync(path.join(wsDir, 'package.json'), 'utf8'));
|
|
130
|
+
registerDeps(wsPkgJson, path.relative(projectDir, wsDir));
|
|
131
|
+
} catch {
|
|
132
|
+
// Ignore unparseable workspace package.json
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Discover files to scan
|
|
137
|
+
const { files, skippedLargeFiles } = findFiles(projectDir);
|
|
138
|
+
const knownAliases = extractPathAliases(projectDir, rootPkgJson);
|
|
139
|
+
const userIgnored = readIgnoreFile(projectDir);
|
|
140
|
+
const peerDeps = collectPeerDependencies(projectDir);
|
|
141
|
+
const { usedPackages: configPackages, hasTsConfig } = scanConfigFiles(projectDir);
|
|
142
|
+
const { binToPkg, missingNodeModules } = mapInstalledBins(projectDir);
|
|
143
|
+
|
|
144
|
+
// Collect scripts from root and workspaces
|
|
145
|
+
const allScripts = { ...(rootPkgJson.scripts || {}) };
|
|
146
|
+
for (const wsDir of workspaceDirs) {
|
|
147
|
+
try {
|
|
148
|
+
const wsPkg = JSON.parse(fs.readFileSync(path.join(wsDir, 'package.json'), 'utf8'));
|
|
149
|
+
Object.assign(allScripts, wsPkg.scripts || {});
|
|
150
|
+
} catch {
|
|
151
|
+
// Ignore
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const binUsageInScripts = findBinUsageInScripts(allScripts, binToPkg);
|
|
156
|
+
const dotenvInScripts = checkDotenvInScripts(allScripts);
|
|
157
|
+
|
|
158
|
+
// Track code imports: pkgName -> Array<{ file: string, line: number, isTypeOnly: boolean }>
|
|
159
|
+
const codeImports = new Map();
|
|
160
|
+
const parseWarnings = [];
|
|
161
|
+
let hasTypeScriptFiles = false;
|
|
162
|
+
|
|
163
|
+
for (const fileObj of files) {
|
|
164
|
+
if (fileObj.ext === '.ts' || fileObj.ext === '.tsx') {
|
|
165
|
+
hasTypeScriptFiles = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const content = fs.readFileSync(fileObj.fullPath, 'utf8');
|
|
170
|
+
const { imports, warning } = parseImports(content, fileObj.fullPath, { knownAliases });
|
|
171
|
+
|
|
172
|
+
if (warning) {
|
|
173
|
+
parseWarnings.push(warning);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const item of imports) {
|
|
177
|
+
if (!codeImports.has(item.packageName)) {
|
|
178
|
+
codeImports.set(item.packageName, []);
|
|
179
|
+
}
|
|
180
|
+
codeImports.get(item.packageName).push({
|
|
181
|
+
file: fileObj.relativePath,
|
|
182
|
+
line: item.line,
|
|
183
|
+
isTypeOnly: item.isTypeOnly,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
parseWarnings.push(`Could not read file: ${fileObj.relativePath}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build verdicts:
|
|
192
|
+
const used = new Map(); // pkgName -> { locations: [], reasons: [] }
|
|
193
|
+
const unused = [];
|
|
194
|
+
const probablyUnused = [];
|
|
195
|
+
const missing = [];
|
|
196
|
+
|
|
197
|
+
// Helper to mark a package as used with a specific reason
|
|
198
|
+
function markUsed(pkgName, reason, locations = []) {
|
|
199
|
+
if (!used.has(pkgName)) {
|
|
200
|
+
used.set(pkgName, { locations: [], reasons: [] });
|
|
201
|
+
}
|
|
202
|
+
const entry = used.get(pkgName);
|
|
203
|
+
if (reason && !entry.reasons.includes(reason)) {
|
|
204
|
+
entry.reasons.push(reason);
|
|
205
|
+
}
|
|
206
|
+
if (locations && locations.length > 0) {
|
|
207
|
+
entry.locations.push(...locations);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 1. Evaluate all declared packages
|
|
212
|
+
for (const [pkgName, meta] of declaredDeps.entries()) {
|
|
213
|
+
let isFound = false;
|
|
214
|
+
|
|
215
|
+
// a) Found in user .helpforbegsignore
|
|
216
|
+
if (userIgnored.has(pkgName)) {
|
|
217
|
+
markUsed(pkgName, 'Listed in .helpforbegsignore');
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// b) Found in source code imports
|
|
222
|
+
if (codeImports.has(pkgName)) {
|
|
223
|
+
markUsed(pkgName, 'Imported in source code', codeImports.get(pkgName));
|
|
224
|
+
isFound = true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// c) Found in package.json scripts (directly or via bin name)
|
|
228
|
+
if (binUsageInScripts.has(pkgName)) {
|
|
229
|
+
const uses = binUsageInScripts.get(pkgName);
|
|
230
|
+
const scriptsList = uses.map((u) => `"${u.scriptName}"`).join(', ');
|
|
231
|
+
markUsed(pkgName, `Executed in package.json scripts: ${scriptsList}`);
|
|
232
|
+
isFound = true;
|
|
233
|
+
} else {
|
|
234
|
+
// Also check if the raw package name itself appears as a command in scripts
|
|
235
|
+
const escaped = pkgName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
236
|
+
const scriptRegex = new RegExp(`(^|[;&|\\s])${escaped}(\\s|$|[;&|])`);
|
|
237
|
+
for (const [sName, sCmd] of Object.entries(allScripts)) {
|
|
238
|
+
if (typeof sCmd === 'string' && scriptRegex.test(sCmd)) {
|
|
239
|
+
markUsed(pkgName, `Referenced in script "${sName}"`);
|
|
240
|
+
isFound = true;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// d) Found in config files (ESLint, Prettier, Babel, tsconfig)
|
|
247
|
+
if (configPackages.has(pkgName)) {
|
|
248
|
+
markUsed(pkgName, 'Referenced in project configuration files');
|
|
249
|
+
isFound = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// e) Declared as a peerDependency by an installed package
|
|
253
|
+
if (peerDeps.has(pkgName)) {
|
|
254
|
+
markUsed(pkgName, 'Required as a peer dependency by another installed package');
|
|
255
|
+
isFound = true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// f) Handling @types/xyz packages
|
|
259
|
+
if (pkgName.startsWith('@types/')) {
|
|
260
|
+
const basePkg = pkgName.slice('@types/'.length);
|
|
261
|
+
|
|
262
|
+
// If the base package is used in code or declared
|
|
263
|
+
if (codeImports.has(basePkg) || declaredDeps.has(basePkg)) {
|
|
264
|
+
markUsed(pkgName, `TypeScript definitions for used package "${basePkg}"`);
|
|
265
|
+
isFound = true;
|
|
266
|
+
}
|
|
267
|
+
// Special rule: @types/node is used if project has any TS files or tsconfig.json
|
|
268
|
+
else if (basePkg === 'node' && (hasTypeScriptFiles || hasTsConfig)) {
|
|
269
|
+
markUsed(pkgName, 'TypeScript type definitions for Node.js runtime');
|
|
270
|
+
isFound = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// g) Special dotenv check:
|
|
275
|
+
if (pkgName === 'dotenv') {
|
|
276
|
+
if (isFound || dotenvInScripts) {
|
|
277
|
+
markUsed(pkgName, dotenvInScripts ? 'Loaded via -r dotenv/config in scripts' : 'Imported in source code');
|
|
278
|
+
continue;
|
|
279
|
+
} else {
|
|
280
|
+
unused.push({
|
|
281
|
+
name: pkgName,
|
|
282
|
+
isDev: meta.isDev,
|
|
283
|
+
reason: 'Neither imported in code nor referenced with -r dotenv/config in scripts',
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// If already confirmed as used, we're done with this package
|
|
290
|
+
if (isFound) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// h) Built-in safe-list check:
|
|
295
|
+
// If a package is in the safe-list but wasn't found in scripts or configs,
|
|
296
|
+
// we mark it as "probably unused" so the user is encouraged to double check
|
|
297
|
+
// rather than accidentally breaking their tooling.
|
|
298
|
+
if (isSafeListed(pkgName)) {
|
|
299
|
+
probablyUnused.push({
|
|
300
|
+
name: pkgName,
|
|
301
|
+
isDev: meta.isDev,
|
|
302
|
+
reason: 'Known development tool, but no active script or config reference was detected',
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Otherwise, high confidence UNUSED
|
|
308
|
+
unused.push({
|
|
309
|
+
name: pkgName,
|
|
310
|
+
isDev: meta.isDev,
|
|
311
|
+
reason: `Installed in ${meta.isDev ? 'devDependencies' : 'dependencies'} but never imported or referenced`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 2. Detect missing packages (imported in code but not in package.json)
|
|
316
|
+
for (const [pkgName, locations] of codeImports.entries()) {
|
|
317
|
+
if (
|
|
318
|
+
!declaredDeps.has(pkgName) &&
|
|
319
|
+
!workspacePkgNames.has(pkgName) &&
|
|
320
|
+
!userIgnored.has(pkgName)
|
|
321
|
+
) {
|
|
322
|
+
missing.push({
|
|
323
|
+
name: pkgName,
|
|
324
|
+
locations,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
projectDir,
|
|
331
|
+
rootPkgJson,
|
|
332
|
+
isMonorepo,
|
|
333
|
+
workspacesCount: workspaceDirs.length,
|
|
334
|
+
missingNodeModules,
|
|
335
|
+
filesReadCount: files.length,
|
|
336
|
+
skippedLargeFiles,
|
|
337
|
+
parseWarnings,
|
|
338
|
+
used,
|
|
339
|
+
unused,
|
|
340
|
+
probablyUnused,
|
|
341
|
+
missing,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
analyzeProject,
|
|
347
|
+
resolveWorkspaces,
|
|
348
|
+
};
|