oh-my-node-modules 1.2.7 → 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 +450 -83
- package/dist/index.js +435 -80
- 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,89 +3104,112 @@ 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
|
-
const { path: currentPath, depth } = pathsToScan.shift();
|
|
2966
|
-
if (visitedPaths.has(currentPath))
|
|
2967
|
-
continue;
|
|
2968
|
-
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
2969
|
-
continue;
|
|
2970
|
-
if (shouldExcludePath(currentPath, options.excludePatterns))
|
|
2971
|
-
continue;
|
|
2972
|
-
visitedPaths.add(currentPath);
|
|
2973
|
-
result.directoriesScanned++;
|
|
2974
|
-
try {
|
|
2975
|
-
const entries = await fs2.readdir(currentPath, { withFileTypes: true });
|
|
2976
|
-
const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
|
|
2977
|
-
if (hasNodeModules) {
|
|
2978
|
-
const nodeModulesPath = join2(currentPath, "node_modules");
|
|
2979
|
-
const info = await analyzeNodeModules(nodeModulesPath, currentPath);
|
|
2980
|
-
if (options.minSizeBytes && info.sizeBytes < options.minSizeBytes) {} else if (options.olderThanDays && getAgeInDays2(info.lastModified) < options.olderThanDays) {} else {
|
|
2981
|
-
result.nodeModules.push(info);
|
|
2982
|
-
}
|
|
2983
|
-
}
|
|
2984
|
-
for (const entry of entries) {
|
|
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
|
-
}
|
|
2992
|
-
}
|
|
2993
|
-
} catch (error) {
|
|
2994
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2995
|
-
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
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);
|
|
2996
3121
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
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);
|
|
3141
|
+
}
|
|
3001
3142
|
}
|
|
3143
|
+
return null;
|
|
3144
|
+
} catch {
|
|
3145
|
+
return null;
|
|
3002
3146
|
}
|
|
3003
|
-
|
|
3004
|
-
|
|
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 };
|
|
3161
|
+
}
|
|
3162
|
+
} catch {
|
|
3163
|
+
return null;
|
|
3005
3164
|
}
|
|
3006
|
-
return result;
|
|
3007
3165
|
}
|
|
3008
|
-
async function
|
|
3009
|
-
const
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
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);
|
|
3173
|
+
}
|
|
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
|
+
}
|
|
3016
3183
|
return {
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
sizeBytes: totalSize,
|
|
3022
|
-
sizeFormatted: formatBytes(totalSize),
|
|
3023
|
-
packageCount,
|
|
3024
|
-
totalPackageCount,
|
|
3025
|
-
lastModified: stats.mtime,
|
|
3026
|
-
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
3027
|
-
selected: false,
|
|
3028
|
-
isFavorite: false,
|
|
3029
|
-
ageCategory,
|
|
3030
|
-
sizeCategory
|
|
3184
|
+
bytes: 0,
|
|
3185
|
+
packageCount: 0,
|
|
3186
|
+
totalPackageCount: 0,
|
|
3187
|
+
isNative: false
|
|
3031
3188
|
};
|
|
3032
3189
|
}
|
|
3033
|
-
|
|
3190
|
+
|
|
3191
|
+
// src/scanner.ts
|
|
3192
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
3193
|
+
var SIZE_CALCULATION_CONCURRENCY = 3;
|
|
3194
|
+
async function findRepoRoot(startPath) {
|
|
3195
|
+
let currentPath = startPath;
|
|
3196
|
+
const root = process.platform === "win32" ? "C:\\" : "/";
|
|
3197
|
+
while (currentPath !== root && currentPath !== dirname(currentPath)) {
|
|
3198
|
+
const gitPath = join2(currentPath, ".git");
|
|
3199
|
+
try {
|
|
3200
|
+
if (await fileExists(gitPath)) {
|
|
3201
|
+
return currentPath;
|
|
3202
|
+
}
|
|
3203
|
+
} catch {}
|
|
3204
|
+
currentPath = dirname(currentPath);
|
|
3205
|
+
}
|
|
3206
|
+
return startPath;
|
|
3207
|
+
}
|
|
3208
|
+
function getAgeInDays2(date) {
|
|
3209
|
+
const now = new Date;
|
|
3210
|
+
return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
3211
|
+
}
|
|
3212
|
+
async function calculateDirectorySizeFallback(dirPath) {
|
|
3034
3213
|
let totalSize = 0;
|
|
3035
3214
|
let packageCount = 0;
|
|
3036
3215
|
let totalPackageCount = 0;
|
|
@@ -3067,7 +3246,7 @@ async function calculateDirectorySize(dirPath) {
|
|
|
3067
3246
|
pathsToProcess.push(entryPath);
|
|
3068
3247
|
}
|
|
3069
3248
|
} catch {}
|
|
3070
|
-
}
|
|
3249
|
+
}
|
|
3071
3250
|
} catch {}
|
|
3072
3251
|
if (currentPath === dirPath) {
|
|
3073
3252
|
isTopLevel = false;
|
|
@@ -3075,9 +3254,181 @@ async function calculateDirectorySize(dirPath) {
|
|
|
3075
3254
|
}
|
|
3076
3255
|
return { totalSize, packageCount, totalPackageCount };
|
|
3077
3256
|
}
|
|
3078
|
-
function
|
|
3079
|
-
const
|
|
3080
|
-
|
|
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);
|
|
3081
3432
|
}
|
|
3082
3433
|
async function loadIgnorePatterns() {
|
|
3083
3434
|
const patterns = [
|
|
@@ -3237,9 +3588,10 @@ function getVersion() {
|
|
|
3237
3588
|
}
|
|
3238
3589
|
}
|
|
3239
3590
|
function formatItem(item) {
|
|
3240
|
-
const
|
|
3591
|
+
const isPending = item.isPending;
|
|
3592
|
+
const size = isPending ? import_picocolors2.default.yellow(" (...) ") : import_picocolors2.default.cyan(item.sizeFormatted.padStart(8));
|
|
3241
3593
|
const age = import_picocolors2.default.gray(item.lastModifiedFormatted);
|
|
3242
|
-
const warning = item.sizeCategory === "huge" ? import_picocolors2.default.red(" ⚠") : "";
|
|
3594
|
+
const warning = !isPending && item.sizeCategory === "huge" ? import_picocolors2.default.red(" ⚠") : "";
|
|
3243
3595
|
return `${size} ${item.projectName}${warning} [${age}]`;
|
|
3244
3596
|
}
|
|
3245
3597
|
async function interactiveMode(rootPath) {
|
|
@@ -3252,13 +3604,28 @@ async function interactiveMode(rootPath) {
|
|
|
3252
3604
|
rootPath,
|
|
3253
3605
|
excludePatterns,
|
|
3254
3606
|
followSymlinks: false
|
|
3255
|
-
})
|
|
3607
|
+
}, (_progress, found) => {
|
|
3608
|
+
if (found > 0) {
|
|
3609
|
+
s.message(`Scanning... found ${found} node_modules`);
|
|
3610
|
+
}
|
|
3611
|
+
}, true);
|
|
3256
3612
|
s.stop(`Found ${result.nodeModules.length} node_modules directories`);
|
|
3257
3613
|
if (result.nodeModules.length === 0) {
|
|
3258
3614
|
ge(import_picocolors2.default.yellow("No node_modules found."));
|
|
3259
3615
|
return;
|
|
3260
3616
|
}
|
|
3261
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");
|
|
3262
3629
|
const stats = calculateStatistics(items);
|
|
3263
3630
|
console.log(`
|
|
3264
3631
|
${import_picocolors2.default.gray("Total:")} ${import_picocolors2.default.white(stats.totalSizeFormatted)} across ${import_picocolors2.default.white(String(stats.totalProjects))} projects`);
|
|
@@ -3417,7 +3784,7 @@ Dry run - no files deleted.`));
|
|
|
3417
3784
|
}
|
|
3418
3785
|
}
|
|
3419
3786
|
var program2 = new Command;
|
|
3420
|
-
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) => {
|
|
3421
3788
|
if (options.scan) {
|
|
3422
3789
|
await quickScanMode(path, options.json);
|
|
3423
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,89 +338,112 @@ 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
|
-
const { path: currentPath, depth } = pathsToScan.shift();
|
|
200
|
-
if (visitedPaths.has(currentPath))
|
|
201
|
-
continue;
|
|
202
|
-
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
203
|
-
continue;
|
|
204
|
-
if (shouldExcludePath(currentPath, options.excludePatterns))
|
|
205
|
-
continue;
|
|
206
|
-
visitedPaths.add(currentPath);
|
|
207
|
-
result.directoriesScanned++;
|
|
208
|
-
try {
|
|
209
|
-
const entries = await fs2.readdir(currentPath, { withFileTypes: true });
|
|
210
|
-
const hasNodeModules = entries.some((entry) => entry.isDirectory() && entry.name === "node_modules");
|
|
211
|
-
if (hasNodeModules) {
|
|
212
|
-
const nodeModulesPath = join2(currentPath, "node_modules");
|
|
213
|
-
const info = await analyzeNodeModules(nodeModulesPath, currentPath);
|
|
214
|
-
if (options.minSizeBytes && info.sizeBytes < options.minSizeBytes) {} else if (options.olderThanDays && getAgeInDays2(info.lastModified) < options.olderThanDays) {} else {
|
|
215
|
-
result.nodeModules.push(info);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
for (const entry of entries) {
|
|
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
|
-
}
|
|
226
|
-
}
|
|
227
|
-
} catch (error) {
|
|
228
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
229
|
-
result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);
|
|
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);
|
|
230
355
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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);
|
|
375
|
+
}
|
|
235
376
|
}
|
|
377
|
+
return null;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
236
380
|
}
|
|
237
|
-
|
|
238
|
-
|
|
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 };
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
return null;
|
|
239
398
|
}
|
|
240
|
-
return result;
|
|
241
399
|
}
|
|
242
|
-
async function
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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);
|
|
407
|
+
}
|
|
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
|
+
}
|
|
250
417
|
return {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
sizeBytes: totalSize,
|
|
256
|
-
sizeFormatted: formatBytes(totalSize),
|
|
257
|
-
packageCount,
|
|
258
|
-
totalPackageCount,
|
|
259
|
-
lastModified: stats.mtime,
|
|
260
|
-
lastModifiedFormatted: formatRelativeTime(stats.mtime),
|
|
261
|
-
selected: false,
|
|
262
|
-
isFavorite: false,
|
|
263
|
-
ageCategory,
|
|
264
|
-
sizeCategory
|
|
418
|
+
bytes: 0,
|
|
419
|
+
packageCount: 0,
|
|
420
|
+
totalPackageCount: 0,
|
|
421
|
+
isNative: false
|
|
265
422
|
};
|
|
266
423
|
}
|
|
267
|
-
|
|
424
|
+
|
|
425
|
+
// src/scanner.ts
|
|
426
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
427
|
+
var SIZE_CALCULATION_CONCURRENCY = 3;
|
|
428
|
+
async function findRepoRoot(startPath) {
|
|
429
|
+
let currentPath = startPath;
|
|
430
|
+
const root = process.platform === "win32" ? "C:\\" : "/";
|
|
431
|
+
while (currentPath !== root && currentPath !== dirname(currentPath)) {
|
|
432
|
+
const gitPath = join2(currentPath, ".git");
|
|
433
|
+
try {
|
|
434
|
+
if (await fileExists(gitPath)) {
|
|
435
|
+
return currentPath;
|
|
436
|
+
}
|
|
437
|
+
} catch {}
|
|
438
|
+
currentPath = dirname(currentPath);
|
|
439
|
+
}
|
|
440
|
+
return startPath;
|
|
441
|
+
}
|
|
442
|
+
function getAgeInDays2(date) {
|
|
443
|
+
const now = new Date;
|
|
444
|
+
return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
445
|
+
}
|
|
446
|
+
async function calculateDirectorySizeFallback(dirPath) {
|
|
268
447
|
let totalSize = 0;
|
|
269
448
|
let packageCount = 0;
|
|
270
449
|
let totalPackageCount = 0;
|
|
@@ -301,7 +480,7 @@ async function calculateDirectorySize(dirPath) {
|
|
|
301
480
|
pathsToProcess.push(entryPath);
|
|
302
481
|
}
|
|
303
482
|
} catch {}
|
|
304
|
-
}
|
|
483
|
+
}
|
|
305
484
|
} catch {}
|
|
306
485
|
if (currentPath === dirPath) {
|
|
307
486
|
isTopLevel = false;
|
|
@@ -309,9 +488,181 @@ async function calculateDirectorySize(dirPath) {
|
|
|
309
488
|
}
|
|
310
489
|
return { totalSize, packageCount, totalPackageCount };
|
|
311
490
|
}
|
|
312
|
-
function
|
|
313
|
-
const
|
|
314
|
-
|
|
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);
|
|
315
666
|
}
|
|
316
667
|
async function loadIgnorePatterns() {
|
|
317
668
|
const patterns = [
|
|
@@ -383,10 +734,12 @@ async function quickScan(rootPath) {
|
|
|
383
734
|
const projectPath = currentPath;
|
|
384
735
|
const nodeModulesPath = join2(currentPath, "node_modules");
|
|
385
736
|
const packageJson = await readPackageJson(projectPath);
|
|
737
|
+
const repoPath = await findRepoRoot(projectPath);
|
|
386
738
|
results.push({
|
|
387
739
|
path: nodeModulesPath,
|
|
388
740
|
projectPath,
|
|
389
|
-
projectName: packageJson?.name || basename(projectPath)
|
|
741
|
+
projectName: packageJson?.name || basename(projectPath),
|
|
742
|
+
repoPath
|
|
390
743
|
});
|
|
391
744
|
}
|
|
392
745
|
for (const entry of entries) {
|
|
@@ -558,6 +911,7 @@ function invertSelection(nodeModules) {
|
|
|
558
911
|
// src/index.ts
|
|
559
912
|
var VERSION = "1.0.0";
|
|
560
913
|
export {
|
|
914
|
+
updateNodeModulesSize,
|
|
561
915
|
toggleSelection,
|
|
562
916
|
sortNodeModules,
|
|
563
917
|
selectBySize,
|
|
@@ -580,6 +934,7 @@ export {
|
|
|
580
934
|
filterNodeModules,
|
|
581
935
|
deleteSelectedNodeModules,
|
|
582
936
|
calculateStatistics,
|
|
937
|
+
calculatePendingSizes,
|
|
583
938
|
analyzeNodeModules,
|
|
584
939
|
VERSION,
|
|
585
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
|
},
|