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.
Files changed (3) hide show
  1. package/dist/cli.js +439 -88
  2. package/dist/index.js +421 -84
  3. 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/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
- }
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
- processedCount++;
2998
- if (onProgress) {
2999
- const progress = Math.min(100, Math.round(processedCount / totalEstimate * 100));
3000
- onProgress(progress);
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
- if (onProgress) {
3004
- onProgress(100);
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
- return result;
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
- 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
- };
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 calculateDirectorySize(dirPath) {
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
- } else if (stats.isSymbolicLink()) {}
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 getAgeInDays2(date) {
3095
- const now = new Date;
3096
- return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
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 size = import_picocolors2.default.cyan(item.sizeFormatted.padStart(8));
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("[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) => {
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/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
- }
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
- processedCount++;
232
- if (onProgress) {
233
- const progress = Math.min(100, Math.round(processedCount / totalEstimate * 100));
234
- onProgress(progress);
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
- if (onProgress) {
238
- onProgress(100);
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
- return result;
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
- 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
- };
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 calculateDirectorySize(dirPath) {
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
- } else if (stats.isSymbolicLink()) {}
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 getAgeInDays2(date) {
329
- const now = new Date;
330
- return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
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.8",
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
  },