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 CHANGED
@@ -17,7 +17,7 @@ npm install -g oh-my-node-modules
17
17
  ### Interactive mode
18
18
 
19
19
  ```bash
20
- onm # Start in current directory
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/scanner.ts
2952
- async function scanForNodeModules(options, onProgress) {
2953
- const result = {
2954
- nodeModules: [],
2955
- directoriesScanned: 0,
2956
- errors: []
2957
- };
2958
- const visitedPaths = new Set;
2959
- const pathsToScan = [
2960
- { path: options.rootPath, depth: 0 }
2961
- ];
2962
- let processedCount = 0;
2963
- let totalEstimate = 1;
2964
- while (pathsToScan.length > 0) {
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
- }
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
- processedCount++;
2998
- if (onProgress) {
2999
- const progress = Math.min(100, Math.round(processedCount / totalEstimate * 100));
3000
- onProgress(progress);
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
- if (onProgress) {
3004
- onProgress(100);
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
- return result;
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
- async function analyzeNodeModules(nodeModulesPath, projectPath) {
3023
- const stats = await fs2.stat(nodeModulesPath);
3024
- const { totalSize, packageCount, totalPackageCount } = await calculateDirectorySize(nodeModulesPath);
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 calculateDirectorySize(dirPath) {
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
- } else if (stats.isSymbolicLink()) {}
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 getAgeInDays2(date) {
3095
- const now = new Date;
3096
- return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
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 size = import_picocolors2.default.cyan(item.sizeFormatted.padStart(8));
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("[path]", "Directory to scan", process.cwd()).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) => {
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/scanner.ts
186
- async function scanForNodeModules(options, onProgress) {
187
- const result = {
188
- nodeModules: [],
189
- directoriesScanned: 0,
190
- errors: []
191
- };
192
- const visitedPaths = new Set;
193
- const pathsToScan = [
194
- { path: options.rootPath, depth: 0 }
195
- ];
196
- let processedCount = 0;
197
- let totalEstimate = 1;
198
- while (pathsToScan.length > 0) {
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
- }
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
- processedCount++;
232
- if (onProgress) {
233
- const progress = Math.min(100, Math.round(processedCount / totalEstimate * 100));
234
- onProgress(progress);
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
- if (onProgress) {
238
- onProgress(100);
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
- return result;
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
- async function analyzeNodeModules(nodeModulesPath, projectPath) {
257
- const stats = await fs2.stat(nodeModulesPath);
258
- const { totalSize, packageCount, totalPackageCount } = await calculateDirectorySize(nodeModulesPath);
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 calculateDirectorySize(dirPath) {
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
- } else if (stats.isSymbolicLink()) {}
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 getAgeInDays2(date) {
329
- const now = new Date;
330
- return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
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.2.8",
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
  },