oh-my-node-modules 1.2.8 → 1.2.9
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/dist/cli.js +439 -88
- package/dist/index.js +421 -84
- package/package.json +2 -1
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";
|
|
@@ -2948,67 +3104,97 @@ async function readPackageJson(projectPath) {
|
|
|
2948
3104
|
}
|
|
2949
3105
|
}
|
|
2950
3106
|
|
|
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
|
-
}
|
|
3107
|
+
// src/native-size.ts
|
|
3108
|
+
import { exec } from "child_process";
|
|
3109
|
+
import { promisify } from "util";
|
|
3110
|
+
import { platform } from "os";
|
|
3111
|
+
var execAsync = promisify(exec);
|
|
3112
|
+
async function getSizeWithDu(dirPath) {
|
|
3113
|
+
try {
|
|
3114
|
+
const { stdout } = await execAsync(`du -sb "${dirPath}"`, {
|
|
3115
|
+
timeout: 30000,
|
|
3116
|
+
maxBuffer: 1024 * 1024
|
|
3117
|
+
});
|
|
3118
|
+
const match = stdout.trim().match(/^(\d+)/);
|
|
3119
|
+
if (match) {
|
|
3120
|
+
return parseInt(match[1], 10);
|
|
3121
|
+
}
|
|
3122
|
+
return null;
|
|
3123
|
+
} catch {
|
|
3124
|
+
return null;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
async function getSizeWithDir(dirPath) {
|
|
3128
|
+
try {
|
|
3129
|
+
const { stdout } = await execAsync(`dir /s "${dirPath}"`, {
|
|
3130
|
+
timeout: 30000,
|
|
3131
|
+
maxBuffer: 1024 * 1024
|
|
3132
|
+
});
|
|
3133
|
+
const lines = stdout.split(`
|
|
3134
|
+
`);
|
|
3135
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
3136
|
+
const line = lines[i];
|
|
3137
|
+
const match = line.match(/File\(s\)\s+([\d,]+)\s+bytes?/i);
|
|
3138
|
+
if (match) {
|
|
3139
|
+
const bytesStr = match[1].replace(/,/g, "");
|
|
3140
|
+
return parseInt(bytesStr, 10);
|
|
2992
3141
|
}
|
|
2993
|
-
} catch (error) {
|
|
2994
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2995
|
-
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
2996
3142
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3143
|
+
return null;
|
|
3144
|
+
} catch {
|
|
3145
|
+
return null;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
async function countPackagesNative(dirPath) {
|
|
3149
|
+
try {
|
|
3150
|
+
const isWindows = platform() === "win32";
|
|
3151
|
+
if (isWindows) {
|
|
3152
|
+
const { stdout } = await execAsync(`dir /b /ad "${dirPath}" | find /c /v ""`, { timeout: 1e4 });
|
|
3153
|
+
const topLevel = parseInt(stdout.trim(), 10) || 0;
|
|
3154
|
+
return { topLevel, total: topLevel };
|
|
3155
|
+
} else {
|
|
3156
|
+
const { stdout: topLevelOut } = await execAsync(`find "${dirPath}" -maxdepth 1 -type d ! -name ".*" ! -name "node_modules" | wc -l`, { timeout: 1e4 });
|
|
3157
|
+
const topLevel = Math.max(0, parseInt(topLevelOut.trim(), 10) - 1);
|
|
3158
|
+
const { stdout: totalOut } = await execAsync(`find "${dirPath}" -type d ! -path "${dirPath}" ! -name ".*" | wc -l`, { timeout: 1e4 });
|
|
3159
|
+
const total = parseInt(totalOut.trim(), 10);
|
|
3160
|
+
return { topLevel, total };
|
|
3001
3161
|
}
|
|
3162
|
+
} catch {
|
|
3163
|
+
return null;
|
|
3002
3164
|
}
|
|
3003
|
-
|
|
3004
|
-
|
|
3165
|
+
}
|
|
3166
|
+
async function getFastDirectorySize(dirPath) {
|
|
3167
|
+
const isWindows = platform() === "win32";
|
|
3168
|
+
let sizeBytes = null;
|
|
3169
|
+
if (isWindows) {
|
|
3170
|
+
sizeBytes = await getSizeWithDir(dirPath);
|
|
3171
|
+
} else {
|
|
3172
|
+
sizeBytes = await getSizeWithDu(dirPath);
|
|
3005
3173
|
}
|
|
3006
|
-
|
|
3174
|
+
const packageCounts = await countPackagesNative(dirPath);
|
|
3175
|
+
if (sizeBytes !== null && packageCounts !== null) {
|
|
3176
|
+
return {
|
|
3177
|
+
bytes: sizeBytes,
|
|
3178
|
+
packageCount: packageCounts.topLevel,
|
|
3179
|
+
totalPackageCount: packageCounts.total,
|
|
3180
|
+
isNative: true
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
return {
|
|
3184
|
+
bytes: 0,
|
|
3185
|
+
packageCount: 0,
|
|
3186
|
+
totalPackageCount: 0,
|
|
3187
|
+
isNative: false
|
|
3188
|
+
};
|
|
3007
3189
|
}
|
|
3190
|
+
|
|
3191
|
+
// src/scanner.ts
|
|
3192
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
3193
|
+
var SIZE_CALCULATION_CONCURRENCY = 3;
|
|
3008
3194
|
async function findRepoRoot(startPath) {
|
|
3009
3195
|
let currentPath = startPath;
|
|
3010
|
-
const root = "/";
|
|
3011
|
-
while (currentPath !== root) {
|
|
3196
|
+
const root = process.platform === "win32" ? "C:\\" : "/";
|
|
3197
|
+
while (currentPath !== root && currentPath !== dirname(currentPath)) {
|
|
3012
3198
|
const gitPath = join2(currentPath, ".git");
|
|
3013
3199
|
try {
|
|
3014
3200
|
if (await fileExists(gitPath)) {
|
|
@@ -3019,34 +3205,11 @@ async function findRepoRoot(startPath) {
|
|
|
3019
3205
|
}
|
|
3020
3206
|
return startPath;
|
|
3021
3207
|
}
|
|
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
|
-
};
|
|
3208
|
+
function getAgeInDays2(date) {
|
|
3209
|
+
const now = new Date;
|
|
3210
|
+
return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
3048
3211
|
}
|
|
3049
|
-
async function
|
|
3212
|
+
async function calculateDirectorySizeFallback(dirPath) {
|
|
3050
3213
|
let totalSize = 0;
|
|
3051
3214
|
let packageCount = 0;
|
|
3052
3215
|
let totalPackageCount = 0;
|
|
@@ -3083,7 +3246,7 @@ async function calculateDirectorySize(dirPath) {
|
|
|
3083
3246
|
pathsToProcess.push(entryPath);
|
|
3084
3247
|
}
|
|
3085
3248
|
} catch {}
|
|
3086
|
-
}
|
|
3249
|
+
}
|
|
3087
3250
|
} catch {}
|
|
3088
3251
|
if (currentPath === dirPath) {
|
|
3089
3252
|
isTopLevel = false;
|
|
@@ -3091,9 +3254,181 @@ async function calculateDirectorySize(dirPath) {
|
|
|
3091
3254
|
}
|
|
3092
3255
|
return { totalSize, packageCount, totalPackageCount };
|
|
3093
3256
|
}
|
|
3094
|
-
function
|
|
3095
|
-
const
|
|
3096
|
-
|
|
3257
|
+
async function calculateSizeWithFallback(dirPath) {
|
|
3258
|
+
const nativeResult = await getFastDirectorySize(dirPath);
|
|
3259
|
+
if (nativeResult.isNative && nativeResult.bytes > 0) {
|
|
3260
|
+
return {
|
|
3261
|
+
totalSize: nativeResult.bytes,
|
|
3262
|
+
packageCount: nativeResult.packageCount,
|
|
3263
|
+
totalPackageCount: nativeResult.totalPackageCount,
|
|
3264
|
+
isNative: true
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
const fallbackResult = await calculateDirectorySizeFallback(dirPath);
|
|
3268
|
+
return {
|
|
3269
|
+
...fallbackResult,
|
|
3270
|
+
isNative: false
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
async function analyzeNodeModules(nodeModulesPath, projectPath, lazy = false) {
|
|
3274
|
+
const stats = await fs2.stat(nodeModulesPath);
|
|
3275
|
+
if (lazy) {
|
|
3276
|
+
const packageJson2 = await readPackageJson(projectPath);
|
|
3277
|
+
const projectName2 = packageJson2?.name || basename(projectPath);
|
|
3278
|
+
const repoPath2 = await findRepoRoot(projectPath);
|
|
3279
|
+
return {
|
|
3280
|
+
path: nodeModulesPath,
|
|
3281
|
+
projectPath,
|
|
3282
|
+
projectName: projectName2,
|
|
3283
|
+
projectVersion: packageJson2?.version,
|
|
3284
|
+
repoPath: repoPath2,
|
|
3285
|
+
sizeBytes: 0,
|
|
3286
|
+
sizeFormatted: "calculating...",
|
|
3287
|
+
packageCount: 0,
|
|
3288
|
+
totalPackageCount: 0,
|
|
3289
|
+
lastModified: stats.mtime,
|
|
3290
|
+
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
3291
|
+
selected: false,
|
|
3292
|
+
isFavorite: false,
|
|
3293
|
+
ageCategory: getAgeCategory(stats.mtime),
|
|
3294
|
+
sizeCategory: "small",
|
|
3295
|
+
isPending: true
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(nodeModulesPath);
|
|
3299
|
+
const packageJson = await readPackageJson(projectPath);
|
|
3300
|
+
const projectName = packageJson?.name || basename(projectPath);
|
|
3301
|
+
const repoPath = await findRepoRoot(projectPath);
|
|
3302
|
+
const sizeCategory = getSizeCategory(totalSize);
|
|
3303
|
+
const ageCategory = getAgeCategory(stats.mtime);
|
|
3304
|
+
return {
|
|
3305
|
+
path: nodeModulesPath,
|
|
3306
|
+
projectPath,
|
|
3307
|
+
projectName,
|
|
3308
|
+
projectVersion: packageJson?.version,
|
|
3309
|
+
repoPath,
|
|
3310
|
+
sizeBytes: totalSize,
|
|
3311
|
+
sizeFormatted: formatBytes(totalSize),
|
|
3312
|
+
packageCount,
|
|
3313
|
+
totalPackageCount,
|
|
3314
|
+
lastModified: stats.mtime,
|
|
3315
|
+
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
3316
|
+
selected: false,
|
|
3317
|
+
isFavorite: false,
|
|
3318
|
+
ageCategory,
|
|
3319
|
+
sizeCategory,
|
|
3320
|
+
isNativeCalculation: isNative
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
async function updateNodeModulesSize(info) {
|
|
3324
|
+
const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(info.path);
|
|
3325
|
+
return {
|
|
3326
|
+
...info,
|
|
3327
|
+
sizeBytes: totalSize,
|
|
3328
|
+
sizeFormatted: formatBytes(totalSize),
|
|
3329
|
+
packageCount,
|
|
3330
|
+
totalPackageCount,
|
|
3331
|
+
sizeCategory: getSizeCategory(totalSize),
|
|
3332
|
+
isPending: false,
|
|
3333
|
+
isNativeCalculation: isNative
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
async function scanForNodeModules(options, onProgress, lazy = false) {
|
|
3337
|
+
const result = {
|
|
3338
|
+
nodeModules: [],
|
|
3339
|
+
directoriesScanned: 0,
|
|
3340
|
+
errors: []
|
|
3341
|
+
};
|
|
3342
|
+
const visitedPaths = new Set;
|
|
3343
|
+
const pathsToScan = [
|
|
3344
|
+
{ path: options.rootPath, depth: 0 }
|
|
3345
|
+
];
|
|
3346
|
+
const scanLimit = pLimit(DEFAULT_CONCURRENCY);
|
|
3347
|
+
const sizeLimit = pLimit(SIZE_CALCULATION_CONCURRENCY);
|
|
3348
|
+
let processedCount = 0;
|
|
3349
|
+
let totalEstimate = 1;
|
|
3350
|
+
let foundCount = 0;
|
|
3351
|
+
while (pathsToScan.length > 0) {
|
|
3352
|
+
const batch = pathsToScan.splice(0, Math.min(pathsToScan.length, DEFAULT_CONCURRENCY * 2));
|
|
3353
|
+
const scanPromises = batch.map(({ path: currentPath, depth }) => scanLimit(async () => {
|
|
3354
|
+
if (visitedPaths.has(currentPath))
|
|
3355
|
+
return;
|
|
3356
|
+
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
3357
|
+
return;
|
|
3358
|
+
if (shouldExcludePath(currentPath, options.excludePatterns))
|
|
3359
|
+
return;
|
|
3360
|
+
visitedPaths.add(currentPath);
|
|
3361
|
+
result.directoriesScanned++;
|
|
3362
|
+
try {
|
|
3363
|
+
const entries = await fs2.readdir(currentPath, { withFileTypes: true });
|
|
3364
|
+
const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
|
|
3365
|
+
if (hasNodeModules) {
|
|
3366
|
+
const nodeModulesPath = join2(currentPath, "node_modules");
|
|
3367
|
+
const info = await sizeLimit(() => analyzeNodeModules(nodeModulesPath, currentPath, lazy));
|
|
3368
|
+
const passesFilter = (!options.minSizeBytes || info.sizeBytes >= options.minSizeBytes) && (!options.olderThanDays || getAgeInDays2(info.lastModified) >= options.olderThanDays);
|
|
3369
|
+
if (passesFilter || lazy) {
|
|
3370
|
+
result.nodeModules.push(info);
|
|
3371
|
+
foundCount++;
|
|
3372
|
+
if (onProgress) {
|
|
3373
|
+
onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundCount);
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
for (const entry of entries) {
|
|
3378
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
3379
|
+
const subPath = join2(currentPath, entry.name);
|
|
3380
|
+
if (!shouldExcludePath(subPath, options.excludePatterns)) {
|
|
3381
|
+
pathsToScan.push({ path: subPath, depth: depth + 1 });
|
|
3382
|
+
totalEstimate++;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
} catch (error) {
|
|
3387
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3388
|
+
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
3389
|
+
}
|
|
3390
|
+
processedCount++;
|
|
3391
|
+
if (onProgress) {
|
|
3392
|
+
onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundCount);
|
|
3393
|
+
}
|
|
3394
|
+
}));
|
|
3395
|
+
await Promise.all(scanPromises);
|
|
3396
|
+
}
|
|
3397
|
+
if (onProgress) {
|
|
3398
|
+
onProgress(100, foundCount);
|
|
3399
|
+
}
|
|
3400
|
+
return result;
|
|
3401
|
+
}
|
|
3402
|
+
async function calculatePendingSizes(nodeModules, onProgress) {
|
|
3403
|
+
const pending = nodeModules.filter((nm) => nm.isPending);
|
|
3404
|
+
if (pending.length === 0)
|
|
3405
|
+
return nodeModules;
|
|
3406
|
+
const limit = pLimit(SIZE_CALCULATION_CONCURRENCY);
|
|
3407
|
+
let completed = 0;
|
|
3408
|
+
const total = pending.length;
|
|
3409
|
+
const updatePromises = pending.map((pendingItem) => limit(async () => {
|
|
3410
|
+
try {
|
|
3411
|
+
const updated = await updateNodeModulesSize(pendingItem);
|
|
3412
|
+
completed++;
|
|
3413
|
+
if (onProgress) {
|
|
3414
|
+
onProgress(completed, total);
|
|
3415
|
+
}
|
|
3416
|
+
return updated;
|
|
3417
|
+
} catch (error) {
|
|
3418
|
+
completed++;
|
|
3419
|
+
if (onProgress) {
|
|
3420
|
+
onProgress(completed, total);
|
|
3421
|
+
}
|
|
3422
|
+
return {
|
|
3423
|
+
...pendingItem,
|
|
3424
|
+
sizeFormatted: "error",
|
|
3425
|
+
isPending: false
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
}));
|
|
3429
|
+
const updatedItems = await Promise.all(updatePromises);
|
|
3430
|
+
const updatedMap = new Map(updatedItems.map((item) => [item.path, item]));
|
|
3431
|
+
return nodeModules.map((item) => updatedMap.get(item.path) || item);
|
|
3097
3432
|
}
|
|
3098
3433
|
async function loadIgnorePatterns() {
|
|
3099
3434
|
const patterns = [
|
|
@@ -3253,9 +3588,10 @@ function getVersion() {
|
|
|
3253
3588
|
}
|
|
3254
3589
|
}
|
|
3255
3590
|
function formatItem(item) {
|
|
3256
|
-
const
|
|
3591
|
+
const isPending = item.isPending;
|
|
3592
|
+
const size = isPending ? import_picocolors2.default.yellow(" (...) ") : import_picocolors2.default.cyan(item.sizeFormatted.padStart(8));
|
|
3257
3593
|
const age = import_picocolors2.default.gray(item.lastModifiedFormatted);
|
|
3258
|
-
const warning = item.sizeCategory === "huge" ? import_picocolors2.default.red(" ⚠") : "";
|
|
3594
|
+
const warning = !isPending && item.sizeCategory === "huge" ? import_picocolors2.default.red(" ⚠") : "";
|
|
3259
3595
|
return `${size} ${item.projectName}${warning} [${age}]`;
|
|
3260
3596
|
}
|
|
3261
3597
|
async function interactiveMode(rootPath) {
|
|
@@ -3268,13 +3604,28 @@ async function interactiveMode(rootPath) {
|
|
|
3268
3604
|
rootPath,
|
|
3269
3605
|
excludePatterns,
|
|
3270
3606
|
followSymlinks: false
|
|
3271
|
-
})
|
|
3607
|
+
}, (_progress, found) => {
|
|
3608
|
+
if (found > 0) {
|
|
3609
|
+
s.message(`Scanning... found ${found} node_modules`);
|
|
3610
|
+
}
|
|
3611
|
+
}, true);
|
|
3272
3612
|
s.stop(`Found ${result.nodeModules.length} node_modules directories`);
|
|
3273
3613
|
if (result.nodeModules.length === 0) {
|
|
3274
3614
|
ge(import_picocolors2.default.yellow("No node_modules found."));
|
|
3275
3615
|
return;
|
|
3276
3616
|
}
|
|
3277
3617
|
let items = sortNodeModules(result.nodeModules, "size-desc");
|
|
3618
|
+
console.log(`
|
|
3619
|
+
${import_picocolors2.default.gray("Calculating sizes...")}`);
|
|
3620
|
+
console.log(`${import_picocolors2.default.gray("Projects sorted by size:")}
|
|
3621
|
+
`);
|
|
3622
|
+
const sizeSpinner = _2();
|
|
3623
|
+
sizeSpinner.start("Calculating directory sizes...");
|
|
3624
|
+
items = await calculatePendingSizes(items, (completed, total) => {
|
|
3625
|
+
sizeSpinner.message(`Calculating sizes... ${completed}/${total}`);
|
|
3626
|
+
});
|
|
3627
|
+
sizeSpinner.stop(`Calculated sizes for ${items.length} directories`);
|
|
3628
|
+
items = sortNodeModules(items, "size-desc");
|
|
3278
3629
|
const stats = calculateStatistics(items);
|
|
3279
3630
|
console.log(`
|
|
3280
3631
|
${import_picocolors2.default.gray("Total:")} ${import_picocolors2.default.white(stats.totalSizeFormatted)} across ${import_picocolors2.default.white(String(stats.totalProjects))} projects`);
|
|
@@ -3433,7 +3784,7 @@ Dry run - no files deleted.`));
|
|
|
3433
3784
|
}
|
|
3434
3785
|
}
|
|
3435
3786
|
var program2 = new Command;
|
|
3436
|
-
program2.name("onm").description("Find and clean up node_modules directories").version(getVersion()).argument("
|
|
3787
|
+
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
3788
|
if (options.scan) {
|
|
3438
3789
|
await quickScanMode(path, options.json);
|
|
3439
3790
|
} 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";
|
|
@@ -182,67 +338,97 @@ async function readPackageJson(projectPath) {
|
|
|
182
338
|
}
|
|
183
339
|
}
|
|
184
340
|
|
|
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
|
-
}
|
|
341
|
+
// src/native-size.ts
|
|
342
|
+
import { exec } from "child_process";
|
|
343
|
+
import { promisify } from "util";
|
|
344
|
+
import { platform } from "os";
|
|
345
|
+
var execAsync = promisify(exec);
|
|
346
|
+
async function getSizeWithDu(dirPath) {
|
|
347
|
+
try {
|
|
348
|
+
const { stdout } = await execAsync(`du -sb "${dirPath}"`, {
|
|
349
|
+
timeout: 30000,
|
|
350
|
+
maxBuffer: 1024 * 1024
|
|
351
|
+
});
|
|
352
|
+
const match = stdout.trim().match(/^(\d+)/);
|
|
353
|
+
if (match) {
|
|
354
|
+
return parseInt(match[1], 10);
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function getSizeWithDir(dirPath) {
|
|
362
|
+
try {
|
|
363
|
+
const { stdout } = await execAsync(`dir /s "${dirPath}"`, {
|
|
364
|
+
timeout: 30000,
|
|
365
|
+
maxBuffer: 1024 * 1024
|
|
366
|
+
});
|
|
367
|
+
const lines = stdout.split(`
|
|
368
|
+
`);
|
|
369
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
370
|
+
const line = lines[i];
|
|
371
|
+
const match = line.match(/File\(s\)\s+([\d,]+)\s+bytes?/i);
|
|
372
|
+
if (match) {
|
|
373
|
+
const bytesStr = match[1].replace(/,/g, "");
|
|
374
|
+
return parseInt(bytesStr, 10);
|
|
226
375
|
}
|
|
227
|
-
} catch (error) {
|
|
228
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
229
|
-
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
230
376
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
377
|
+
return null;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function countPackagesNative(dirPath) {
|
|
383
|
+
try {
|
|
384
|
+
const isWindows = platform() === "win32";
|
|
385
|
+
if (isWindows) {
|
|
386
|
+
const { stdout } = await execAsync(`dir /b /ad "${dirPath}" | find /c /v ""`, { timeout: 1e4 });
|
|
387
|
+
const topLevel = parseInt(stdout.trim(), 10) || 0;
|
|
388
|
+
return { topLevel, total: topLevel };
|
|
389
|
+
} else {
|
|
390
|
+
const { stdout: topLevelOut } = await execAsync(`find "${dirPath}" -maxdepth 1 -type d ! -name ".*" ! -name "node_modules" | wc -l`, { timeout: 1e4 });
|
|
391
|
+
const topLevel = Math.max(0, parseInt(topLevelOut.trim(), 10) - 1);
|
|
392
|
+
const { stdout: totalOut } = await execAsync(`find "${dirPath}" -type d ! -path "${dirPath}" ! -name ".*" | wc -l`, { timeout: 1e4 });
|
|
393
|
+
const total = parseInt(totalOut.trim(), 10);
|
|
394
|
+
return { topLevel, total };
|
|
235
395
|
}
|
|
396
|
+
} catch {
|
|
397
|
+
return null;
|
|
236
398
|
}
|
|
237
|
-
|
|
238
|
-
|
|
399
|
+
}
|
|
400
|
+
async function getFastDirectorySize(dirPath) {
|
|
401
|
+
const isWindows = platform() === "win32";
|
|
402
|
+
let sizeBytes = null;
|
|
403
|
+
if (isWindows) {
|
|
404
|
+
sizeBytes = await getSizeWithDir(dirPath);
|
|
405
|
+
} else {
|
|
406
|
+
sizeBytes = await getSizeWithDu(dirPath);
|
|
239
407
|
}
|
|
240
|
-
|
|
408
|
+
const packageCounts = await countPackagesNative(dirPath);
|
|
409
|
+
if (sizeBytes !== null && packageCounts !== null) {
|
|
410
|
+
return {
|
|
411
|
+
bytes: sizeBytes,
|
|
412
|
+
packageCount: packageCounts.topLevel,
|
|
413
|
+
totalPackageCount: packageCounts.total,
|
|
414
|
+
isNative: true
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
bytes: 0,
|
|
419
|
+
packageCount: 0,
|
|
420
|
+
totalPackageCount: 0,
|
|
421
|
+
isNative: false
|
|
422
|
+
};
|
|
241
423
|
}
|
|
424
|
+
|
|
425
|
+
// src/scanner.ts
|
|
426
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
427
|
+
var SIZE_CALCULATION_CONCURRENCY = 3;
|
|
242
428
|
async function findRepoRoot(startPath) {
|
|
243
429
|
let currentPath = startPath;
|
|
244
|
-
const root = "/";
|
|
245
|
-
while (currentPath !== root) {
|
|
430
|
+
const root = process.platform === "win32" ? "C:\\" : "/";
|
|
431
|
+
while (currentPath !== root && currentPath !== dirname(currentPath)) {
|
|
246
432
|
const gitPath = join2(currentPath, ".git");
|
|
247
433
|
try {
|
|
248
434
|
if (await fileExists(gitPath)) {
|
|
@@ -253,34 +439,11 @@ async function findRepoRoot(startPath) {
|
|
|
253
439
|
}
|
|
254
440
|
return startPath;
|
|
255
441
|
}
|
|
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
|
-
};
|
|
442
|
+
function getAgeInDays2(date) {
|
|
443
|
+
const now = new Date;
|
|
444
|
+
return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
282
445
|
}
|
|
283
|
-
async function
|
|
446
|
+
async function calculateDirectorySizeFallback(dirPath) {
|
|
284
447
|
let totalSize = 0;
|
|
285
448
|
let packageCount = 0;
|
|
286
449
|
let totalPackageCount = 0;
|
|
@@ -317,7 +480,7 @@ async function calculateDirectorySize(dirPath) {
|
|
|
317
480
|
pathsToProcess.push(entryPath);
|
|
318
481
|
}
|
|
319
482
|
} catch {}
|
|
320
|
-
}
|
|
483
|
+
}
|
|
321
484
|
} catch {}
|
|
322
485
|
if (currentPath === dirPath) {
|
|
323
486
|
isTopLevel = false;
|
|
@@ -325,9 +488,181 @@ async function calculateDirectorySize(dirPath) {
|
|
|
325
488
|
}
|
|
326
489
|
return { totalSize, packageCount, totalPackageCount };
|
|
327
490
|
}
|
|
328
|
-
function
|
|
329
|
-
const
|
|
330
|
-
|
|
491
|
+
async function calculateSizeWithFallback(dirPath) {
|
|
492
|
+
const nativeResult = await getFastDirectorySize(dirPath);
|
|
493
|
+
if (nativeResult.isNative && nativeResult.bytes > 0) {
|
|
494
|
+
return {
|
|
495
|
+
totalSize: nativeResult.bytes,
|
|
496
|
+
packageCount: nativeResult.packageCount,
|
|
497
|
+
totalPackageCount: nativeResult.totalPackageCount,
|
|
498
|
+
isNative: true
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
const fallbackResult = await calculateDirectorySizeFallback(dirPath);
|
|
502
|
+
return {
|
|
503
|
+
...fallbackResult,
|
|
504
|
+
isNative: false
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async function analyzeNodeModules(nodeModulesPath, projectPath, lazy = false) {
|
|
508
|
+
const stats = await fs2.stat(nodeModulesPath);
|
|
509
|
+
if (lazy) {
|
|
510
|
+
const packageJson2 = await readPackageJson(projectPath);
|
|
511
|
+
const projectName2 = packageJson2?.name || basename(projectPath);
|
|
512
|
+
const repoPath2 = await findRepoRoot(projectPath);
|
|
513
|
+
return {
|
|
514
|
+
path: nodeModulesPath,
|
|
515
|
+
projectPath,
|
|
516
|
+
projectName: projectName2,
|
|
517
|
+
projectVersion: packageJson2?.version,
|
|
518
|
+
repoPath: repoPath2,
|
|
519
|
+
sizeBytes: 0,
|
|
520
|
+
sizeFormatted: "calculating...",
|
|
521
|
+
packageCount: 0,
|
|
522
|
+
totalPackageCount: 0,
|
|
523
|
+
lastModified: stats.mtime,
|
|
524
|
+
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
525
|
+
selected: false,
|
|
526
|
+
isFavorite: false,
|
|
527
|
+
ageCategory: getAgeCategory(stats.mtime),
|
|
528
|
+
sizeCategory: "small",
|
|
529
|
+
isPending: true
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(nodeModulesPath);
|
|
533
|
+
const packageJson = await readPackageJson(projectPath);
|
|
534
|
+
const projectName = packageJson?.name || basename(projectPath);
|
|
535
|
+
const repoPath = await findRepoRoot(projectPath);
|
|
536
|
+
const sizeCategory = getSizeCategory(totalSize);
|
|
537
|
+
const ageCategory = getAgeCategory(stats.mtime);
|
|
538
|
+
return {
|
|
539
|
+
path: nodeModulesPath,
|
|
540
|
+
projectPath,
|
|
541
|
+
projectName,
|
|
542
|
+
projectVersion: packageJson?.version,
|
|
543
|
+
repoPath,
|
|
544
|
+
sizeBytes: totalSize,
|
|
545
|
+
sizeFormatted: formatBytes(totalSize),
|
|
546
|
+
packageCount,
|
|
547
|
+
totalPackageCount,
|
|
548
|
+
lastModified: stats.mtime,
|
|
549
|
+
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
550
|
+
selected: false,
|
|
551
|
+
isFavorite: false,
|
|
552
|
+
ageCategory,
|
|
553
|
+
sizeCategory,
|
|
554
|
+
isNativeCalculation: isNative
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
async function updateNodeModulesSize(info) {
|
|
558
|
+
const { totalSize, packageCount, totalPackageCount, isNative } = await calculateSizeWithFallback(info.path);
|
|
559
|
+
return {
|
|
560
|
+
...info,
|
|
561
|
+
sizeBytes: totalSize,
|
|
562
|
+
sizeFormatted: formatBytes(totalSize),
|
|
563
|
+
packageCount,
|
|
564
|
+
totalPackageCount,
|
|
565
|
+
sizeCategory: getSizeCategory(totalSize),
|
|
566
|
+
isPending: false,
|
|
567
|
+
isNativeCalculation: isNative
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async function scanForNodeModules(options, onProgress, lazy = false) {
|
|
571
|
+
const result = {
|
|
572
|
+
nodeModules: [],
|
|
573
|
+
directoriesScanned: 0,
|
|
574
|
+
errors: []
|
|
575
|
+
};
|
|
576
|
+
const visitedPaths = new Set;
|
|
577
|
+
const pathsToScan = [
|
|
578
|
+
{ path: options.rootPath, depth: 0 }
|
|
579
|
+
];
|
|
580
|
+
const scanLimit = pLimit(DEFAULT_CONCURRENCY);
|
|
581
|
+
const sizeLimit = pLimit(SIZE_CALCULATION_CONCURRENCY);
|
|
582
|
+
let processedCount = 0;
|
|
583
|
+
let totalEstimate = 1;
|
|
584
|
+
let foundCount = 0;
|
|
585
|
+
while (pathsToScan.length > 0) {
|
|
586
|
+
const batch = pathsToScan.splice(0, Math.min(pathsToScan.length, DEFAULT_CONCURRENCY * 2));
|
|
587
|
+
const scanPromises = batch.map(({ path: currentPath, depth }) => scanLimit(async () => {
|
|
588
|
+
if (visitedPaths.has(currentPath))
|
|
589
|
+
return;
|
|
590
|
+
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
591
|
+
return;
|
|
592
|
+
if (shouldExcludePath(currentPath, options.excludePatterns))
|
|
593
|
+
return;
|
|
594
|
+
visitedPaths.add(currentPath);
|
|
595
|
+
result.directoriesScanned++;
|
|
596
|
+
try {
|
|
597
|
+
const entries = await fs2.readdir(currentPath, { withFileTypes: true });
|
|
598
|
+
const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
|
|
599
|
+
if (hasNodeModules) {
|
|
600
|
+
const nodeModulesPath = join2(currentPath, "node_modules");
|
|
601
|
+
const info = await sizeLimit(() => analyzeNodeModules(nodeModulesPath, currentPath, lazy));
|
|
602
|
+
const passesFilter = (!options.minSizeBytes || info.sizeBytes >= options.minSizeBytes) && (!options.olderThanDays || getAgeInDays2(info.lastModified) >= options.olderThanDays);
|
|
603
|
+
if (passesFilter || lazy) {
|
|
604
|
+
result.nodeModules.push(info);
|
|
605
|
+
foundCount++;
|
|
606
|
+
if (onProgress) {
|
|
607
|
+
onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundCount);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
for (const entry of entries) {
|
|
612
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
613
|
+
const subPath = join2(currentPath, entry.name);
|
|
614
|
+
if (!shouldExcludePath(subPath, options.excludePatterns)) {
|
|
615
|
+
pathsToScan.push({ path: subPath, depth: depth + 1 });
|
|
616
|
+
totalEstimate++;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
} catch (error) {
|
|
621
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
622
|
+
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
623
|
+
}
|
|
624
|
+
processedCount++;
|
|
625
|
+
if (onProgress) {
|
|
626
|
+
onProgress(Math.min(100, Math.round(processedCount / totalEstimate * 100)), foundCount);
|
|
627
|
+
}
|
|
628
|
+
}));
|
|
629
|
+
await Promise.all(scanPromises);
|
|
630
|
+
}
|
|
631
|
+
if (onProgress) {
|
|
632
|
+
onProgress(100, foundCount);
|
|
633
|
+
}
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
636
|
+
async function calculatePendingSizes(nodeModules, onProgress) {
|
|
637
|
+
const pending = nodeModules.filter((nm) => nm.isPending);
|
|
638
|
+
if (pending.length === 0)
|
|
639
|
+
return nodeModules;
|
|
640
|
+
const limit = pLimit(SIZE_CALCULATION_CONCURRENCY);
|
|
641
|
+
let completed = 0;
|
|
642
|
+
const total = pending.length;
|
|
643
|
+
const updatePromises = pending.map((pendingItem) => limit(async () => {
|
|
644
|
+
try {
|
|
645
|
+
const updated = await updateNodeModulesSize(pendingItem);
|
|
646
|
+
completed++;
|
|
647
|
+
if (onProgress) {
|
|
648
|
+
onProgress(completed, total);
|
|
649
|
+
}
|
|
650
|
+
return updated;
|
|
651
|
+
} catch (error) {
|
|
652
|
+
completed++;
|
|
653
|
+
if (onProgress) {
|
|
654
|
+
onProgress(completed, total);
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
...pendingItem,
|
|
658
|
+
sizeFormatted: "error",
|
|
659
|
+
isPending: false
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}));
|
|
663
|
+
const updatedItems = await Promise.all(updatePromises);
|
|
664
|
+
const updatedMap = new Map(updatedItems.map((item) => [item.path, item]));
|
|
665
|
+
return nodeModules.map((item) => updatedMap.get(item.path) || item);
|
|
331
666
|
}
|
|
332
667
|
async function loadIgnorePatterns() {
|
|
333
668
|
const patterns = [
|
|
@@ -576,6 +911,7 @@ function invertSelection(nodeModules) {
|
|
|
576
911
|
// src/index.ts
|
|
577
912
|
var VERSION = "1.0.0";
|
|
578
913
|
export {
|
|
914
|
+
updateNodeModulesSize,
|
|
579
915
|
toggleSelection,
|
|
580
916
|
sortNodeModules,
|
|
581
917
|
selectBySize,
|
|
@@ -598,6 +934,7 @@ export {
|
|
|
598
934
|
filterNodeModules,
|
|
599
935
|
deleteSelectedNodeModules,
|
|
600
936
|
calculateStatistics,
|
|
937
|
+
calculatePendingSizes,
|
|
601
938
|
analyzeNodeModules,
|
|
602
939
|
VERSION,
|
|
603
940
|
SIZE_THRESHOLDS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-node-modules",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
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
|
},
|