next-prune 1.3.0 → 1.5.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 +205 -106
- package/dist/asset-scanner.js +57 -43
- package/dist/cli.js +65631 -73
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/hooks/use-terminal-size.js +19 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/scanner.js +166 -84
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/package.json +23 -51
- package/readme.md +67 -21
- package/src/app.tsx +858 -0
- package/src/cli.ts +435 -0
- package/src/core/asset-scanner.ts +213 -0
- package/src/core/config.ts +263 -0
- package/src/core/delete.ts +55 -0
- package/src/core/format.ts +52 -0
- package/src/core/index.ts +7 -0
- package/src/core/scanner.ts +548 -0
- package/src/core/types.ts +89 -0
- package/src/core/workspaces.ts +496 -0
- package/src/index.tsx +23 -0
- package/src/ui/artifact-list.tsx +161 -0
- package/src/ui/confirm-modal.tsx +101 -0
- package/src/ui/dashboard.tsx +104 -0
- package/src/ui/footer.tsx +35 -0
- package/src/ui/header.tsx +20 -0
- package/src/ui/types.ts +21 -0
package/dist/app.js
CHANGED
|
@@ -1,37 +1,27 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
5
5
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
6
6
|
import { scanArtifacts, getArtifactStats, human } from './scanner.js';
|
|
7
7
|
import { findUnusedAssets } from './asset-scanner.js';
|
|
8
|
+
import { DEFAULT_CONFIG } from './config.js';
|
|
9
|
+
import { useTerminalSize } from './hooks/use-terminal-size.js';
|
|
8
10
|
import { Header } from './ui/header.js';
|
|
9
11
|
import { Dashboard } from './ui/dashboard.js';
|
|
10
12
|
import { Footer } from './ui/footer.js';
|
|
11
13
|
import { ArtifactList } from './ui/artifact-list.js';
|
|
12
14
|
import { ConfirmModal } from './ui/confirm-modal.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const [rows, setRows] = useState(process.stdout?.rows || 24);
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
const onResize = () => setRows(process.stdout?.rows || 24);
|
|
26
|
-
process.stdout?.on?.('resize', onResize);
|
|
27
|
-
return () => process.stdout?.off?.('resize', onResize);
|
|
28
|
-
}, []);
|
|
29
|
-
return rows;
|
|
30
|
-
}
|
|
31
|
-
const DEFAULT_CONFIG = {
|
|
32
|
-
alwaysDelete: [],
|
|
33
|
-
neverDelete: [],
|
|
34
|
-
checkUnusedAssets: false
|
|
15
|
+
const toPosixPath = value => value.split(path.sep).join('/');
|
|
16
|
+
const matchesConfigPath = (relPath, pattern) => {
|
|
17
|
+
const rel = toPosixPath(relPath);
|
|
18
|
+
const normalizedPattern = String(pattern).replaceAll('\\', '/');
|
|
19
|
+
return rel === normalizedPattern || rel.startsWith(`${normalizedPattern}/`);
|
|
20
|
+
};
|
|
21
|
+
const SORT_MODES = ['size', 'age', 'path'];
|
|
22
|
+
const clampIndex = (nextIndex, totalItems) => {
|
|
23
|
+
if (totalItems <= 0) return 0;
|
|
24
|
+
return Math.max(0, Math.min(totalItems - 1, nextIndex));
|
|
35
25
|
};
|
|
36
26
|
export default function App({
|
|
37
27
|
cwd = process.cwd(),
|
|
@@ -45,17 +35,18 @@ export default function App({
|
|
|
45
35
|
} = useApp();
|
|
46
36
|
const [items, setItems] = useState([]);
|
|
47
37
|
const [loading, setLoading] = useState(!testMode);
|
|
48
|
-
//
|
|
38
|
+
// Selected paths are tracked as a set of absolute paths.
|
|
49
39
|
const [selectedPaths, setSelectedPaths] = useState(new Set());
|
|
50
40
|
const [index, setIndex] = useState(0);
|
|
51
41
|
const [sortBy, setSortBy] = useState('size'); // 'size' | 'age' | 'path'
|
|
52
42
|
const [confirm, setConfirm] = useState(false);
|
|
53
43
|
const [error, setError] = useState('');
|
|
54
|
-
// Removed unused showHelp
|
|
55
44
|
|
|
56
45
|
// Terminal dimensions
|
|
57
|
-
const
|
|
58
|
-
|
|
46
|
+
const {
|
|
47
|
+
columns: cols,
|
|
48
|
+
rows
|
|
49
|
+
} = useTerminalSize();
|
|
59
50
|
|
|
60
51
|
// Derived sorted items
|
|
61
52
|
const sortedItems = useMemo(() => {
|
|
@@ -70,13 +61,47 @@ export default function App({
|
|
|
70
61
|
return 0;
|
|
71
62
|
});
|
|
72
63
|
}, [items, sortBy, cwd]);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
const itemByPath = useMemo(() => new Map(items.map(it => [it.path, it])), [items]);
|
|
65
|
+
const {
|
|
66
|
+
foundCount,
|
|
67
|
+
totalSize,
|
|
68
|
+
selectedSize
|
|
69
|
+
} = useMemo(() => {
|
|
70
|
+
let found = 0;
|
|
71
|
+
let total = 0;
|
|
72
|
+
let selected = 0;
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
if (item.status === 'deleted') continue;
|
|
75
|
+
found++;
|
|
76
|
+
const size = typeof item.size === 'number' ? item.size : 0;
|
|
77
|
+
total += size;
|
|
78
|
+
if (selectedPaths.has(item.path)) {
|
|
79
|
+
selected += size;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
foundCount: found,
|
|
84
|
+
totalSize: total,
|
|
85
|
+
selectedSize: selected
|
|
86
|
+
};
|
|
78
87
|
}, [items, selectedPaths]);
|
|
79
|
-
const
|
|
88
|
+
const sortedIndexByPath = useMemo(() => {
|
|
89
|
+
const map = new Map();
|
|
90
|
+
for (const [sortedIndex, item] of sortedItems.entries()) {
|
|
91
|
+
map.set(item.path, sortedIndex);
|
|
92
|
+
}
|
|
93
|
+
return map;
|
|
94
|
+
}, [sortedItems]);
|
|
95
|
+
const selectedIds = useMemo(() => {
|
|
96
|
+
const indices = new Set();
|
|
97
|
+
for (const selectedPath of selectedPaths) {
|
|
98
|
+
const sortedIndex = sortedIndexByPath.get(selectedPath);
|
|
99
|
+
if (sortedIndex !== undefined) {
|
|
100
|
+
indices.add(sortedIndex);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return indices;
|
|
104
|
+
}, [selectedPaths, sortedIndexByPath]);
|
|
80
105
|
|
|
81
106
|
// Viewport Logic
|
|
82
107
|
const listBoxHeight = Math.max(5, (rows || 24) - 12); // Approx height for list
|
|
@@ -92,7 +117,7 @@ export default function App({
|
|
|
92
117
|
const viewEnd = Math.min(viewStart + listBoxHeight, sortedItems.length);
|
|
93
118
|
|
|
94
119
|
// Handlers
|
|
95
|
-
const doScan = async () => {
|
|
120
|
+
const doScan = useCallback(async () => {
|
|
96
121
|
setLoading(true);
|
|
97
122
|
setError('');
|
|
98
123
|
try {
|
|
@@ -112,7 +137,7 @@ export default function App({
|
|
|
112
137
|
if (config.neverDelete?.length > 0) {
|
|
113
138
|
nextItems = nextItems.filter(it => {
|
|
114
139
|
const rel = path.relative(cwd, it.path);
|
|
115
|
-
return !config.neverDelete.some(pattern => rel
|
|
140
|
+
return !config.neverDelete.some(pattern => matchesConfigPath(rel, pattern));
|
|
116
141
|
});
|
|
117
142
|
}
|
|
118
143
|
setItems(nextItems);
|
|
@@ -122,19 +147,23 @@ export default function App({
|
|
|
122
147
|
} finally {
|
|
123
148
|
setLoading(false);
|
|
124
149
|
}
|
|
125
|
-
};
|
|
126
|
-
const performDeletion = async () => {
|
|
150
|
+
}, [cwd, config.checkUnusedAssets, config.neverDelete]);
|
|
151
|
+
const performDeletion = useCallback(async () => {
|
|
127
152
|
try {
|
|
128
|
-
const pathsToDelete = [
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
153
|
+
const pathsToDelete = [];
|
|
154
|
+
for (const selectedPath of selectedPaths) {
|
|
155
|
+
const item = itemByPath.get(selectedPath);
|
|
156
|
+
if (item && item.status !== 'deleted') {
|
|
157
|
+
pathsToDelete.push(selectedPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
132
160
|
if (pathsToDelete.length === 0) {
|
|
133
161
|
setConfirm(false);
|
|
134
162
|
return;
|
|
135
163
|
}
|
|
164
|
+
const pathsToDeleteSet = new Set(pathsToDelete);
|
|
136
165
|
if (dryRun) {
|
|
137
|
-
setItems(prev => prev.map(it =>
|
|
166
|
+
setItems(prev => prev.map(it => pathsToDeleteSet.has(it.path) ? {
|
|
138
167
|
...it,
|
|
139
168
|
status: 'dry-run'
|
|
140
169
|
} : it));
|
|
@@ -145,99 +174,172 @@ export default function App({
|
|
|
145
174
|
}
|
|
146
175
|
|
|
147
176
|
// Mark deleting
|
|
148
|
-
setItems(prev => prev.map(it =>
|
|
177
|
+
setItems(prev => prev.map(it => pathsToDeleteSet.has(it.path) ? {
|
|
149
178
|
...it,
|
|
150
179
|
status: 'deleting'
|
|
151
180
|
} : it));
|
|
152
|
-
|
|
153
|
-
const successes = new Set();
|
|
154
|
-
for (const p of pathsToDelete) {
|
|
181
|
+
const deletionResults = await Promise.all(pathsToDelete.map(async pathToDelete => {
|
|
155
182
|
try {
|
|
156
|
-
await fs.rm(
|
|
183
|
+
await fs.rm(pathToDelete, {
|
|
157
184
|
recursive: true,
|
|
158
185
|
force: true
|
|
159
186
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
return {
|
|
188
|
+
path: pathToDelete,
|
|
189
|
+
ok: true
|
|
190
|
+
};
|
|
163
191
|
} catch {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
192
|
+
return {
|
|
193
|
+
path: pathToDelete,
|
|
194
|
+
ok: false
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}));
|
|
198
|
+
const succeededPaths = new Set();
|
|
199
|
+
const failedPaths = [];
|
|
200
|
+
let freed = 0;
|
|
201
|
+
for (const result of deletionResults) {
|
|
202
|
+
if (result.ok) {
|
|
203
|
+
succeededPaths.add(result.path);
|
|
204
|
+
const item = itemByPath.get(result.path);
|
|
205
|
+
freed += typeof item?.size === 'number' ? item.size : 0;
|
|
206
|
+
} else {
|
|
207
|
+
failedPaths.push(result.path);
|
|
169
208
|
}
|
|
170
209
|
}
|
|
210
|
+
if (failedPaths.length > 0) {
|
|
211
|
+
const failedPathSet = new Set(failedPaths);
|
|
212
|
+
setItems(prev => prev.map(it => failedPathSet.has(it.path) ? {
|
|
213
|
+
...it,
|
|
214
|
+
status: 'error'
|
|
215
|
+
} : it));
|
|
216
|
+
setError(`Failed to delete: ${failedPaths[0]}`);
|
|
217
|
+
}
|
|
171
218
|
|
|
172
219
|
// Mark deleted
|
|
173
|
-
if (
|
|
174
|
-
setItems(prev => prev.map(it =>
|
|
220
|
+
if (succeededPaths.size > 0) {
|
|
221
|
+
setItems(prev => prev.map(it => succeededPaths.has(it.path) ? {
|
|
175
222
|
...it,
|
|
176
223
|
status: 'deleted',
|
|
177
224
|
size: 0
|
|
178
225
|
} : it));
|
|
179
|
-
setError(`✅ Deleted ${
|
|
226
|
+
setError(`✅ Deleted ${succeededPaths.size} items (freed ${human(freed)})`);
|
|
180
227
|
}
|
|
181
228
|
} finally {
|
|
182
229
|
setSelectedPaths(new Set());
|
|
183
230
|
setConfirm(false);
|
|
184
231
|
}
|
|
185
|
-
};
|
|
232
|
+
}, [selectedPaths, itemByPath, dryRun, selectedSize]);
|
|
233
|
+
const moveFocusBy = useCallback(delta => {
|
|
234
|
+
setIndex(previous => clampIndex(previous + delta, sortedItems.length));
|
|
235
|
+
}, [sortedItems.length]);
|
|
236
|
+
const jumpFocusTo = useCallback(targetIndex => {
|
|
237
|
+
setIndex(clampIndex(targetIndex, sortedItems.length));
|
|
238
|
+
}, [sortedItems.length]);
|
|
239
|
+
const toggleFocusedItemSelection = useCallback(() => {
|
|
240
|
+
const focusedItem = sortedItems[index];
|
|
241
|
+
if (!focusedItem || focusedItem.status === 'deleted') return;
|
|
242
|
+
setSelectedPaths(previous => {
|
|
243
|
+
const next = new Set(previous);
|
|
244
|
+
if (next.has(focusedItem.path)) {
|
|
245
|
+
next.delete(focusedItem.path);
|
|
246
|
+
} else {
|
|
247
|
+
next.add(focusedItem.path);
|
|
248
|
+
}
|
|
249
|
+
return next;
|
|
250
|
+
});
|
|
251
|
+
}, [sortedItems, index]);
|
|
252
|
+
const selectAllItems = useCallback(() => {
|
|
253
|
+
const allPaths = sortedItems.filter(it => it.status !== 'deleted').map(it => it.path);
|
|
254
|
+
setSelectedPaths(new Set(allPaths));
|
|
255
|
+
}, [sortedItems]);
|
|
256
|
+
const cycleSortMode = useCallback(() => {
|
|
257
|
+
setSortBy(previous => {
|
|
258
|
+
const nextIndex = (SORT_MODES.indexOf(previous) + 1) % SORT_MODES.length;
|
|
259
|
+
return SORT_MODES[nextIndex];
|
|
260
|
+
});
|
|
261
|
+
}, []);
|
|
262
|
+
const openDeleteConfirmation = useCallback(() => {
|
|
263
|
+
if (selectedPaths.size > 0) {
|
|
264
|
+
setConfirm(true);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const focusedItem = sortedItems[index];
|
|
268
|
+
if (!focusedItem || focusedItem.status === 'deleted') return;
|
|
269
|
+
setSelectedPaths(new Set([focusedItem.path]));
|
|
270
|
+
setConfirm(true);
|
|
271
|
+
}, [selectedPaths, sortedItems, index]);
|
|
272
|
+
const handleConfirmInput = useCallback((input, key) => {
|
|
273
|
+
if (!confirm) return false;
|
|
274
|
+
if (key.escape || input?.toLowerCase() === 'n') {
|
|
275
|
+
setConfirm(false);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
if (input?.toLowerCase() === 'y' || key.return) {
|
|
279
|
+
performDeletion();
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}, [confirm, performDeletion]);
|
|
186
283
|
useInput((input, key) => {
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
284
|
+
if (handleConfirmInput(input, key)) return;
|
|
285
|
+
if (key.escape || input?.toLowerCase() === 'q') {
|
|
286
|
+
exit();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (key.upArrow) {
|
|
290
|
+
moveFocusBy(-1);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (key.downArrow) {
|
|
294
|
+
moveFocusBy(1);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (key.pageUp) {
|
|
298
|
+
moveFocusBy(-listBoxHeight);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (key.pageDown) {
|
|
302
|
+
moveFocusBy(listBoxHeight);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (key.home) {
|
|
306
|
+
jumpFocusTo(0);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (key.end) {
|
|
310
|
+
jumpFocusTo(sortedItems.length - 1);
|
|
190
311
|
return;
|
|
191
312
|
}
|
|
192
|
-
if (key.escape || input?.toLowerCase() === 'q') exit();
|
|
193
|
-
if (key.upArrow) setIndex(Math.max(0, index - 1));
|
|
194
|
-
if (key.downArrow) setIndex(Math.min(sortedItems.length - 1, index + 1));
|
|
195
|
-
|
|
196
|
-
// Page navigation
|
|
197
|
-
if (key.pageUp) setIndex(Math.max(0, index - listBoxHeight));
|
|
198
|
-
if (key.pageDown) setIndex(Math.min(sortedItems.length - 1, index + listBoxHeight));
|
|
199
|
-
if (key.home) setIndex(0);
|
|
200
|
-
if (key.end) setIndex(sortedItems.length - 1);
|
|
201
313
|
if (input === ' ') {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
setSelectedPaths(prev => {
|
|
205
|
-
const next = new Set(prev);
|
|
206
|
-
if (next.has(currentPath)) next.delete(currentPath);else next.add(currentPath);
|
|
207
|
-
return next;
|
|
208
|
-
});
|
|
209
|
-
}
|
|
314
|
+
toggleFocusedItemSelection();
|
|
315
|
+
return;
|
|
210
316
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
317
|
+
const loweredInput = input?.toLowerCase();
|
|
318
|
+
if (loweredInput === 'a') {
|
|
319
|
+
selectAllItems();
|
|
320
|
+
return;
|
|
214
321
|
}
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const modes = ['size', 'age', 'path'];
|
|
219
|
-
const next = modes[(modes.indexOf(sortBy) + 1) % modes.length];
|
|
220
|
-
setSortBy(next);
|
|
322
|
+
if (loweredInput === 'c') {
|
|
323
|
+
setSelectedPaths(new Set());
|
|
324
|
+
return;
|
|
221
325
|
}
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
326
|
+
if (loweredInput === 'r' && !loading) {
|
|
327
|
+
doScan();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (loweredInput === 's') {
|
|
331
|
+
cycleSortMode();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (loweredInput === 'd' || key.return) {
|
|
335
|
+
openDeleteConfirmation();
|
|
233
336
|
}
|
|
234
337
|
});
|
|
235
338
|
|
|
236
339
|
// Initial scan
|
|
237
340
|
useEffect(() => {
|
|
238
341
|
if (!testMode) doScan();
|
|
239
|
-
|
|
240
|
-
}, [cwd, testMode]);
|
|
342
|
+
}, [testMode, doScan]);
|
|
241
343
|
|
|
242
344
|
// Auto-select based on config
|
|
243
345
|
useEffect(() => {
|
|
@@ -252,7 +354,7 @@ export default function App({
|
|
|
252
354
|
const pre = new Set();
|
|
253
355
|
for (const it of items) {
|
|
254
356
|
const rel = path.relative(cwd, it.path);
|
|
255
|
-
const hit = config.alwaysDelete.some(p => rel
|
|
357
|
+
const hit = config.alwaysDelete.some(p => matchesConfigPath(rel, p));
|
|
256
358
|
if (hit && it.status !== 'deleted') pre.add(it.path);
|
|
257
359
|
}
|
|
258
360
|
if (pre.size > 0) setSelectedPaths(pre);
|
|
@@ -307,10 +409,7 @@ export default function App({
|
|
|
307
409
|
}, error)), /*#__PURE__*/React.createElement(ArtifactList, {
|
|
308
410
|
items: sortedItems,
|
|
309
411
|
selectedIndex: index,
|
|
310
|
-
selectedIds:
|
|
311
|
-
// Map path back to current sorted index for display
|
|
312
|
-
return sortedItems.findIndex(it => it.path === p);
|
|
313
|
-
}).filter(i => i !== -1)),
|
|
412
|
+
selectedIds: selectedIds,
|
|
314
413
|
viewStart: viewStart,
|
|
315
414
|
viewEnd: viewEnd,
|
|
316
415
|
cwd: cwd,
|
package/dist/asset-scanner.js
CHANGED
|
@@ -23,6 +23,18 @@ async function* walkFiles(dir, extensions) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
const collectSourceFilesInDir = async directoryPath => {
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(directoryPath);
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const files = [];
|
|
33
|
+
for await (const filePath of walkFiles(directoryPath, SOURCE_EXTENSIONS)) {
|
|
34
|
+
files.push(filePath);
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
};
|
|
26
38
|
export const findUnusedAssets = async cwd => {
|
|
27
39
|
const publicDir = path.join(cwd, 'public');
|
|
28
40
|
try {
|
|
@@ -32,61 +44,63 @@ export const findUnusedAssets = async cwd => {
|
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
// 1. Gather all assets in public/
|
|
35
|
-
const assets = [];
|
|
47
|
+
const assets = []; // { fullPath, filename, relPath }
|
|
36
48
|
for await (const p of walkFiles(publicDir, IMAGE_EXTENSIONS)) {
|
|
37
|
-
assets.push(
|
|
49
|
+
assets.push({
|
|
50
|
+
fullPath: p,
|
|
51
|
+
filename: path.basename(p),
|
|
52
|
+
relPath: path.relative(publicDir, p).split(path.sep).join('/')
|
|
53
|
+
});
|
|
38
54
|
}
|
|
39
55
|
if (assets.length === 0) return [];
|
|
40
56
|
|
|
41
57
|
// 2. Gather all source files
|
|
42
58
|
const sourceFiles = [];
|
|
43
59
|
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
60
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
// Check root files
|
|
62
|
+
try {
|
|
63
|
+
const rootFiles = await fs.readdir(cwd);
|
|
64
|
+
for (const f of rootFiles) {
|
|
65
|
+
if (SOURCE_EXTENSIONS.has(path.extname(f).toLowerCase())) {
|
|
66
|
+
sourceFiles.push(path.join(cwd, f));
|
|
67
|
+
}
|
|
69
68
|
}
|
|
70
|
-
}
|
|
71
|
-
const
|
|
69
|
+
} catch {}
|
|
70
|
+
const sourceFilesByDirectory = await Promise.all(srcDirs.map(async dirName => collectSourceFilesInDir(path.join(cwd, dirName))));
|
|
71
|
+
sourceFiles.push(...sourceFilesByDirectory.flat());
|
|
72
72
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
// 3. Check usage by streaming files
|
|
74
|
+
// We keep a set of indices of assets that are NOT yet found.
|
|
75
|
+
// Initially all assets are candidates.
|
|
76
|
+
const candidateIndices = new Set(assets.keys());
|
|
77
|
+
for (const file of sourceFiles) {
|
|
78
|
+
if (candidateIndices.size === 0) break; // All found
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
try {
|
|
81
|
+
// eslint-disable-next-line no-await-in-loop
|
|
82
|
+
const content = await fs.readFile(file, 'utf8');
|
|
83
|
+
for (const index of candidateIndices) {
|
|
84
|
+
const asset = assets[index];
|
|
85
|
+
// Naive check: filename or relative path
|
|
86
|
+
// We check "logo.png" or "images/logo.png"
|
|
87
|
+
// Also check with leading slash for paths like "/images/logo.png"
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
const nameMatch = content.includes(asset.filename);
|
|
90
|
+
if (nameMatch) {
|
|
91
|
+
candidateIndices.delete(index);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const relMatch = content.includes(asset.relPath) || content.includes('/' + asset.relPath);
|
|
95
|
+
if (relMatch) {
|
|
96
|
+
candidateIndices.delete(index);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// ignore read errors
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
|
-
|
|
103
|
+
|
|
104
|
+
// 4. Return remaining candidates (unused)
|
|
105
|
+
return [...candidateIndices].map(i => assets[i].fullPath);
|
|
92
106
|
};
|