oh-my-node-modules 1.2.8 → 1.3.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 +5 -5
- package/dist/cli.js +451 -88
- package/dist/index.js +433 -84
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install -g oh-my-node-modules
|
|
|
17
17
|
### Interactive mode
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
onm
|
|
20
|
+
onm . # Start in current directory
|
|
21
21
|
onm ~/projects # Scan specific directory
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -37,16 +37,16 @@ Keyboard shortcuts:
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
# Quick scan report
|
|
40
|
-
onm --scan
|
|
40
|
+
onm --scan .
|
|
41
41
|
|
|
42
42
|
# JSON output
|
|
43
|
-
onm --scan --json
|
|
43
|
+
onm --scan --json .
|
|
44
44
|
|
|
45
45
|
# Auto-delete large node_modules
|
|
46
|
-
onm --auto --min-size 500mb --yes
|
|
46
|
+
onm --auto --min-size 500mb --yes .
|
|
47
47
|
|
|
48
48
|
# Preview what would be deleted
|
|
49
|
-
onm --auto --min-size 1gb --dry-run
|
|
49
|
+
onm --auto --min-size 1gb --dry-run .
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## Features
|
package/dist/cli.js
CHANGED
|
@@ -2809,6 +2809,162 @@ var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
|
2809
2809
|
import { promises as fs2 } from "fs";
|
|
2810
2810
|
import { join as join2, basename, dirname } from "path";
|
|
2811
2811
|
|
|
2812
|
+
// node_modules/yocto-queue/index.js
|
|
2813
|
+
class Node {
|
|
2814
|
+
value;
|
|
2815
|
+
next;
|
|
2816
|
+
constructor(value) {
|
|
2817
|
+
this.value = value;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
class Queue {
|
|
2822
|
+
#head;
|
|
2823
|
+
#tail;
|
|
2824
|
+
#size;
|
|
2825
|
+
constructor() {
|
|
2826
|
+
this.clear();
|
|
2827
|
+
}
|
|
2828
|
+
enqueue(value) {
|
|
2829
|
+
const node = new Node(value);
|
|
2830
|
+
if (this.#head) {
|
|
2831
|
+
this.#tail.next = node;
|
|
2832
|
+
this.#tail = node;
|
|
2833
|
+
} else {
|
|
2834
|
+
this.#head = node;
|
|
2835
|
+
this.#tail = node;
|
|
2836
|
+
}
|
|
2837
|
+
this.#size++;
|
|
2838
|
+
}
|
|
2839
|
+
dequeue() {
|
|
2840
|
+
const current = this.#head;
|
|
2841
|
+
if (!current) {
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
this.#head = this.#head.next;
|
|
2845
|
+
this.#size--;
|
|
2846
|
+
if (!this.#head) {
|
|
2847
|
+
this.#tail = undefined;
|
|
2848
|
+
}
|
|
2849
|
+
return current.value;
|
|
2850
|
+
}
|
|
2851
|
+
peek() {
|
|
2852
|
+
if (!this.#head) {
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
return this.#head.value;
|
|
2856
|
+
}
|
|
2857
|
+
clear() {
|
|
2858
|
+
this.#head = undefined;
|
|
2859
|
+
this.#tail = undefined;
|
|
2860
|
+
this.#size = 0;
|
|
2861
|
+
}
|
|
2862
|
+
get size() {
|
|
2863
|
+
return this.#size;
|
|
2864
|
+
}
|
|
2865
|
+
*[Symbol.iterator]() {
|
|
2866
|
+
let current = this.#head;
|
|
2867
|
+
while (current) {
|
|
2868
|
+
yield current.value;
|
|
2869
|
+
current = current.next;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
*drain() {
|
|
2873
|
+
while (this.#head) {
|
|
2874
|
+
yield this.dequeue();
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
// node_modules/p-limit/index.js
|
|
2880
|
+
function pLimit(concurrency) {
|
|
2881
|
+
let rejectOnClear = false;
|
|
2882
|
+
if (typeof concurrency === "object") {
|
|
2883
|
+
({ concurrency, rejectOnClear = false } = concurrency);
|
|
2884
|
+
}
|
|
2885
|
+
validateConcurrency(concurrency);
|
|
2886
|
+
if (typeof rejectOnClear !== "boolean") {
|
|
2887
|
+
throw new TypeError("Expected `rejectOnClear` to be a boolean");
|
|
2888
|
+
}
|
|
2889
|
+
const queue = new Queue;
|
|
2890
|
+
let activeCount = 0;
|
|
2891
|
+
const resumeNext = () => {
|
|
2892
|
+
if (activeCount < concurrency && queue.size > 0) {
|
|
2893
|
+
activeCount++;
|
|
2894
|
+
queue.dequeue().run();
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
const next = () => {
|
|
2898
|
+
activeCount--;
|
|
2899
|
+
resumeNext();
|
|
2900
|
+
};
|
|
2901
|
+
const run = async (function_, resolve, arguments_) => {
|
|
2902
|
+
const result = (async () => function_(...arguments_))();
|
|
2903
|
+
resolve(result);
|
|
2904
|
+
try {
|
|
2905
|
+
await result;
|
|
2906
|
+
} catch {}
|
|
2907
|
+
next();
|
|
2908
|
+
};
|
|
2909
|
+
const enqueue = (function_, resolve, reject, arguments_) => {
|
|
2910
|
+
const queueItem = { reject };
|
|
2911
|
+
new Promise((internalResolve) => {
|
|
2912
|
+
queueItem.run = internalResolve;
|
|
2913
|
+
queue.enqueue(queueItem);
|
|
2914
|
+
}).then(run.bind(undefined, function_, resolve, arguments_));
|
|
2915
|
+
if (activeCount < concurrency) {
|
|
2916
|
+
resumeNext();
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2919
|
+
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
|
|
2920
|
+
enqueue(function_, resolve, reject, arguments_);
|
|
2921
|
+
});
|
|
2922
|
+
Object.defineProperties(generator, {
|
|
2923
|
+
activeCount: {
|
|
2924
|
+
get: () => activeCount
|
|
2925
|
+
},
|
|
2926
|
+
pendingCount: {
|
|
2927
|
+
get: () => queue.size
|
|
2928
|
+
},
|
|
2929
|
+
clearQueue: {
|
|
2930
|
+
value() {
|
|
2931
|
+
if (!rejectOnClear) {
|
|
2932
|
+
queue.clear();
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
const abortError = AbortSignal.abort().reason;
|
|
2936
|
+
while (queue.size > 0) {
|
|
2937
|
+
queue.dequeue().reject(abortError);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
},
|
|
2941
|
+
concurrency: {
|
|
2942
|
+
get: () => concurrency,
|
|
2943
|
+
set(newConcurrency) {
|
|
2944
|
+
validateConcurrency(newConcurrency);
|
|
2945
|
+
concurrency = newConcurrency;
|
|
2946
|
+
queueMicrotask(() => {
|
|
2947
|
+
while (activeCount < concurrency && queue.size > 0) {
|
|
2948
|
+
resumeNext();
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
},
|
|
2953
|
+
map: {
|
|
2954
|
+
async value(iterable, function_) {
|
|
2955
|
+
const promises = Array.from(iterable, (value, index) => this(function_, value, index));
|
|
2956
|
+
return Promise.all(promises);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
return generator;
|
|
2961
|
+
}
|
|
2962
|
+
function validateConcurrency(concurrency) {
|
|
2963
|
+
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
|
|
2964
|
+
throw new TypeError("Expected `concurrency` to be a number from 1 and up");
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2812
2968
|
// src/utils.ts
|
|
2813
2969
|
import { promises as fs } from "fs";
|
|
2814
2970
|
import { join } from "path";
|
|
@@ -2924,6 +3080,9 @@ function calculateStatistics(items) {
|
|
|
2924
3080
|
}
|
|
2925
3081
|
function shouldExcludePath(path, patterns) {
|
|
2926
3082
|
return patterns.some((pattern) => {
|
|
3083
|
+
if (pattern === "**/.*" || pattern === ".*") {
|
|
3084
|
+
return /(^|\/)\.[^\/]*($|\/)/.test(path);
|
|
3085
|
+
}
|
|
2927
3086
|
const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
2928
3087
|
const regex = new RegExp(regexPattern, "i");
|
|
2929
3088
|
return regex.test(path);
|
|
@@ -2948,67 +3107,96 @@ async function readPackageJson(projectPath) {
|
|
|
2948
3107
|
}
|
|
2949
3108
|
}
|
|
2950
3109
|
|
|
2951
|
-
// src/
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
2986
|
-
const subPath = join2(currentPath, entry.name);
|
|
2987
|
-
if (!shouldExcludePath(subPath, options.excludePatterns)) {
|
|
2988
|
-
pathsToScan.push({ path: subPath, depth: depth + 1 });
|
|
2989
|
-
totalEstimate++;
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
3110
|
+
// src/native-size.ts
|
|
3111
|
+
import { exec } from "child_process";
|
|
3112
|
+
import { promisify } from "util";
|
|
3113
|
+
import { platform } from "os";
|
|
3114
|
+
var execAsync = promisify(exec);
|
|
3115
|
+
async function getSizeWithDu(dirPath) {
|
|
3116
|
+
try {
|
|
3117
|
+
const { stdout } = await execAsync(`du -sb "${dirPath}"`, {
|
|
3118
|
+
timeout: 30000,
|
|
3119
|
+
maxBuffer: 1024 * 1024
|
|
3120
|
+
});
|
|
3121
|
+
const match = stdout.trim().match(/^(\d+)/);
|
|
3122
|
+
if (match) {
|
|
3123
|
+
return parseInt(match[1], 10);
|
|
3124
|
+
}
|
|
3125
|
+
return null;
|
|
3126
|
+
} catch {
|
|
3127
|
+
return null;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
async function getSizeWithDir(dirPath) {
|
|
3131
|
+
try {
|
|
3132
|
+
const { stdout } = await execAsync(`dir /s "${dirPath}"`, {
|
|
3133
|
+
timeout: 30000,
|
|
3134
|
+
maxBuffer: 1024 * 1024
|
|
3135
|
+
});
|
|
3136
|
+
const lines = stdout.split(`
|
|
3137
|
+
`);
|
|
3138
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
3139
|
+
const line = lines[i];
|
|
3140
|
+
const match = line.match(/File\(s\)\s+([\d,]+)\s+bytes?/i);
|
|
3141
|
+
if (match) {
|
|
3142
|
+
const bytesStr = match[1].replace(/,/g, "");
|
|
3143
|
+
return parseInt(bytesStr, 10);
|
|
2992
3144
|
}
|
|
2993
|
-
} catch (error) {
|
|
2994
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2995
|
-
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
2996
3145
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3146
|
+
return null;
|
|
3147
|
+
} catch {
|
|
3148
|
+
return null;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
async function countPackagesNative(dirPath) {
|
|
3152
|
+
try {
|
|
3153
|
+
const isWindows = platform() === "win32";
|
|
3154
|
+
if (isWindows) {
|
|
3155
|
+
const { stdout } = await execAsync(`dir /b /ad "${dirPath}" | find /c /v ""`, { timeout: 1e4 });
|
|
3156
|
+
const topLevel = parseInt(stdout.trim(), 10) || 0;
|
|
3157
|
+
return { topLevel, total: topLevel };
|
|
3158
|
+
} else {
|
|
3159
|
+
const { stdout: topLevelOut } = await execAsync(`find "${dirPath}" -maxdepth 1 -type d ! -name ".*" ! -name "node_modules" | wc -l`, { timeout: 1e4 });
|
|
3160
|
+
const topLevel = Math.max(0, parseInt(topLevelOut.trim(), 10) - 1);
|
|
3161
|
+
const { stdout: totalOut } = await execAsync(`find "${dirPath}" -type d ! -path "${dirPath}" ! -name ".*" | wc -l`, { timeout: 1e4 });
|
|
3162
|
+
const total = parseInt(totalOut.trim(), 10);
|
|
3163
|
+
return { topLevel, total };
|
|
3001
3164
|
}
|
|
3165
|
+
} catch {
|
|
3166
|
+
return null;
|
|
3002
3167
|
}
|
|
3003
|
-
|
|
3004
|
-
|
|
3168
|
+
}
|
|
3169
|
+
async function getFastDirectorySize(dirPath) {
|
|
3170
|
+
const isWindows = platform() === "win32";
|
|
3171
|
+
let sizeBytes = null;
|
|
3172
|
+
if (isWindows) {
|
|
3173
|
+
sizeBytes = await getSizeWithDir(dirPath);
|
|
3174
|
+
} else {
|
|
3175
|
+
sizeBytes = await getSizeWithDu(dirPath);
|
|
3005
3176
|
}
|
|
3006
|
-
|
|
3177
|
+
const packageCounts = await countPackagesNative(dirPath);
|
|
3178
|
+
if (sizeBytes !== null && packageCounts !== null) {
|
|
3179
|
+
return {
|
|
3180
|
+
bytes: sizeBytes,
|
|
3181
|
+
packageCount: packageCounts.topLevel,
|
|
3182
|
+
totalPackageCount: packageCounts.total,
|
|
3183
|
+
isNative: true
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
return {
|
|
3187
|
+
bytes: 0,
|
|
3188
|
+
packageCount: 0,
|
|
3189
|
+
totalPackageCount: 0,
|
|
3190
|
+
isNative: false
|
|
3191
|
+
};
|
|
3007
3192
|
}
|
|
3193
|
+
|
|
3194
|
+
// src/scanner.ts
|
|
3195
|
+
var SIZE_CALCULATION_CONCURRENCY = 4;
|
|
3008
3196
|
async function findRepoRoot(startPath) {
|
|
3009
3197
|
let currentPath = startPath;
|
|
3010
|
-
const root = "/";
|
|
3011
|
-
while (currentPath !== root) {
|
|
3198
|
+
const root = process.platform === "win32" ? "C:\\" : "/";
|
|
3199
|
+
while (currentPath !== root && currentPath !== dirname(currentPath)) {
|
|
3012
3200
|
const gitPath = join2(currentPath, ".git");
|
|
3013
3201
|
try {
|
|
3014
3202
|
if (await fileExists(gitPath)) {
|
|
@@ -3019,34 +3207,11 @@ async function findRepoRoot(startPath) {
|
|
|
3019
3207
|
}
|
|
3020
3208
|
return startPath;
|
|
3021
3209
|
}
|
|
3022
|
-
|
|
3023
|
-
const
|
|
3024
|
-
|
|
3025
|
-
const packageJson = await readPackageJson(projectPath);
|
|
3026
|
-
const projectName = packageJson?.name || basename(projectPath);
|
|
3027
|
-
const projectVersion = packageJson?.version;
|
|
3028
|
-
const repoPath = await findRepoRoot(projectPath);
|
|
3029
|
-
const sizeCategory = getSizeCategory(totalSize);
|
|
3030
|
-
const ageCategory = getAgeCategory(stats.mtime);
|
|
3031
|
-
return {
|
|
3032
|
-
path: nodeModulesPath,
|
|
3033
|
-
projectPath,
|
|
3034
|
-
projectName,
|
|
3035
|
-
projectVersion,
|
|
3036
|
-
repoPath,
|
|
3037
|
-
sizeBytes: totalSize,
|
|
3038
|
-
sizeFormatted: formatBytes(totalSize),
|
|
3039
|
-
packageCount,
|
|
3040
|
-
totalPackageCount,
|
|
3041
|
-
lastModified: stats.mtime,
|
|
3042
|
-
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
3043
|
-
selected: false,
|
|
3044
|
-
isFavorite: false,
|
|
3045
|
-
ageCategory,
|
|
3046
|
-
sizeCategory
|
|
3047
|
-
};
|
|
3210
|
+
function getAgeInDays2(date) {
|
|
3211
|
+
const now = new Date;
|
|
3212
|
+
return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
3048
3213
|
}
|
|
3049
|
-
async function
|
|
3214
|
+
async function calculateDirectorySizeFallback(dirPath) {
|
|
3050
3215
|
let totalSize = 0;
|
|
3051
3216
|
let packageCount = 0;
|
|
3052
3217
|
let totalPackageCount = 0;
|
|
@@ -3083,7 +3248,7 @@ async function calculateDirectorySize(dirPath) {
|
|
|
3083
3248
|
pathsToProcess.push(entryPath);
|
|
3084
3249
|
}
|
|
3085
3250
|
} catch {}
|
|
3086
|
-
}
|
|
3251
|
+
}
|
|
3087
3252
|
} catch {}
|
|
3088
3253
|
if (currentPath === dirPath) {
|
|
3089
3254
|
isTopLevel = false;
|
|
@@ -3091,9 +3256,191 @@ async function calculateDirectorySize(dirPath) {
|
|
|
3091
3256
|
}
|
|
3092
3257
|
return { totalSize, packageCount, totalPackageCount };
|
|
3093
3258
|
}
|
|
3094
|
-
function
|
|
3095
|
-
const
|
|
3096
|
-
|
|
3259
|
+
async function calculateSizeWithFallback(dirPath) {
|
|
3260
|
+
const nativeResult = await getFastDirectorySize(dirPath);
|
|
3261
|
+
if (nativeResult.isNative && nativeResult.bytes > 0) {
|
|
3262
|
+
return {
|
|
3263
|
+
totalSize: nativeResult.bytes,
|
|
3264
|
+
packageCount: nativeResult.packageCount,
|
|
3265
|
+
totalPackageCount: nativeResult.totalPackageCount,
|
|
3266
|
+
isNative: true
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
const fallbackResult = await calculateDirectorySizeFallback(dirPath);
|
|
3270
|
+
return {
|
|
3271
|
+
...fallbackResult,
|
|
3272
|
+
isNative: false
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
async function analyzeNodeModules(nodeModulesPath, projectPath, lazy = false) {
|
|
3276
|
+
const stats = await fs2.stat(nodeModulesPath);
|
|
3277
|
+
if (lazy) {
|
|
3278
|
+
const packageJson2 = await readPackageJson(projectPath);
|
|
3279
|
+
const projectName2 = packageJson2?.name || basename(projectPath);
|
|
3280
|
+
const repoPath2 = await findRepoRoot(projectPath);
|
|
3281
|
+
return {
|
|
3282
|
+
path: nodeModulesPath,
|
|
3283
|
+
projectPath,
|
|
3284
|
+
projectName: projectName2,
|
|
3285
|
+
projectVersion: packageJson2?.version,
|
|
3286
|
+
repoPath: repoPath2,
|
|
3287
|
+
sizeBytes: 0,
|
|
3288
|
+
sizeFormatted: "calculating...",
|
|
3289
|
+
packageCount: 0,
|
|
3290
|
+
totalPackageCount: 0,
|
|
3291
|
+
lastModified: stats.mtime,
|
|
3292
|
+
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
3293
|
+
selected: false,
|
|
3294
|
+
isFavorite: false,
|
|
3295
|
+
ageCategory: getAgeCategory(stats.mtime),
|
|
3296
|
+
sizeCategory: "small",
|
|
3297
|
+
isPending: true
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(nodeModulesPath);
|
|
3301
|
+
const packageJson = await readPackageJson(projectPath);
|
|
3302
|
+
const projectName = packageJson?.name || basename(projectPath);
|
|
3303
|
+
const repoPath = await findRepoRoot(projectPath);
|
|
3304
|
+
const sizeCategory = getSizeCategory(totalSize);
|
|
3305
|
+
const ageCategory = getAgeCategory(stats.mtime);
|
|
3306
|
+
return {
|
|
3307
|
+
path: nodeModulesPath,
|
|
3308
|
+
projectPath,
|
|
3309
|
+
projectName,
|
|
3310
|
+
projectVersion: packageJson?.version,
|
|
3311
|
+
repoPath,
|
|
3312
|
+
sizeBytes: totalSize,
|
|
3313
|
+
sizeFormatted: formatBytes(totalSize),
|
|
3314
|
+
packageCount,
|
|
3315
|
+
totalPackageCount,
|
|
3316
|
+
lastModified: stats.mtime,
|
|
3317
|
+
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
3318
|
+
selected: false,
|
|
3319
|
+
isFavorite: false,
|
|
3320
|
+
ageCategory,
|
|
3321
|
+
sizeCategory,
|
|
3322
|
+
isNativeCalculation: isNative
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
async function updateNodeModulesSize(info) {
|
|
3326
|
+
const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(info.path);
|
|
3327
|
+
return {
|
|
3328
|
+
...info,
|
|
3329
|
+
sizeBytes: totalSize,
|
|
3330
|
+
sizeFormatted: formatBytes(totalSize),
|
|
3331
|
+
packageCount,
|
|
3332
|
+
totalPackageCount,
|
|
3333
|
+
sizeCategory: getSizeCategory(totalSize),
|
|
3334
|
+
isPending: false,
|
|
3335
|
+
isNativeCalculation: isNative
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
async function scanForNodeModules(options, onProgress, lazy = false) {
|
|
3339
|
+
const result = {
|
|
3340
|
+
nodeModules: [],
|
|
3341
|
+
directoriesScanned: 0,
|
|
3342
|
+
errors: []
|
|
3343
|
+
};
|
|
3344
|
+
const visitedPaths = new Set;
|
|
3345
|
+
const pathsToScan = [
|
|
3346
|
+
{ path: options.rootPath, depth: 0 }
|
|
3347
|
+
];
|
|
3348
|
+
const foundNodeModules = [];
|
|
3349
|
+
let processedCount = 0;
|
|
3350
|
+
let totalEstimate = 1;
|
|
3351
|
+
while (pathsToScan.length > 0) {
|
|
3352
|
+
const { path: currentPath, depth } = pathsToScan.shift();
|
|
3353
|
+
if (visitedPaths.has(currentPath))
|
|
3354
|
+
continue;
|
|
3355
|
+
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
3356
|
+
continue;
|
|
3357
|
+
if (shouldExcludePath(currentPath, options.excludePatterns))
|
|
3358
|
+
continue;
|
|
3359
|
+
visitedPaths.add(currentPath);
|
|
3360
|
+
result.directoriesScanned++;
|
|
3361
|
+
try {
|
|
3362
|
+
const entries = await fs2.readdir(currentPath, { withFileTypes: true });
|
|
3363
|
+
const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
|
|
3364
|
+
if (hasNodeModules) {
|
|
3365
|
+
const nodeModulesPath = join2(currentPath, "node_modules");
|
|
3366
|
+
foundNodeModules.push({ nodeModulesPath, projectPath: currentPath });
|
|
3367
|
+
if (onProgress) {
|
|
3368
|
+
onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundNodeModules.length);
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
for (const entry of entries) {
|
|
3372
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
3373
|
+
const subPath = join2(currentPath, entry.name);
|
|
3374
|
+
if (!shouldExcludePath(subPath, options.excludePatterns)) {
|
|
3375
|
+
pathsToScan.push({ path: subPath, depth: depth + 1 });
|
|
3376
|
+
totalEstimate++;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
} catch (error) {
|
|
3381
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3382
|
+
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
3383
|
+
}
|
|
3384
|
+
processedCount++;
|
|
3385
|
+
if (onProgress && foundNodeModules.length === 0) {
|
|
3386
|
+
onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), 0);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
if (foundNodeModules.length > 0) {
|
|
3390
|
+
const sizeLimit = pLimit(SIZE_CALCULATION_CONCURRENCY);
|
|
3391
|
+
const analysisPromises = foundNodeModules.map(({ nodeModulesPath, projectPath }) => sizeLimit(async () => {
|
|
3392
|
+
try {
|
|
3393
|
+
const info = await analyzeNodeModules(nodeModulesPath, projectPath, lazy);
|
|
3394
|
+
if (!lazy) {
|
|
3395
|
+
const passesFilter = (!options.minSizeBytes || info.sizeBytes >= options.minSizeBytes) && (!options.olderThanDays || getAgeInDays2(info.lastModified) >= options.olderThanDays);
|
|
3396
|
+
if (passesFilter) {
|
|
3397
|
+
result.nodeModules.push(info);
|
|
3398
|
+
}
|
|
3399
|
+
} else {
|
|
3400
|
+
result.nodeModules.push(info);
|
|
3401
|
+
}
|
|
3402
|
+
} catch (error) {
|
|
3403
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3404
|
+
result.errors.push(`Error analyzing ${nodeModulesPath}: ${errorMessage}`);
|
|
3405
|
+
}
|
|
3406
|
+
}));
|
|
3407
|
+
await Promise.all(analysisPromises);
|
|
3408
|
+
}
|
|
3409
|
+
if (onProgress) {
|
|
3410
|
+
onProgress(100, result.nodeModules.length);
|
|
3411
|
+
}
|
|
3412
|
+
return result;
|
|
3413
|
+
}
|
|
3414
|
+
async function calculatePendingSizes(nodeModules, onProgress) {
|
|
3415
|
+
const pending = nodeModules.filter((nm) => nm.isPending);
|
|
3416
|
+
if (pending.length === 0)
|
|
3417
|
+
return nodeModules;
|
|
3418
|
+
const limit = pLimit(SIZE_CALCULATION_CONCURRENCY);
|
|
3419
|
+
let completed = 0;
|
|
3420
|
+
const total = pending.length;
|
|
3421
|
+
const updatePromises = pending.map((pendingItem) => limit(async () => {
|
|
3422
|
+
try {
|
|
3423
|
+
const updated = await updateNodeModulesSize(pendingItem);
|
|
3424
|
+
completed++;
|
|
3425
|
+
if (onProgress) {
|
|
3426
|
+
onProgress(completed, total);
|
|
3427
|
+
}
|
|
3428
|
+
return updated;
|
|
3429
|
+
} catch (error) {
|
|
3430
|
+
completed++;
|
|
3431
|
+
if (onProgress) {
|
|
3432
|
+
onProgress(completed, total);
|
|
3433
|
+
}
|
|
3434
|
+
return {
|
|
3435
|
+
...pendingItem,
|
|
3436
|
+
sizeFormatted: "error",
|
|
3437
|
+
isPending: false
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
}));
|
|
3441
|
+
const updatedItems = await Promise.all(updatePromises);
|
|
3442
|
+
const updatedMap = new Map(updatedItems.map((item) => [item.path, item]));
|
|
3443
|
+
return nodeModules.map((item) => updatedMap.get(item.path) || item);
|
|
3097
3444
|
}
|
|
3098
3445
|
async function loadIgnorePatterns() {
|
|
3099
3446
|
const patterns = [
|
|
@@ -3253,9 +3600,10 @@ function getVersion() {
|
|
|
3253
3600
|
}
|
|
3254
3601
|
}
|
|
3255
3602
|
function formatItem(item) {
|
|
3256
|
-
const
|
|
3603
|
+
const isPending = item.isPending;
|
|
3604
|
+
const size = isPending ? import_picocolors2.default.yellow(" (...) ") : import_picocolors2.default.cyan(item.sizeFormatted.padStart(8));
|
|
3257
3605
|
const age = import_picocolors2.default.gray(item.lastModifiedFormatted);
|
|
3258
|
-
const warning = item.sizeCategory === "huge" ? import_picocolors2.default.red(" ⚠") : "";
|
|
3606
|
+
const warning = !isPending && item.sizeCategory === "huge" ? import_picocolors2.default.red(" ⚠") : "";
|
|
3259
3607
|
return `${size} ${item.projectName}${warning} [${age}]`;
|
|
3260
3608
|
}
|
|
3261
3609
|
async function interactiveMode(rootPath) {
|
|
@@ -3268,13 +3616,28 @@ async function interactiveMode(rootPath) {
|
|
|
3268
3616
|
rootPath,
|
|
3269
3617
|
excludePatterns,
|
|
3270
3618
|
followSymlinks: false
|
|
3271
|
-
})
|
|
3619
|
+
}, (_progress, found) => {
|
|
3620
|
+
if (found > 0) {
|
|
3621
|
+
s.message(`Scanning... found ${found} node_modules`);
|
|
3622
|
+
}
|
|
3623
|
+
}, true);
|
|
3272
3624
|
s.stop(`Found ${result.nodeModules.length} node_modules directories`);
|
|
3273
3625
|
if (result.nodeModules.length === 0) {
|
|
3274
3626
|
ge(import_picocolors2.default.yellow("No node_modules found."));
|
|
3275
3627
|
return;
|
|
3276
3628
|
}
|
|
3277
3629
|
let items = sortNodeModules(result.nodeModules, "size-desc");
|
|
3630
|
+
console.log(`
|
|
3631
|
+
${import_picocolors2.default.gray("Calculating sizes...")}`);
|
|
3632
|
+
console.log(`${import_picocolors2.default.gray("Projects sorted by size:")}
|
|
3633
|
+
`);
|
|
3634
|
+
const sizeSpinner = _2();
|
|
3635
|
+
sizeSpinner.start("Calculating directory sizes...");
|
|
3636
|
+
items = await calculatePendingSizes(items, (completed, total) => {
|
|
3637
|
+
sizeSpinner.message(`Calculating sizes... ${completed}/${total}`);
|
|
3638
|
+
});
|
|
3639
|
+
sizeSpinner.stop(`Calculated sizes for ${items.length} directories`);
|
|
3640
|
+
items = sortNodeModules(items, "size-desc");
|
|
3278
3641
|
const stats = calculateStatistics(items);
|
|
3279
3642
|
console.log(`
|
|
3280
3643
|
${import_picocolors2.default.gray("Total:")} ${import_picocolors2.default.white(stats.totalSizeFormatted)} across ${import_picocolors2.default.white(String(stats.totalProjects))} projects`);
|
|
@@ -3433,7 +3796,7 @@ Dry run - no files deleted.`));
|
|
|
3433
3796
|
}
|
|
3434
3797
|
}
|
|
3435
3798
|
var program2 = new Command;
|
|
3436
|
-
program2.name("onm").description("Find and clean up node_modules directories").version(getVersion()).argument("
|
|
3799
|
+
program2.name("onm").description("Find and clean up node_modules directories").version(getVersion()).argument("<path>", "Directory to scan").option("--scan", "quick scan mode (no interactive UI)").option("--auto", "auto-delete mode with filters").option("--min-size <size>", "minimum size in bytes for auto mode").option("--dry-run", "simulate deletion without actually deleting").option("--json", "output as JSON").action(async (path, options) => {
|
|
3437
3800
|
if (options.scan) {
|
|
3438
3801
|
await quickScanMode(path, options.json);
|
|
3439
3802
|
} else if (options.auto) {
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,162 @@
|
|
|
2
2
|
import { promises as fs2 } from "fs";
|
|
3
3
|
import { join as join2, basename, dirname } from "path";
|
|
4
4
|
|
|
5
|
+
// node_modules/yocto-queue/index.js
|
|
6
|
+
class Node {
|
|
7
|
+
value;
|
|
8
|
+
next;
|
|
9
|
+
constructor(value) {
|
|
10
|
+
this.value = value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class Queue {
|
|
15
|
+
#head;
|
|
16
|
+
#tail;
|
|
17
|
+
#size;
|
|
18
|
+
constructor() {
|
|
19
|
+
this.clear();
|
|
20
|
+
}
|
|
21
|
+
enqueue(value) {
|
|
22
|
+
const node = new Node(value);
|
|
23
|
+
if (this.#head) {
|
|
24
|
+
this.#tail.next = node;
|
|
25
|
+
this.#tail = node;
|
|
26
|
+
} else {
|
|
27
|
+
this.#head = node;
|
|
28
|
+
this.#tail = node;
|
|
29
|
+
}
|
|
30
|
+
this.#size++;
|
|
31
|
+
}
|
|
32
|
+
dequeue() {
|
|
33
|
+
const current = this.#head;
|
|
34
|
+
if (!current) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.#head = this.#head.next;
|
|
38
|
+
this.#size--;
|
|
39
|
+
if (!this.#head) {
|
|
40
|
+
this.#tail = undefined;
|
|
41
|
+
}
|
|
42
|
+
return current.value;
|
|
43
|
+
}
|
|
44
|
+
peek() {
|
|
45
|
+
if (!this.#head) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
return this.#head.value;
|
|
49
|
+
}
|
|
50
|
+
clear() {
|
|
51
|
+
this.#head = undefined;
|
|
52
|
+
this.#tail = undefined;
|
|
53
|
+
this.#size = 0;
|
|
54
|
+
}
|
|
55
|
+
get size() {
|
|
56
|
+
return this.#size;
|
|
57
|
+
}
|
|
58
|
+
*[Symbol.iterator]() {
|
|
59
|
+
let current = this.#head;
|
|
60
|
+
while (current) {
|
|
61
|
+
yield current.value;
|
|
62
|
+
current = current.next;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
*drain() {
|
|
66
|
+
while (this.#head) {
|
|
67
|
+
yield this.dequeue();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// node_modules/p-limit/index.js
|
|
73
|
+
function pLimit(concurrency) {
|
|
74
|
+
let rejectOnClear = false;
|
|
75
|
+
if (typeof concurrency === "object") {
|
|
76
|
+
({ concurrency, rejectOnClear = false } = concurrency);
|
|
77
|
+
}
|
|
78
|
+
validateConcurrency(concurrency);
|
|
79
|
+
if (typeof rejectOnClear !== "boolean") {
|
|
80
|
+
throw new TypeError("Expected `rejectOnClear` to be a boolean");
|
|
81
|
+
}
|
|
82
|
+
const queue = new Queue;
|
|
83
|
+
let activeCount = 0;
|
|
84
|
+
const resumeNext = () => {
|
|
85
|
+
if (activeCount < concurrency && queue.size > 0) {
|
|
86
|
+
activeCount++;
|
|
87
|
+
queue.dequeue().run();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const next = () => {
|
|
91
|
+
activeCount--;
|
|
92
|
+
resumeNext();
|
|
93
|
+
};
|
|
94
|
+
const run = async (function_, resolve, arguments_) => {
|
|
95
|
+
const result = (async () => function_(...arguments_))();
|
|
96
|
+
resolve(result);
|
|
97
|
+
try {
|
|
98
|
+
await result;
|
|
99
|
+
} catch {}
|
|
100
|
+
next();
|
|
101
|
+
};
|
|
102
|
+
const enqueue = (function_, resolve, reject, arguments_) => {
|
|
103
|
+
const queueItem = { reject };
|
|
104
|
+
new Promise((internalResolve) => {
|
|
105
|
+
queueItem.run = internalResolve;
|
|
106
|
+
queue.enqueue(queueItem);
|
|
107
|
+
}).then(run.bind(undefined, function_, resolve, arguments_));
|
|
108
|
+
if (activeCount < concurrency) {
|
|
109
|
+
resumeNext();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
|
|
113
|
+
enqueue(function_, resolve, reject, arguments_);
|
|
114
|
+
});
|
|
115
|
+
Object.defineProperties(generator, {
|
|
116
|
+
activeCount: {
|
|
117
|
+
get: () => activeCount
|
|
118
|
+
},
|
|
119
|
+
pendingCount: {
|
|
120
|
+
get: () => queue.size
|
|
121
|
+
},
|
|
122
|
+
clearQueue: {
|
|
123
|
+
value() {
|
|
124
|
+
if (!rejectOnClear) {
|
|
125
|
+
queue.clear();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const abortError = AbortSignal.abort().reason;
|
|
129
|
+
while (queue.size > 0) {
|
|
130
|
+
queue.dequeue().reject(abortError);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
concurrency: {
|
|
135
|
+
get: () => concurrency,
|
|
136
|
+
set(newConcurrency) {
|
|
137
|
+
validateConcurrency(newConcurrency);
|
|
138
|
+
concurrency = newConcurrency;
|
|
139
|
+
queueMicrotask(() => {
|
|
140
|
+
while (activeCount < concurrency && queue.size > 0) {
|
|
141
|
+
resumeNext();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
map: {
|
|
147
|
+
async value(iterable, function_) {
|
|
148
|
+
const promises = Array.from(iterable, (value, index) => this(function_, value, index));
|
|
149
|
+
return Promise.all(promises);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return generator;
|
|
154
|
+
}
|
|
155
|
+
function validateConcurrency(concurrency) {
|
|
156
|
+
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
|
|
157
|
+
throw new TypeError("Expected `concurrency` to be a number from 1 and up");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
5
161
|
// src/utils.ts
|
|
6
162
|
import { promises as fs } from "fs";
|
|
7
163
|
import { join } from "path";
|
|
@@ -150,6 +306,9 @@ function calculateStatistics(items) {
|
|
|
150
306
|
}
|
|
151
307
|
function shouldExcludePath(path, patterns) {
|
|
152
308
|
return patterns.some((pattern) => {
|
|
309
|
+
if (pattern === "**/.*" || pattern === ".*") {
|
|
310
|
+
return /(^|\/)\.[^\/]*($|\/)/.test(path);
|
|
311
|
+
}
|
|
153
312
|
const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
154
313
|
const regex = new RegExp(regexPattern, "i");
|
|
155
314
|
return regex.test(path);
|
|
@@ -182,67 +341,96 @@ async function readPackageJson(projectPath) {
|
|
|
182
341
|
}
|
|
183
342
|
}
|
|
184
343
|
|
|
185
|
-
// src/
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
220
|
-
const subPath = join2(currentPath, entry.name);
|
|
221
|
-
if (!shouldExcludePath(subPath, options.excludePatterns)) {
|
|
222
|
-
pathsToScan.push({ path: subPath, depth: depth + 1 });
|
|
223
|
-
totalEstimate++;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
344
|
+
// src/native-size.ts
|
|
345
|
+
import { exec } from "child_process";
|
|
346
|
+
import { promisify } from "util";
|
|
347
|
+
import { platform } from "os";
|
|
348
|
+
var execAsync = promisify(exec);
|
|
349
|
+
async function getSizeWithDu(dirPath) {
|
|
350
|
+
try {
|
|
351
|
+
const { stdout } = await execAsync(`du -sb "${dirPath}"`, {
|
|
352
|
+
timeout: 30000,
|
|
353
|
+
maxBuffer: 1024 * 1024
|
|
354
|
+
});
|
|
355
|
+
const match = stdout.trim().match(/^(\d+)/);
|
|
356
|
+
if (match) {
|
|
357
|
+
return parseInt(match[1], 10);
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
} catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function getSizeWithDir(dirPath) {
|
|
365
|
+
try {
|
|
366
|
+
const { stdout } = await execAsync(`dir /s "${dirPath}"`, {
|
|
367
|
+
timeout: 30000,
|
|
368
|
+
maxBuffer: 1024 * 1024
|
|
369
|
+
});
|
|
370
|
+
const lines = stdout.split(`
|
|
371
|
+
`);
|
|
372
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
373
|
+
const line = lines[i];
|
|
374
|
+
const match = line.match(/File\(s\)\s+([\d,]+)\s+bytes?/i);
|
|
375
|
+
if (match) {
|
|
376
|
+
const bytesStr = match[1].replace(/,/g, "");
|
|
377
|
+
return parseInt(bytesStr, 10);
|
|
226
378
|
}
|
|
227
|
-
} catch (error) {
|
|
228
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
229
|
-
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
230
379
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
380
|
+
return null;
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function countPackagesNative(dirPath) {
|
|
386
|
+
try {
|
|
387
|
+
const isWindows = platform() === "win32";
|
|
388
|
+
if (isWindows) {
|
|
389
|
+
const { stdout } = await execAsync(`dir /b /ad "${dirPath}" | find /c /v ""`, { timeout: 1e4 });
|
|
390
|
+
const topLevel = parseInt(stdout.trim(), 10) || 0;
|
|
391
|
+
return { topLevel, total: topLevel };
|
|
392
|
+
} else {
|
|
393
|
+
const { stdout: topLevelOut } = await execAsync(`find "${dirPath}" -maxdepth 1 -type d ! -name ".*" ! -name "node_modules" | wc -l`, { timeout: 1e4 });
|
|
394
|
+
const topLevel = Math.max(0, parseInt(topLevelOut.trim(), 10) - 1);
|
|
395
|
+
const { stdout: totalOut } = await execAsync(`find "${dirPath}" -type d ! -path "${dirPath}" ! -name ".*" | wc -l`, { timeout: 1e4 });
|
|
396
|
+
const total = parseInt(totalOut.trim(), 10);
|
|
397
|
+
return { topLevel, total };
|
|
235
398
|
}
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
236
401
|
}
|
|
237
|
-
|
|
238
|
-
|
|
402
|
+
}
|
|
403
|
+
async function getFastDirectorySize(dirPath) {
|
|
404
|
+
const isWindows = platform() === "win32";
|
|
405
|
+
let sizeBytes = null;
|
|
406
|
+
if (isWindows) {
|
|
407
|
+
sizeBytes = await getSizeWithDir(dirPath);
|
|
408
|
+
} else {
|
|
409
|
+
sizeBytes = await getSizeWithDu(dirPath);
|
|
239
410
|
}
|
|
240
|
-
|
|
411
|
+
const packageCounts = await countPackagesNative(dirPath);
|
|
412
|
+
if (sizeBytes !== null && packageCounts !== null) {
|
|
413
|
+
return {
|
|
414
|
+
bytes: sizeBytes,
|
|
415
|
+
packageCount: packageCounts.topLevel,
|
|
416
|
+
totalPackageCount: packageCounts.total,
|
|
417
|
+
isNative: true
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
bytes: 0,
|
|
422
|
+
packageCount: 0,
|
|
423
|
+
totalPackageCount: 0,
|
|
424
|
+
isNative: false
|
|
425
|
+
};
|
|
241
426
|
}
|
|
427
|
+
|
|
428
|
+
// src/scanner.ts
|
|
429
|
+
var SIZE_CALCULATION_CONCURRENCY = 4;
|
|
242
430
|
async function findRepoRoot(startPath) {
|
|
243
431
|
let currentPath = startPath;
|
|
244
|
-
const root = "/";
|
|
245
|
-
while (currentPath !== root) {
|
|
432
|
+
const root = process.platform === "win32" ? "C:\\" : "/";
|
|
433
|
+
while (currentPath !== root && currentPath !== dirname(currentPath)) {
|
|
246
434
|
const gitPath = join2(currentPath, ".git");
|
|
247
435
|
try {
|
|
248
436
|
if (await fileExists(gitPath)) {
|
|
@@ -253,34 +441,11 @@ async function findRepoRoot(startPath) {
|
|
|
253
441
|
}
|
|
254
442
|
return startPath;
|
|
255
443
|
}
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
const packageJson = await readPackageJson(projectPath);
|
|
260
|
-
const projectName = packageJson?.name || basename(projectPath);
|
|
261
|
-
const projectVersion = packageJson?.version;
|
|
262
|
-
const repoPath = await findRepoRoot(projectPath);
|
|
263
|
-
const sizeCategory = getSizeCategory(totalSize);
|
|
264
|
-
const ageCategory = getAgeCategory(stats.mtime);
|
|
265
|
-
return {
|
|
266
|
-
path: nodeModulesPath,
|
|
267
|
-
projectPath,
|
|
268
|
-
projectName,
|
|
269
|
-
projectVersion,
|
|
270
|
-
repoPath,
|
|
271
|
-
sizeBytes: totalSize,
|
|
272
|
-
sizeFormatted: formatBytes(totalSize),
|
|
273
|
-
packageCount,
|
|
274
|
-
totalPackageCount,
|
|
275
|
-
lastModified: stats.mtime,
|
|
276
|
-
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
277
|
-
selected: false,
|
|
278
|
-
isFavorite: false,
|
|
279
|
-
ageCategory,
|
|
280
|
-
sizeCategory
|
|
281
|
-
};
|
|
444
|
+
function getAgeInDays2(date) {
|
|
445
|
+
const now = new Date;
|
|
446
|
+
return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
282
447
|
}
|
|
283
|
-
async function
|
|
448
|
+
async function calculateDirectorySizeFallback(dirPath) {
|
|
284
449
|
let totalSize = 0;
|
|
285
450
|
let packageCount = 0;
|
|
286
451
|
let totalPackageCount = 0;
|
|
@@ -317,7 +482,7 @@ async function calculateDirectorySize(dirPath) {
|
|
|
317
482
|
pathsToProcess.push(entryPath);
|
|
318
483
|
}
|
|
319
484
|
} catch {}
|
|
320
|
-
}
|
|
485
|
+
}
|
|
321
486
|
} catch {}
|
|
322
487
|
if (currentPath === dirPath) {
|
|
323
488
|
isTopLevel = false;
|
|
@@ -325,9 +490,191 @@ async function calculateDirectorySize(dirPath) {
|
|
|
325
490
|
}
|
|
326
491
|
return { totalSize, packageCount, totalPackageCount };
|
|
327
492
|
}
|
|
328
|
-
function
|
|
329
|
-
const
|
|
330
|
-
|
|
493
|
+
async function calculateSizeWithFallback(dirPath) {
|
|
494
|
+
const nativeResult = await getFastDirectorySize(dirPath);
|
|
495
|
+
if (nativeResult.isNative && nativeResult.bytes > 0) {
|
|
496
|
+
return {
|
|
497
|
+
totalSize: nativeResult.bytes,
|
|
498
|
+
packageCount: nativeResult.packageCount,
|
|
499
|
+
totalPackageCount: nativeResult.totalPackageCount,
|
|
500
|
+
isNative: true
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const fallbackResult = await calculateDirectorySizeFallback(dirPath);
|
|
504
|
+
return {
|
|
505
|
+
...fallbackResult,
|
|
506
|
+
isNative: false
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
async function analyzeNodeModules(nodeModulesPath, projectPath, lazy = false) {
|
|
510
|
+
const stats = await fs2.stat(nodeModulesPath);
|
|
511
|
+
if (lazy) {
|
|
512
|
+
const packageJson2 = await readPackageJson(projectPath);
|
|
513
|
+
const projectName2 = packageJson2?.name || basename(projectPath);
|
|
514
|
+
const repoPath2 = await findRepoRoot(projectPath);
|
|
515
|
+
return {
|
|
516
|
+
path: nodeModulesPath,
|
|
517
|
+
projectPath,
|
|
518
|
+
projectName: projectName2,
|
|
519
|
+
projectVersion: packageJson2?.version,
|
|
520
|
+
repoPath: repoPath2,
|
|
521
|
+
sizeBytes: 0,
|
|
522
|
+
sizeFormatted: "calculating...",
|
|
523
|
+
packageCount: 0,
|
|
524
|
+
totalPackageCount: 0,
|
|
525
|
+
lastModified: stats.mtime,
|
|
526
|
+
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
527
|
+
selected: false,
|
|
528
|
+
isFavorite: false,
|
|
529
|
+
ageCategory: getAgeCategory(stats.mtime),
|
|
530
|
+
sizeCategory: "small",
|
|
531
|
+
isPending: true
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(nodeModulesPath);
|
|
535
|
+
const packageJson = await readPackageJson(projectPath);
|
|
536
|
+
const projectName = packageJson?.name || basename(projectPath);
|
|
537
|
+
const repoPath = await findRepoRoot(projectPath);
|
|
538
|
+
const sizeCategory = getSizeCategory(totalSize);
|
|
539
|
+
const ageCategory = getAgeCategory(stats.mtime);
|
|
540
|
+
return {
|
|
541
|
+
path: nodeModulesPath,
|
|
542
|
+
projectPath,
|
|
543
|
+
projectName,
|
|
544
|
+
projectVersion: packageJson?.version,
|
|
545
|
+
repoPath,
|
|
546
|
+
sizeBytes: totalSize,
|
|
547
|
+
sizeFormatted: formatBytes(totalSize),
|
|
548
|
+
packageCount,
|
|
549
|
+
totalPackageCount,
|
|
550
|
+
lastModified: stats.mtime,
|
|
551
|
+
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
552
|
+
selected: false,
|
|
553
|
+
isFavorite: false,
|
|
554
|
+
ageCategory,
|
|
555
|
+
sizeCategory,
|
|
556
|
+
isNativeCalculation: isNative
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
async function updateNodeModulesSize(info) {
|
|
560
|
+
const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(info.path);
|
|
561
|
+
return {
|
|
562
|
+
...info,
|
|
563
|
+
sizeBytes: totalSize,
|
|
564
|
+
sizeFormatted: formatBytes(totalSize),
|
|
565
|
+
packageCount,
|
|
566
|
+
totalPackageCount,
|
|
567
|
+
sizeCategory: getSizeCategory(totalSize),
|
|
568
|
+
isPending: false,
|
|
569
|
+
isNativeCalculation: isNative
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
async function scanForNodeModules(options, onProgress, lazy = false) {
|
|
573
|
+
const result = {
|
|
574
|
+
nodeModules: [],
|
|
575
|
+
directoriesScanned: 0,
|
|
576
|
+
errors: []
|
|
577
|
+
};
|
|
578
|
+
const visitedPaths = new Set;
|
|
579
|
+
const pathsToScan = [
|
|
580
|
+
{ path: options.rootPath, depth: 0 }
|
|
581
|
+
];
|
|
582
|
+
const foundNodeModules = [];
|
|
583
|
+
let processedCount = 0;
|
|
584
|
+
let totalEstimate = 1;
|
|
585
|
+
while (pathsToScan.length > 0) {
|
|
586
|
+
const { path: currentPath, depth } = pathsToScan.shift();
|
|
587
|
+
if (visitedPaths.has(currentPath))
|
|
588
|
+
continue;
|
|
589
|
+
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
590
|
+
continue;
|
|
591
|
+
if (shouldExcludePath(currentPath, options.excludePatterns))
|
|
592
|
+
continue;
|
|
593
|
+
visitedPaths.add(currentPath);
|
|
594
|
+
result.directoriesScanned++;
|
|
595
|
+
try {
|
|
596
|
+
const entries = await fs2.readdir(currentPath, { withFileTypes: true });
|
|
597
|
+
const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
|
|
598
|
+
if (hasNodeModules) {
|
|
599
|
+
const nodeModulesPath = join2(currentPath, "node_modules");
|
|
600
|
+
foundNodeModules.push({ nodeModulesPath, projectPath: currentPath });
|
|
601
|
+
if (onProgress) {
|
|
602
|
+
onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundNodeModules.length);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
for (const entry of entries) {
|
|
606
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
607
|
+
const subPath = join2(currentPath, entry.name);
|
|
608
|
+
if (!shouldExcludePath(subPath, options.excludePatterns)) {
|
|
609
|
+
pathsToScan.push({ path: subPath, depth: depth + 1 });
|
|
610
|
+
totalEstimate++;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} catch (error) {
|
|
615
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
616
|
+
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
617
|
+
}
|
|
618
|
+
processedCount++;
|
|
619
|
+
if (onProgress && foundNodeModules.length === 0) {
|
|
620
|
+
onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), 0);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (foundNodeModules.length > 0) {
|
|
624
|
+
const sizeLimit = pLimit(SIZE_CALCULATION_CONCURRENCY);
|
|
625
|
+
const analysisPromises = foundNodeModules.map(({ nodeModulesPath, projectPath }) => sizeLimit(async () => {
|
|
626
|
+
try {
|
|
627
|
+
const info = await analyzeNodeModules(nodeModulesPath, projectPath, lazy);
|
|
628
|
+
if (!lazy) {
|
|
629
|
+
const passesFilter = (!options.minSizeBytes || info.sizeBytes >= options.minSizeBytes) && (!options.olderThanDays || getAgeInDays2(info.lastModified) >= options.olderThanDays);
|
|
630
|
+
if (passesFilter) {
|
|
631
|
+
result.nodeModules.push(info);
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
result.nodeModules.push(info);
|
|
635
|
+
}
|
|
636
|
+
} catch (error) {
|
|
637
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
638
|
+
result.errors.push(`Error analyzing ${nodeModulesPath}: ${errorMessage}`);
|
|
639
|
+
}
|
|
640
|
+
}));
|
|
641
|
+
await Promise.all(analysisPromises);
|
|
642
|
+
}
|
|
643
|
+
if (onProgress) {
|
|
644
|
+
onProgress(100, result.nodeModules.length);
|
|
645
|
+
}
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
async function calculatePendingSizes(nodeModules, onProgress) {
|
|
649
|
+
const pending = nodeModules.filter((nm) => nm.isPending);
|
|
650
|
+
if (pending.length === 0)
|
|
651
|
+
return nodeModules;
|
|
652
|
+
const limit = pLimit(SIZE_CALCULATION_CONCURRENCY);
|
|
653
|
+
let completed = 0;
|
|
654
|
+
const total = pending.length;
|
|
655
|
+
const updatePromises = pending.map((pendingItem) => limit(async () => {
|
|
656
|
+
try {
|
|
657
|
+
const updated = await updateNodeModulesSize(pendingItem);
|
|
658
|
+
completed++;
|
|
659
|
+
if (onProgress) {
|
|
660
|
+
onProgress(completed, total);
|
|
661
|
+
}
|
|
662
|
+
return updated;
|
|
663
|
+
} catch (error) {
|
|
664
|
+
completed++;
|
|
665
|
+
if (onProgress) {
|
|
666
|
+
onProgress(completed, total);
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
...pendingItem,
|
|
670
|
+
sizeFormatted: "error",
|
|
671
|
+
isPending: false
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
}));
|
|
675
|
+
const updatedItems = await Promise.all(updatePromises);
|
|
676
|
+
const updatedMap = new Map(updatedItems.map((item) => [item.path, item]));
|
|
677
|
+
return nodeModules.map((item) => updatedMap.get(item.path) || item);
|
|
331
678
|
}
|
|
332
679
|
async function loadIgnorePatterns() {
|
|
333
680
|
const patterns = [
|
|
@@ -576,6 +923,7 @@ function invertSelection(nodeModules) {
|
|
|
576
923
|
// src/index.ts
|
|
577
924
|
var VERSION = "1.0.0";
|
|
578
925
|
export {
|
|
926
|
+
updateNodeModulesSize,
|
|
579
927
|
toggleSelection,
|
|
580
928
|
sortNodeModules,
|
|
581
929
|
selectBySize,
|
|
@@ -598,6 +946,7 @@ export {
|
|
|
598
946
|
filterNodeModules,
|
|
599
947
|
deleteSelectedNodeModules,
|
|
600
948
|
calculateStatistics,
|
|
949
|
+
calculatePendingSizes,
|
|
601
950
|
analyzeNodeModules,
|
|
602
951
|
VERSION,
|
|
603
952
|
SIZE_THRESHOLDS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-node-modules",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Visualize, analyze, and clean up node_modules directories to reclaim disk space",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@clack/prompts": "^0.8.2",
|
|
67
67
|
"commander": "^14.0.3",
|
|
68
|
+
"p-limit": "^7.3.0",
|
|
68
69
|
"picocolors": "^1.1.1",
|
|
69
70
|
"zod": "^3.23.8"
|
|
70
71
|
},
|