next-prune 1.0.2 → 1.1.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/dist/app.js +157 -74
- package/dist/asset-scanner.js +92 -0
- package/dist/cli.js +33 -5
- package/dist/config.js +45 -0
- package/dist/scanner.js +149 -76
- package/package.json +95 -98
package/dist/app.js
CHANGED
|
@@ -3,7 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
5
5
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
6
|
-
import {
|
|
6
|
+
import { scanArtifacts, getArtifactStats, FRAMES, human, timeAgo } from './scanner.js';
|
|
7
|
+
import { findUnusedAssets } from './asset-scanner.js';
|
|
7
8
|
|
|
8
9
|
// Scanner utilities are imported from ./scanner.js
|
|
9
10
|
|
|
@@ -38,11 +39,11 @@ function useTerminalRows() {
|
|
|
38
39
|
// Greedy pack segments into up to maxLines based on terminal width
|
|
39
40
|
function packSegments(segments, cols, maxLines = 3) {
|
|
40
41
|
const sep = ' • ';
|
|
41
|
-
const prefixLen = 3; //
|
|
42
|
-
const max = Math.max(24, (cols || 80) - 6); //
|
|
42
|
+
const prefixLen = 3; // Approx width of '🎮 '
|
|
43
|
+
const max = Math.max(24, (cols || 80) - 6); // Leave room for borders/padding
|
|
43
44
|
const lines = [];
|
|
44
45
|
let cur = [];
|
|
45
|
-
let curLen = prefixLen; //
|
|
46
|
+
let curLen = prefixLen; // First line has a small prefix
|
|
46
47
|
const pushLine = () => {
|
|
47
48
|
if (cur.length > 0) lines.push(cur);
|
|
48
49
|
cur = [];
|
|
@@ -75,7 +76,7 @@ function packLabeledSegments(segments, cols, prefixLen = 0, maxLines = 3) {
|
|
|
75
76
|
curLen = 0;
|
|
76
77
|
};
|
|
77
78
|
for (const seg of segments) {
|
|
78
|
-
const segLen = seg.label.length + 1 + String(seg.value).length; //
|
|
79
|
+
const segLen = seg.label.length + 1 + String(seg.value).length; // Space before value
|
|
79
80
|
const addLen = (cur.length === 0 ? 0 : sepLen) + segLen;
|
|
80
81
|
if (cur.length > 0 && curLen + addLen > max) {
|
|
81
82
|
pushLine();
|
|
@@ -95,16 +96,22 @@ function truncateMiddle(text, max) {
|
|
|
95
96
|
const tail = Math.floor((max - 1) / 2);
|
|
96
97
|
return text.slice(0, head) + '…' + text.slice(text.length - tail);
|
|
97
98
|
}
|
|
99
|
+
const DEFAULT_CONFIG = {
|
|
100
|
+
alwaysDelete: [],
|
|
101
|
+
neverDelete: [],
|
|
102
|
+
checkUnusedAssets: false
|
|
103
|
+
};
|
|
98
104
|
export default function App({
|
|
99
105
|
cwd = process.cwd(),
|
|
100
106
|
dryRun = false,
|
|
101
107
|
confirmImmediately = false,
|
|
102
|
-
testMode = false
|
|
108
|
+
testMode = false,
|
|
109
|
+
config = DEFAULT_CONFIG
|
|
103
110
|
}) {
|
|
104
111
|
const {
|
|
105
112
|
exit
|
|
106
113
|
} = useApp();
|
|
107
|
-
const [items, setItems] = useState([]); // {path, size, status}
|
|
114
|
+
const [items, setItems] = useState([]); // {path, size, mtime, fileCount, status}
|
|
108
115
|
const [loading, setLoading] = useState(!testMode);
|
|
109
116
|
const [selected, setSelected] = useState(new Set());
|
|
110
117
|
const [index, setIndex] = useState(0);
|
|
@@ -182,7 +189,7 @@ export default function App({
|
|
|
182
189
|
const scanSegments = useMemo(() => {
|
|
183
190
|
const segs = [{
|
|
184
191
|
label: 'Found:',
|
|
185
|
-
value: `${foundCount}
|
|
192
|
+
value: `${foundCount} items`
|
|
186
193
|
}, {
|
|
187
194
|
label: 'Total Size:',
|
|
188
195
|
value: human(totalSize)
|
|
@@ -203,13 +210,28 @@ export default function App({
|
|
|
203
210
|
setLoading(true);
|
|
204
211
|
setError('');
|
|
205
212
|
try {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
// Updated to use scanArtifacts which does stats internally
|
|
214
|
+
let nextItems = await scanArtifacts(cwd);
|
|
215
|
+
if (config.checkUnusedAssets) {
|
|
216
|
+
const assetPaths = await findUnusedAssets(cwd);
|
|
217
|
+
const assetStats = await Promise.all(assetPaths.map(p => getArtifactStats(p)));
|
|
218
|
+
const assetItems = assetPaths.map((p, i) => ({
|
|
219
|
+
path: p,
|
|
220
|
+
...assetStats[i],
|
|
221
|
+
type: 'asset' // Marker for UI
|
|
222
|
+
}));
|
|
223
|
+
nextItems = [...nextItems, ...assetItems];
|
|
224
|
+
// Re-sort by size
|
|
225
|
+
nextItems.sort((a, b) => b.size - a.size);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Filter out neverDelete items
|
|
229
|
+
if (config.neverDelete?.length > 0) {
|
|
230
|
+
nextItems = nextItems.filter(it => {
|
|
231
|
+
const rel = path.relative(cwd, it.path);
|
|
232
|
+
return !config.neverDelete.some(pattern => rel === pattern || rel.startsWith(pattern + path.sep));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
213
235
|
setItems(nextItems);
|
|
214
236
|
// Reset UI focus/help after a fresh scan
|
|
215
237
|
setIndex(0);
|
|
@@ -234,7 +256,7 @@ export default function App({
|
|
|
234
256
|
...it,
|
|
235
257
|
status: 'dry-run'
|
|
236
258
|
} : it));
|
|
237
|
-
setError(`✅ Dry-run: would delete ${selectedIndices.length}
|
|
259
|
+
setError(`✅ Dry-run: would delete ${selectedIndices.length} items (${human(selectedSize)})`);
|
|
238
260
|
setSelected(new Set());
|
|
239
261
|
setConfirm(false);
|
|
240
262
|
return;
|
|
@@ -269,6 +291,7 @@ export default function App({
|
|
|
269
291
|
setError(`Failed to delete: ${p} (${error_?.message ?? error_})`);
|
|
270
292
|
}
|
|
271
293
|
}
|
|
294
|
+
|
|
272
295
|
// Mark successful deletions
|
|
273
296
|
if (successes.size > 0) {
|
|
274
297
|
setItems(prev => prev.map((it, i) => successes.has(i) ? {
|
|
@@ -276,7 +299,7 @@ export default function App({
|
|
|
276
299
|
status: 'deleted',
|
|
277
300
|
size: 0
|
|
278
301
|
} : it));
|
|
279
|
-
setError(`✅ Deleted ${successes.size}
|
|
302
|
+
setError(`✅ Deleted ${successes.size} items (freed ${human(freed)})`);
|
|
280
303
|
}
|
|
281
304
|
} finally {
|
|
282
305
|
setSelected(new Set());
|
|
@@ -298,6 +321,7 @@ export default function App({
|
|
|
298
321
|
}
|
|
299
322
|
return;
|
|
300
323
|
}
|
|
324
|
+
|
|
301
325
|
// Global Quit
|
|
302
326
|
if (key.escape || input?.toLowerCase() === 'q' || input === 'c' && key.ctrl) {
|
|
303
327
|
exit();
|
|
@@ -310,30 +334,44 @@ export default function App({
|
|
|
310
334
|
} else if (key.downArrow) {
|
|
311
335
|
// Navigate Down
|
|
312
336
|
setIndex(i => Math.min(Math.max(0, items.length - 1), i + 1));
|
|
313
|
-
} else
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
+
} else switch (input) {
|
|
338
|
+
case ' ':
|
|
339
|
+
{
|
|
340
|
+
// Selection Toggle
|
|
341
|
+
setSelected(cur => {
|
|
342
|
+
const next = new Set(cur);
|
|
343
|
+
if (next.has(index)) next.delete(index);else if (items[index]?.status !== 'deleted') next.add(index);
|
|
344
|
+
return next;
|
|
345
|
+
});
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case 'a':
|
|
349
|
+
{
|
|
350
|
+
// Select All
|
|
351
|
+
setSelected(new Set(items.map((_, i) => i).filter(i => items[i]?.status !== 'deleted')));
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case 'c':
|
|
355
|
+
{
|
|
356
|
+
// Clear Selection
|
|
357
|
+
setSelected(new Set());
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
default:
|
|
361
|
+
{
|
|
362
|
+
if (input?.toLowerCase() === 'd' || key.return) {
|
|
363
|
+
// Open confirm; if none selected, select the focused item if allowed
|
|
364
|
+
if (selected.size > 0) {
|
|
365
|
+
setConfirm(true);
|
|
366
|
+
} else if (items[index]?.status !== 'deleted') {
|
|
367
|
+
setSelected(new Set([index]));
|
|
368
|
+
setConfirm(true);
|
|
369
|
+
}
|
|
370
|
+
} else if (input?.toLowerCase() === 'r' && !loading) {
|
|
371
|
+
// Rescan
|
|
372
|
+
doScan();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
337
375
|
}
|
|
338
376
|
});
|
|
339
377
|
|
|
@@ -343,16 +381,16 @@ export default function App({
|
|
|
343
381
|
viewStart,
|
|
344
382
|
viewEnd
|
|
345
383
|
} = useMemo(() => {
|
|
346
|
-
const rootPad = 2; //
|
|
347
|
-
const titleHeight = 5; //
|
|
348
|
-
const scanHeight = 5 + packedScan.length; //
|
|
349
|
-
const errorHeight = error ? 5 : 0; //
|
|
350
|
-
const footerHeight = showHelp ? 10 : 4 + packedLines.length; //
|
|
351
|
-
const confirmHeight = confirm ? 7 : 0; //
|
|
384
|
+
const rootPad = 2; // Root <Box padding={1}> adds 2 rows total
|
|
385
|
+
const titleHeight = 5; // Round border + padding + 1 line
|
|
386
|
+
const scanHeight = 5 + packedScan.length; // Single border + padding + header + lines
|
|
387
|
+
const errorHeight = error ? 5 : 0; // Approx single-line error box
|
|
388
|
+
const footerHeight = showHelp ? 10 : 4 + packedLines.length; // Help ~6 lines + borders/padding => 10; else compact footer
|
|
389
|
+
const confirmHeight = confirm ? 7 : 0; // Confirm box ~3 lines + borders/padding
|
|
352
390
|
const used = rootPad + titleHeight + scanHeight + errorHeight + footerHeight + confirmHeight;
|
|
353
391
|
const totalRows = rows || 24;
|
|
354
|
-
const boxHeight = Math.max(7, totalRows - used); //
|
|
355
|
-
const inner = Math.max(3, boxHeight - 4); //
|
|
392
|
+
const boxHeight = Math.max(7, totalRows - used); // Include list borders+padding
|
|
393
|
+
const inner = Math.max(3, boxHeight - 4); // Subtract list border+padding
|
|
356
394
|
|
|
357
395
|
// Center the focused item within the visible window when possible
|
|
358
396
|
const half = Math.floor(inner / 2);
|
|
@@ -374,12 +412,31 @@ export default function App({
|
|
|
374
412
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
375
413
|
}, [cwd, testMode]);
|
|
376
414
|
useEffect(() => {
|
|
377
|
-
if (!confirmImmediately) return;
|
|
378
|
-
if (testMode) return;
|
|
379
415
|
if (items.length === 0) return;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
416
|
+
if (testMode) return;
|
|
417
|
+
|
|
418
|
+
// 1. If confirmImmediately is set, select all and confirm
|
|
419
|
+
if (confirmImmediately) {
|
|
420
|
+
setSelected(new Set(items.map((_, i) => i)));
|
|
421
|
+
setConfirm(true);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 2. Otherwise, check config.alwaysDelete
|
|
426
|
+
if (config.alwaysDelete?.length > 0) {
|
|
427
|
+
const preSelected = new Set();
|
|
428
|
+
for (const [i, it] of items.entries()) {
|
|
429
|
+
const rel = path.relative(cwd, it.path);
|
|
430
|
+
const shouldSelect = config.alwaysDelete.some(pattern => rel === pattern || rel.startsWith(pattern + path.sep));
|
|
431
|
+
if (shouldSelect && it.status !== 'deleted') {
|
|
432
|
+
preSelected.add(i);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (preSelected.size > 0) {
|
|
436
|
+
setSelected(preSelected);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}, [confirmImmediately, items, testMode, config.alwaysDelete, cwd]);
|
|
383
440
|
|
|
384
441
|
// Auto-deselect deleted items
|
|
385
442
|
useEffect(() => {
|
|
@@ -426,7 +483,7 @@ export default function App({
|
|
|
426
483
|
key: si
|
|
427
484
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
428
485
|
color: seg.label === 'Status:' ? 'blue' : 'yellow'
|
|
429
|
-
}, seg.label), /*#__PURE__*/React.createElement(Text, null, " ", seg.value), si < line.length - 1 && /*#__PURE__*/React.createElement(Text, null, " \u2022 ")))))))), error
|
|
486
|
+
}, seg.label), /*#__PURE__*/React.createElement(Text, null, " ", seg.value), si < line.length - 1 && /*#__PURE__*/React.createElement(Text, null, " \u2022 ")))))))), error ? /*#__PURE__*/React.createElement(Box, {
|
|
430
487
|
borderStyle: "single",
|
|
431
488
|
borderColor: error.startsWith('✅') ? 'green' : 'red',
|
|
432
489
|
padding: 1,
|
|
@@ -439,7 +496,7 @@ export default function App({
|
|
|
439
496
|
bold: true
|
|
440
497
|
}, "\u274C Error:", ' '), /*#__PURE__*/React.createElement(Text, {
|
|
441
498
|
color: "red"
|
|
442
|
-
}, error))), /*#__PURE__*/React.createElement(Box, {
|
|
499
|
+
}, error))) : null, /*#__PURE__*/React.createElement(Box, {
|
|
443
500
|
flexDirection: "column",
|
|
444
501
|
borderStyle: "single",
|
|
445
502
|
padding: 1,
|
|
@@ -458,27 +515,48 @@ export default function App({
|
|
|
458
515
|
const prefix = isFocus ? '>' : ' ';
|
|
459
516
|
const mark = isSel ? '[x]' : '[ ]';
|
|
460
517
|
const sizeText = it.size === undefined || it.size === null ? '…' : human(it.size);
|
|
518
|
+
const timeText = timeAgo(it.mtime);
|
|
461
519
|
let statusColor = undefined;
|
|
462
520
|
let statusText = '';
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
521
|
+
switch (it.status) {
|
|
522
|
+
case 'deleted':
|
|
523
|
+
{
|
|
524
|
+
// No inline indicator for deleted items; styling conveys state
|
|
525
|
+
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
case 'error':
|
|
529
|
+
{
|
|
530
|
+
statusColor = 'red';
|
|
531
|
+
statusText = ' ❌ error';
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
case 'dry-run':
|
|
535
|
+
{
|
|
536
|
+
statusColor = 'yellow';
|
|
537
|
+
statusText = ' 🔍 dry-run';
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case 'deleting':
|
|
541
|
+
{
|
|
542
|
+
statusColor = 'blue';
|
|
543
|
+
statusText = ' 🗑️ deleting...';
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
// No default
|
|
474
547
|
}
|
|
475
548
|
|
|
476
549
|
// Ensure single-line rendering to avoid terminal scroll jumps
|
|
477
550
|
const containerWidth = Math.max(24, (cols || 80) - 6);
|
|
478
551
|
const leftPart = `${prefix} ${mark} ${sizeText.padStart(7)} `;
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
552
|
+
const metaPart = ` ${timeText.padEnd(8)} `;
|
|
553
|
+
|
|
554
|
+
// Add icon based on type/directory
|
|
555
|
+
// const icon = it.isDirectory === false ? '📄' : '📁';
|
|
556
|
+
|
|
557
|
+
const reserved = leftPart.length + metaPart.length + (statusText ? statusText.length : 0);
|
|
558
|
+
// Adjusted maxRel calculation to account for icon and warning
|
|
559
|
+
const maxRel = Math.max(3, containerWidth - reserved - 4);
|
|
482
560
|
return /*#__PURE__*/React.createElement(Box, {
|
|
483
561
|
key: it.path
|
|
484
562
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
@@ -486,9 +564,14 @@ export default function App({
|
|
|
486
564
|
backgroundColor: isFocus && it.status !== 'deleted' ? 'blue' : undefined,
|
|
487
565
|
dimColor: it.status === 'deleted',
|
|
488
566
|
strikethrough: it.status === 'deleted'
|
|
489
|
-
}, leftPart,
|
|
567
|
+
}, leftPart, /*#__PURE__*/React.createElement(Text, {
|
|
568
|
+
dimColor: it.status === 'deleted',
|
|
569
|
+
color: it.status === 'deleted' ? 'gray' : 'yellow'
|
|
570
|
+
}, metaPart), it.type === 'asset' ? /*#__PURE__*/React.createElement(Text, {
|
|
571
|
+
color: "yellow"
|
|
572
|
+
}, "\u26A0\uFE0F ") : null, it.isDirectory === false ? /*#__PURE__*/React.createElement(Text, null, "\uD83D\uDCC4 ") : /*#__PURE__*/React.createElement(Text, null, "\uD83D\uDCC1 "), truncateMiddle(rel, maxRel)), statusText ? /*#__PURE__*/React.createElement(Text, {
|
|
490
573
|
color: statusColor
|
|
491
|
-
}, statusText));
|
|
574
|
+
}, statusText) : null);
|
|
492
575
|
})), /*#__PURE__*/React.createElement(Box, {
|
|
493
576
|
borderStyle: "single",
|
|
494
577
|
borderColor: "gray",
|
|
@@ -568,7 +651,7 @@ export default function App({
|
|
|
568
651
|
dimColor: true
|
|
569
652
|
}, " ", seg.label), si < line.length - 1 && /*#__PURE__*/React.createElement(Text, {
|
|
570
653
|
dimColor: true
|
|
571
|
-
}, " \u2022 "))))))), confirm
|
|
654
|
+
}, " \u2022 "))))))), confirm ? /*#__PURE__*/React.createElement(Box, {
|
|
572
655
|
borderStyle: "double",
|
|
573
656
|
borderColor: "yellow",
|
|
574
657
|
padding: 1,
|
|
@@ -589,5 +672,5 @@ export default function App({
|
|
|
589
672
|
bold: true
|
|
590
673
|
}, "Esc"), /*#__PURE__*/React.createElement(Text, {
|
|
591
674
|
color: "gray"
|
|
592
|
-
}, " cancel"))));
|
|
675
|
+
}, " cancel"))) : null);
|
|
593
676
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', '.ico', '.bmp']);
|
|
4
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.css', '.scss', '.sass', '.less', '.html', '.md', '.mdx']);
|
|
5
|
+
async function* walkFiles(dir, extensions) {
|
|
6
|
+
let entries = [];
|
|
7
|
+
try {
|
|
8
|
+
entries = await fs.readdir(dir, {
|
|
9
|
+
withFileTypes: true
|
|
10
|
+
});
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const full = path.join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
yield* walkFiles(full, extensions);
|
|
18
|
+
} else if (entry.isFile()) {
|
|
19
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
20
|
+
if (!extensions || extensions.has(ext)) {
|
|
21
|
+
yield full;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export const findUnusedAssets = async cwd => {
|
|
27
|
+
const publicDir = path.join(cwd, 'public');
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(publicDir);
|
|
30
|
+
} catch {
|
|
31
|
+
return []; // No public dir
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 1. Gather all assets in public/
|
|
35
|
+
const assets = [];
|
|
36
|
+
for await (const p of walkFiles(publicDir, IMAGE_EXTENSIONS)) {
|
|
37
|
+
assets.push(p);
|
|
38
|
+
}
|
|
39
|
+
if (assets.length === 0) return [];
|
|
40
|
+
|
|
41
|
+
// 2. Gather all source files
|
|
42
|
+
const sourceFiles = [];
|
|
43
|
+
const srcDirs = ['src', 'app', 'pages', 'components', 'lib', 'utils', 'hooks'];
|
|
44
|
+
// Also check root files
|
|
45
|
+
const rootFiles = await fs.readdir(cwd);
|
|
46
|
+
for (const f of rootFiles) {
|
|
47
|
+
if (SOURCE_EXTENSIONS.has(path.extname(f))) {
|
|
48
|
+
sourceFiles.push(path.join(cwd, f));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const dir of srcDirs) {
|
|
52
|
+
const fullDir = path.join(cwd, dir);
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(fullDir);
|
|
55
|
+
for await (const p of walkFiles(fullDir, SOURCE_EXTENSIONS)) {
|
|
56
|
+
sourceFiles.push(p);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore missing dirs
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3. Read source files content
|
|
64
|
+
const contents = await Promise.all(sourceFiles.map(async f => {
|
|
65
|
+
try {
|
|
66
|
+
return await fs.readFile(f, 'utf8');
|
|
67
|
+
} catch {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
const fullSource = contents.join('\n');
|
|
72
|
+
|
|
73
|
+
// 4. Check for usage
|
|
74
|
+
const unused = [];
|
|
75
|
+
for (const asset of assets) {
|
|
76
|
+
const filename = path.basename(asset);
|
|
77
|
+
const relPath = path.relative(publicDir, asset); // e.g. "images/logo.png"
|
|
78
|
+
|
|
79
|
+
// Naive check: does the filename appear?
|
|
80
|
+
// Or the relative path?
|
|
81
|
+
// We check both "logo.png" and "images/logo.png"
|
|
82
|
+
|
|
83
|
+
const nameMatch = fullSource.includes(filename);
|
|
84
|
+
// For relative path, we need to be careful about slashes.
|
|
85
|
+
// In code it might be "/images/logo.png"
|
|
86
|
+
const relMatch = fullSource.includes(relPath) || fullSource.includes('/' + relPath);
|
|
87
|
+
if (!nameMatch && !relMatch) {
|
|
88
|
+
unused.push(asset);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return unused;
|
|
92
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import meow from 'meow';
|
|
5
|
-
import {
|
|
5
|
+
import { scanArtifacts, getArtifactStats, human, timeAgo } from './scanner.js';
|
|
6
|
+
import { findUnusedAssets } from './asset-scanner.js';
|
|
7
|
+
import { loadConfig } from './config.js';
|
|
6
8
|
const cli = meow(`
|
|
7
9
|
Usage
|
|
8
10
|
$ next-prune [options]
|
|
@@ -53,8 +55,28 @@ const props = {
|
|
|
53
55
|
cwd: cli.flags.cwd ?? process.cwd()
|
|
54
56
|
};
|
|
55
57
|
async function main() {
|
|
58
|
+
const config = await loadConfig(props.cwd);
|
|
56
59
|
if (cli.flags.list || cli.flags.json) {
|
|
57
|
-
|
|
60
|
+
let items = await scanArtifacts(props.cwd);
|
|
61
|
+
if (config.checkUnusedAssets) {
|
|
62
|
+
const assetPaths = await findUnusedAssets(props.cwd);
|
|
63
|
+
const assetStats = await Promise.all(assetPaths.map(p => getArtifactStats(p)));
|
|
64
|
+
const assetItems = assetPaths.map((p, i) => ({
|
|
65
|
+
path: p,
|
|
66
|
+
...assetStats[i],
|
|
67
|
+
type: 'asset'
|
|
68
|
+
}));
|
|
69
|
+
items = [...items, ...assetItems];
|
|
70
|
+
items.sort((a, b) => b.size - a.size);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Filter out neverDelete items
|
|
74
|
+
if (config.neverDelete.length > 0) {
|
|
75
|
+
items = items.filter(it => {
|
|
76
|
+
const rel = path.relative(props.cwd, it.path);
|
|
77
|
+
return !config.neverDelete.some(pattern => rel === pattern || rel.startsWith(pattern + path.sep));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
58
80
|
if (cli.flags.json) {
|
|
59
81
|
process.stdout.write(JSON.stringify(items, null, 2) + '\n');
|
|
60
82
|
return;
|
|
@@ -63,9 +85,12 @@ async function main() {
|
|
|
63
85
|
for (const it of items) total += typeof it.size === 'number' ? it.size : 0;
|
|
64
86
|
for (const it of items) {
|
|
65
87
|
const rel = path.relative(props.cwd, it.path) || '.';
|
|
66
|
-
|
|
88
|
+
const time = it.mtime ? `(${timeAgo(it.mtime)})` : '';
|
|
89
|
+
const type = it.type === 'asset' ? '⚠️ ' : '';
|
|
90
|
+
const icon = it.isDirectory === false ? '📄' : '📁';
|
|
91
|
+
process.stdout.write(`${human(it.size).padStart(6)} ${time.padEnd(10)} ${type}${icon} ${rel}\n`);
|
|
67
92
|
}
|
|
68
|
-
process.stdout.write(`\nTotal: ${human(total)} in ${items.length}
|
|
93
|
+
process.stdout.write(`\nTotal: ${human(total)} in ${items.length} items\n`);
|
|
69
94
|
return;
|
|
70
95
|
}
|
|
71
96
|
const reactModule = await import('react');
|
|
@@ -77,7 +102,10 @@ async function main() {
|
|
|
77
102
|
const {
|
|
78
103
|
default: App
|
|
79
104
|
} = await import('./app.js');
|
|
80
|
-
render(React.createElement(App,
|
|
105
|
+
render(React.createElement(App, {
|
|
106
|
+
...props,
|
|
107
|
+
config
|
|
108
|
+
}));
|
|
81
109
|
}
|
|
82
110
|
|
|
83
111
|
// eslint-disable-next-line unicorn/prefer-top-level-await
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
export const DEFAULT_CONFIG = {
|
|
4
|
+
alwaysDelete: [],
|
|
5
|
+
neverDelete: [],
|
|
6
|
+
checkUnusedAssets: false
|
|
7
|
+
};
|
|
8
|
+
export const loadConfig = async cwd => {
|
|
9
|
+
let config = {
|
|
10
|
+
...DEFAULT_CONFIG
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// 1. Check package.json
|
|
14
|
+
try {
|
|
15
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
16
|
+
const pkgStr = await fs.readFile(pkgPath, 'utf8');
|
|
17
|
+
const pkg = JSON.parse(pkgStr);
|
|
18
|
+
if (pkg['next-prune']) {
|
|
19
|
+
config = {
|
|
20
|
+
...config,
|
|
21
|
+
...pkg['next-prune']
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Ignore if package.json not found or invalid
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Check .next-prunerc.json (overrides package.json)
|
|
29
|
+
try {
|
|
30
|
+
const rcPath = path.join(cwd, '.next-prunerc.json');
|
|
31
|
+
const rcStr = await fs.readFile(rcPath, 'utf8');
|
|
32
|
+
const rc = JSON.parse(rcStr);
|
|
33
|
+
config = {
|
|
34
|
+
...config,
|
|
35
|
+
...rc
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Normalize arrays
|
|
42
|
+
if (!Array.isArray(config.alwaysDelete)) config.alwaysDelete = [];
|
|
43
|
+
if (!Array.isArray(config.neverDelete)) config.neverDelete = [];
|
|
44
|
+
return config;
|
|
45
|
+
};
|
package/dist/scanner.js
CHANGED
|
@@ -3,34 +3,68 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
export const FRAMES = Object.freeze(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']);
|
|
4
4
|
|
|
5
5
|
// Patterns for Next.js and related build artifacts/caches
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// Includes files (logs) and directories
|
|
7
|
+
const ARTIFACT_PATTERNS = ['.next',
|
|
8
|
+
// Next.js build output
|
|
8
9
|
'out',
|
|
9
|
-
// Next.js static export
|
|
10
|
+
// Next.js static export
|
|
10
11
|
'.vercel/output',
|
|
11
|
-
// Vercel
|
|
12
|
+
// Vercel output
|
|
12
13
|
'.turbo',
|
|
13
14
|
// Turborepo cache
|
|
14
15
|
'.vercel_build_output',
|
|
15
|
-
// Legacy Vercel
|
|
16
|
-
'node_modules/.cache/next'
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// Legacy Vercel
|
|
17
|
+
'node_modules/.cache/next',
|
|
18
|
+
// Next.js cache
|
|
19
|
+
'node_modules/.cache/turbopack',
|
|
20
|
+
// Turbopack cache
|
|
21
|
+
'coverage',
|
|
22
|
+
// Test coverage
|
|
23
|
+
'.swc',
|
|
24
|
+
// SWC cache
|
|
25
|
+
'.docusaurus',
|
|
26
|
+
// Docusaurus cache
|
|
27
|
+
'storybook-static',
|
|
28
|
+
// Storybook build
|
|
29
|
+
'npm-debug.log', 'yarn-error.log', 'pnpm-debug.log'];
|
|
30
|
+
|
|
31
|
+
// Directories to skip during recursive scan to avoid performance hits
|
|
32
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist', 'build',
|
|
33
|
+
// careful: some projects use build as output, but often it's source
|
|
34
|
+
'.next', '.turbo', '.vercel', 'coverage', '.swc', '.docusaurus', 'storybook-static']);
|
|
19
35
|
export const human = bytes => {
|
|
20
|
-
if (
|
|
36
|
+
if (bytes === 0) return '0 B';
|
|
37
|
+
if (!bytes) return '-';
|
|
21
38
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
22
39
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
23
40
|
const value = bytes / 1024 ** i;
|
|
24
41
|
return `${value.toFixed(value >= 10 || i === 0 ? 0 : 1)} ${units[i]}`;
|
|
25
42
|
};
|
|
26
|
-
export const
|
|
43
|
+
export const timeAgo = date => {
|
|
44
|
+
if (!date) return '';
|
|
45
|
+
const seconds = Math.floor((Date.now() - date) / 1000);
|
|
46
|
+
let interval = seconds / 31_536_000;
|
|
47
|
+
if (interval > 1) return Math.floor(interval) + 'y ago';
|
|
48
|
+
interval = seconds / 2_592_000;
|
|
49
|
+
if (interval > 1) return Math.floor(interval) + 'mo ago';
|
|
50
|
+
interval = seconds / 86_400;
|
|
51
|
+
if (interval > 1) return Math.floor(interval) + 'd ago';
|
|
52
|
+
interval = seconds / 3600;
|
|
53
|
+
if (interval > 1) return Math.floor(interval) + 'h ago';
|
|
54
|
+
interval = seconds / 60;
|
|
55
|
+
if (interval > 1) return Math.floor(interval) + 'm ago';
|
|
56
|
+
return Math.floor(seconds) + 's ago';
|
|
57
|
+
};
|
|
58
|
+
export const pathExists = async p => {
|
|
27
59
|
try {
|
|
28
|
-
|
|
29
|
-
return
|
|
60
|
+
await fs.stat(p);
|
|
61
|
+
return true;
|
|
30
62
|
} catch {
|
|
31
63
|
return false;
|
|
32
64
|
}
|
|
33
65
|
};
|
|
66
|
+
|
|
67
|
+
// Recursively walk directories, yielding subdirectories
|
|
34
68
|
export async function* walk(root) {
|
|
35
69
|
let entries = [];
|
|
36
70
|
try {
|
|
@@ -42,84 +76,123 @@ export async function* walk(root) {
|
|
|
42
76
|
}
|
|
43
77
|
for (const entry of entries) {
|
|
44
78
|
if (!entry.isDirectory()) continue;
|
|
79
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
45
80
|
const full = path.join(root, entry.name);
|
|
46
81
|
yield full;
|
|
47
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
48
82
|
yield* walk(full);
|
|
49
83
|
}
|
|
50
84
|
}
|
|
51
|
-
export const
|
|
52
|
-
const results =
|
|
85
|
+
export const findArtifacts = async cwd => {
|
|
86
|
+
const results = new Set();
|
|
53
87
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
// Helper to check a specific directory for patterns
|
|
89
|
+
const checkDir = async dir => {
|
|
90
|
+
for (const pattern of ARTIFACT_PATTERNS) {
|
|
91
|
+
// Handle deep patterns like node_modules/.cache/next separately
|
|
92
|
+
if (pattern.includes(path.sep) || pattern.includes('/')) continue;
|
|
93
|
+
const fullPath = path.join(dir, pattern);
|
|
94
|
+
if (await pathExists(fullPath)) {
|
|
95
|
+
results.add(fullPath);
|
|
96
|
+
}
|
|
59
97
|
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Check root
|
|
101
|
+
await checkDir(cwd);
|
|
102
|
+
|
|
103
|
+
// Check specific nested known locations in root
|
|
104
|
+
const nestedCandidates = [path.join(cwd, '.vercel/output'), path.join(cwd, 'node_modules/.cache/next'), path.join(cwd, 'node_modules/.cache/turbopack')];
|
|
105
|
+
for (const cand of nestedCandidates) {
|
|
106
|
+
if (await pathExists(cand)) results.add(cand);
|
|
60
107
|
}
|
|
61
108
|
|
|
62
|
-
// Walk subdirectories
|
|
109
|
+
// Walk subdirectories (monorepo support / nested projects)
|
|
63
110
|
for await (const dir of walk(cwd)) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
111
|
+
await checkDir(dir);
|
|
112
|
+
// Check nested in this subdir
|
|
113
|
+
const nestedSub = [path.join(dir, '.vercel/output'), path.join(dir, 'node_modules/.cache/next'), path.join(dir, 'node_modules/.cache/turbopack')];
|
|
114
|
+
for (const cand of nestedSub) {
|
|
115
|
+
if (await pathExists(cand)) results.add(cand);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...results];
|
|
119
|
+
};
|
|
120
|
+
export const getArtifactStats = async p => {
|
|
121
|
+
try {
|
|
122
|
+
const stat = await fs.lstat(p);
|
|
123
|
+
if (!stat.isDirectory()) {
|
|
124
|
+
return {
|
|
125
|
+
size: stat.size,
|
|
126
|
+
mtime: stat.mtime,
|
|
127
|
+
fileCount: 1,
|
|
128
|
+
isDirectory: false
|
|
129
|
+
};
|
|
71
130
|
}
|
|
131
|
+
let totalSize = 0;
|
|
132
|
+
let fileCount = 0;
|
|
133
|
+
let latestMtime = stat.mtime;
|
|
134
|
+
const processDir = async d => {
|
|
135
|
+
let entries = [];
|
|
136
|
+
try {
|
|
137
|
+
entries = await fs.readdir(d, {
|
|
138
|
+
withFileTypes: true
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const files = [];
|
|
144
|
+
const dirs = [];
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const full = path.join(d, entry.name);
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
dirs.push(full);
|
|
149
|
+
} else {
|
|
150
|
+
files.push(full);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
72
153
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
154
|
+
// Process files in this dir
|
|
155
|
+
const fileStats = await Promise.all(files.map(async f => {
|
|
156
|
+
try {
|
|
157
|
+
return await fs.lstat(f);
|
|
158
|
+
} catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}));
|
|
162
|
+
for (const s of fileStats) {
|
|
163
|
+
if (!s) continue;
|
|
164
|
+
totalSize += s.size;
|
|
165
|
+
fileCount++;
|
|
166
|
+
if (s.mtime > latestMtime) latestMtime = s.mtime;
|
|
78
167
|
}
|
|
79
|
-
}
|
|
80
168
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
169
|
+
// Recurse
|
|
170
|
+
await Promise.all(dirs.map(sub => processDir(sub)));
|
|
171
|
+
};
|
|
172
|
+
await processDir(p);
|
|
173
|
+
return {
|
|
174
|
+
size: totalSize,
|
|
175
|
+
mtime: latestMtime,
|
|
176
|
+
fileCount,
|
|
177
|
+
isDirectory: true
|
|
178
|
+
};
|
|
179
|
+
} catch {
|
|
180
|
+
return {
|
|
181
|
+
size: 0,
|
|
182
|
+
mtime: new Date(),
|
|
183
|
+
fileCount: 0,
|
|
184
|
+
isDirectory: false
|
|
185
|
+
};
|
|
86
186
|
}
|
|
87
|
-
return [...new Set(results)];
|
|
88
|
-
};
|
|
89
|
-
export const getDirSize = async dir => {
|
|
90
|
-
let total = 0;
|
|
91
|
-
const processDir = async d => {
|
|
92
|
-
let entries = [];
|
|
93
|
-
try {
|
|
94
|
-
entries = await fs.readdir(d, {
|
|
95
|
-
withFileTypes: true
|
|
96
|
-
});
|
|
97
|
-
} catch {
|
|
98
|
-
return 0;
|
|
99
|
-
}
|
|
100
|
-
const paths = entries.map(entry => path.join(d, entry.name));
|
|
101
|
-
const stats = await Promise.all(paths.map(p => fs.lstat(p).catch(() => undefined)));
|
|
102
|
-
let sizeHere = 0;
|
|
103
|
-
const subdirs = [];
|
|
104
|
-
for (const [i, st] of stats.entries()) {
|
|
105
|
-
if (!st) continue;
|
|
106
|
-
if (st.isSymbolicLink()) continue;
|
|
107
|
-
if (st.isDirectory()) subdirs.push(paths[i]);else sizeHere += st.size;
|
|
108
|
-
}
|
|
109
|
-
const subSizes = await Promise.all(subdirs.map(d2 => processDir(d2)));
|
|
110
|
-
for (const s of subSizes) sizeHere += s;
|
|
111
|
-
return sizeHere;
|
|
112
|
-
};
|
|
113
|
-
total = await processDir(dir);
|
|
114
|
-
return total;
|
|
115
187
|
};
|
|
116
|
-
export const
|
|
117
|
-
const paths = await
|
|
118
|
-
const
|
|
119
|
-
const items =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
188
|
+
export const scanArtifacts = async cwd => {
|
|
189
|
+
const paths = await findArtifacts(cwd);
|
|
190
|
+
const stats = await Promise.all(paths.map(p => getArtifactStats(p)));
|
|
191
|
+
const items = paths.map((path_, index) => ({
|
|
192
|
+
path: path_,
|
|
193
|
+
...stats[index]
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
// Sort by size (descending)
|
|
197
|
+
return items.sort((a, b) => b.size - a.size);
|
|
125
198
|
};
|
package/package.json
CHANGED
|
@@ -1,99 +1,96 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
}
|
|
2
|
+
"name": "next-prune",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Interactive terminal UI to prune Next.js build artifacts and caches to free disk space",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "next-prune contributors",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/khoa-lucents/next-prune.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/khoa-lucents/next-prune/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/khoa-lucents/next-prune#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"nextjs",
|
|
17
|
+
"next",
|
|
18
|
+
"prune",
|
|
19
|
+
"build",
|
|
20
|
+
"artifacts",
|
|
21
|
+
"cache",
|
|
22
|
+
"vercel",
|
|
23
|
+
"turbo",
|
|
24
|
+
"clean",
|
|
25
|
+
"cli",
|
|
26
|
+
"terminal",
|
|
27
|
+
"ui",
|
|
28
|
+
"ink",
|
|
29
|
+
"disk-space",
|
|
30
|
+
"webpack-cache"
|
|
31
|
+
],
|
|
32
|
+
"bin": {
|
|
33
|
+
"next-prune": "dist/cli.js"
|
|
34
|
+
},
|
|
35
|
+
"type": "module",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=16"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"ink": "^6.6.0",
|
|
44
|
+
"meow": "^14.0.0",
|
|
45
|
+
"react": "^19.2.3"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@babel/cli": "^7.28.3",
|
|
49
|
+
"@babel/preset-react": "^7.28.5",
|
|
50
|
+
"@vdemedes/prettier-config": "^2.0.1",
|
|
51
|
+
"ava": "^6.4.1",
|
|
52
|
+
"chalk": "^5.6.2",
|
|
53
|
+
"eslint-config-xo-react": "^0.29.0",
|
|
54
|
+
"eslint-plugin-react": "^7.37.5",
|
|
55
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
56
|
+
"import-jsx": "^5.0.0",
|
|
57
|
+
"ink-testing-library": "^4.0.0",
|
|
58
|
+
"prettier": "^3.7.4",
|
|
59
|
+
"xo": "^1.2.3"
|
|
60
|
+
},
|
|
61
|
+
"ava": {
|
|
62
|
+
"environmentVariables": {
|
|
63
|
+
"NODE_NO_WARNINGS": "1"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"xo": {
|
|
67
|
+
"react": true,
|
|
68
|
+
"prettier": true,
|
|
69
|
+
"semicolon": true,
|
|
70
|
+
"rules": {
|
|
71
|
+
"react/prop-types": "off",
|
|
72
|
+
"unicorn/expiring-todo-comments": "off",
|
|
73
|
+
"unicorn/prevent-abbreviations": "off",
|
|
74
|
+
"react/jsx-sort-props": "off",
|
|
75
|
+
"react/no-array-index-key": "off",
|
|
76
|
+
"no-undef-init": "off",
|
|
77
|
+
"complexity": "warn",
|
|
78
|
+
"capitalized-comments": "warn",
|
|
79
|
+
"padding-line-between-statements": "warn",
|
|
80
|
+
"no-await-in-loop": "warn",
|
|
81
|
+
"unicorn/prefer-switch": "warn",
|
|
82
|
+
"react/self-closing-comp": "warn"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"prettier": "@vdemedes/prettier-config",
|
|
86
|
+
"babel": {
|
|
87
|
+
"presets": [
|
|
88
|
+
"@babel/preset-react"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"scripts": {
|
|
92
|
+
"build": "babel --out-dir=dist source",
|
|
93
|
+
"dev": "babel --out-dir=dist --watch source",
|
|
94
|
+
"test": "prettier --check . && xo && ava"
|
|
95
|
+
}
|
|
96
|
+
}
|