prunify 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -99
- package/dist/cli.cjs +485 -158
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +485 -158
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -25,10 +25,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
|
-
var
|
|
29
|
-
var
|
|
30
|
-
var
|
|
31
|
-
var
|
|
28
|
+
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
29
|
+
var import_cli_table36 = __toESM(require("cli-table3"), 1);
|
|
30
|
+
var import_node_fs9 = __toESM(require("fs"), 1);
|
|
31
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
32
32
|
var import_node_url = require("url");
|
|
33
33
|
var import_node_readline = __toESM(require("readline"), 1);
|
|
34
34
|
|
|
@@ -80,7 +80,14 @@ var DEFAULT_IGNORE = [
|
|
|
80
80
|
"coverage",
|
|
81
81
|
"coverage/**",
|
|
82
82
|
"**/*.test.ts",
|
|
83
|
+
"**/*.test.tsx",
|
|
84
|
+
"**/*.test.js",
|
|
85
|
+
"**/*.test.jsx",
|
|
83
86
|
"**/*.spec.ts",
|
|
87
|
+
"**/*.spec.tsx",
|
|
88
|
+
"**/*.spec.js",
|
|
89
|
+
"**/*.spec.jsx",
|
|
90
|
+
"**/*.stories.ts",
|
|
84
91
|
"**/*.stories.tsx",
|
|
85
92
|
"**/*.d.ts"
|
|
86
93
|
];
|
|
@@ -225,45 +232,22 @@ function buildGraph(files, getImports) {
|
|
|
225
232
|
function findEntryPoints(rootDir, packageJson) {
|
|
226
233
|
const entries = [
|
|
227
234
|
...resolveNextJsEntries(rootDir),
|
|
228
|
-
...
|
|
235
|
+
...resolveFileBrowserEntries(rootDir),
|
|
236
|
+
...resolvePkgFieldEntries(rootDir, packageJson),
|
|
237
|
+
...resolveFallbackEntries(rootDir)
|
|
229
238
|
];
|
|
230
|
-
if (entries.length === 0) {
|
|
231
|
-
const fallback = resolveFallbackEntry(rootDir);
|
|
232
|
-
if (fallback) entries.push(fallback);
|
|
233
|
-
}
|
|
234
239
|
return [...new Set(entries)];
|
|
235
240
|
}
|
|
236
|
-
function runDFS(graph, entryPoints) {
|
|
237
|
-
const visited = /* @__PURE__ */ new Set();
|
|
238
|
-
const stack = [...entryPoints];
|
|
239
|
-
let node;
|
|
240
|
-
while ((node = stack.pop()) !== void 0) {
|
|
241
|
-
if (visited.has(node)) continue;
|
|
242
|
-
visited.add(node);
|
|
243
|
-
for (const neighbor of graph.get(node) ?? []) {
|
|
244
|
-
if (!visited.has(neighbor)) stack.push(neighbor);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return visited;
|
|
248
|
-
}
|
|
249
|
-
function findDeadChains(graph, deadFiles) {
|
|
250
|
-
const reverseGraph = buildReverseGraph(graph);
|
|
251
|
-
const result = /* @__PURE__ */ new Map();
|
|
252
|
-
for (const deadRoot of deadFiles) {
|
|
253
|
-
result.set(deadRoot, dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph));
|
|
254
|
-
}
|
|
255
|
-
return result;
|
|
256
|
-
}
|
|
257
241
|
function detectCycles(graph) {
|
|
258
242
|
const cycles = [];
|
|
259
243
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
260
244
|
const visited = /* @__PURE__ */ new Set();
|
|
261
245
|
const inStack = /* @__PURE__ */ new Set();
|
|
262
|
-
const
|
|
246
|
+
const path10 = [];
|
|
263
247
|
const acc = { seenKeys, cycles };
|
|
264
248
|
for (const start of graph.keys()) {
|
|
265
249
|
if (!visited.has(start)) {
|
|
266
|
-
dfsForCycles(start, graph, visited, inStack,
|
|
250
|
+
dfsForCycles(start, graph, visited, inStack, path10, acc);
|
|
267
251
|
}
|
|
268
252
|
}
|
|
269
253
|
return cycles;
|
|
@@ -272,12 +256,21 @@ function resolveNextJsEntries(rootDir) {
|
|
|
272
256
|
const isNext = import_node_fs3.default.existsSync(import_node_path3.default.join(rootDir, "next.config.js")) || import_node_fs3.default.existsSync(import_node_path3.default.join(rootDir, "next.config.ts")) || import_node_fs3.default.existsSync(import_node_path3.default.join(rootDir, "next.config.mjs"));
|
|
273
257
|
if (!isNext) return [];
|
|
274
258
|
const entries = [];
|
|
275
|
-
for (const dir of ["pages", "app"]) {
|
|
259
|
+
for (const dir of ["pages", "app", "src/pages", "src/app"]) {
|
|
276
260
|
const dirPath = import_node_path3.default.join(rootDir, dir);
|
|
277
261
|
if (import_node_fs3.default.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath));
|
|
278
262
|
}
|
|
279
263
|
return entries;
|
|
280
264
|
}
|
|
265
|
+
function resolveFileBrowserEntries(rootDir) {
|
|
266
|
+
const PAGE_DIRS = ["src/pages", "src/routes", "src/views", "src/screens"];
|
|
267
|
+
const entries = [];
|
|
268
|
+
for (const rel of PAGE_DIRS) {
|
|
269
|
+
const dirPath = import_node_path3.default.join(rootDir, rel);
|
|
270
|
+
if (import_node_fs3.default.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath));
|
|
271
|
+
}
|
|
272
|
+
return entries;
|
|
273
|
+
}
|
|
281
274
|
function resolvePkgFieldEntries(rootDir, packageJson) {
|
|
282
275
|
const entries = [];
|
|
283
276
|
for (const field of ["main", "module"]) {
|
|
@@ -288,18 +281,31 @@ function resolvePkgFieldEntries(rootDir, packageJson) {
|
|
|
288
281
|
}
|
|
289
282
|
return entries;
|
|
290
283
|
}
|
|
291
|
-
function
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
284
|
+
function resolveFallbackEntries(rootDir) {
|
|
285
|
+
const candidates = [
|
|
286
|
+
"src/main.ts",
|
|
287
|
+
"src/main.tsx",
|
|
288
|
+
"src/main.js",
|
|
289
|
+
"src/main.jsx",
|
|
290
|
+
"src/index.ts",
|
|
291
|
+
"src/index.tsx",
|
|
292
|
+
"src/index.js",
|
|
293
|
+
"src/index.jsx",
|
|
294
|
+
"src/App.ts",
|
|
295
|
+
"src/App.tsx",
|
|
296
|
+
"src/App.js",
|
|
297
|
+
"src/App.jsx",
|
|
298
|
+
"index.ts",
|
|
299
|
+
"index.tsx",
|
|
300
|
+
"index.js",
|
|
301
|
+
"index.jsx"
|
|
302
|
+
];
|
|
303
|
+
return candidates.map((rel) => import_node_path3.default.join(rootDir, rel)).filter((abs) => import_node_fs3.default.existsSync(abs));
|
|
298
304
|
}
|
|
299
305
|
function mkFrame(node, graph) {
|
|
300
306
|
return { node, neighbors: (graph.get(node) ?? /* @__PURE__ */ new Set()).values(), entered: false };
|
|
301
307
|
}
|
|
302
|
-
function dfsForCycles(start, graph, visited, inStack,
|
|
308
|
+
function dfsForCycles(start, graph, visited, inStack, path10, acc) {
|
|
303
309
|
const stack = [mkFrame(start, graph)];
|
|
304
310
|
while (stack.length > 0) {
|
|
305
311
|
const frame = stack.at(-1);
|
|
@@ -311,57 +317,36 @@ function dfsForCycles(start, graph, visited, inStack, path9, acc) {
|
|
|
311
317
|
}
|
|
312
318
|
frame.entered = true;
|
|
313
319
|
inStack.add(frame.node);
|
|
314
|
-
|
|
320
|
+
path10.push(frame.node);
|
|
315
321
|
}
|
|
316
322
|
const { done, value: neighbor } = frame.neighbors.next();
|
|
317
323
|
if (done) {
|
|
318
324
|
stack.pop();
|
|
319
|
-
|
|
325
|
+
path10.pop();
|
|
320
326
|
inStack.delete(frame.node);
|
|
321
327
|
visited.add(frame.node);
|
|
322
328
|
} else {
|
|
323
|
-
handleCycleNeighbor(neighbor, stack,
|
|
329
|
+
handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph);
|
|
324
330
|
}
|
|
325
331
|
}
|
|
326
332
|
}
|
|
327
|
-
function handleCycleNeighbor(neighbor, stack,
|
|
333
|
+
function handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph) {
|
|
328
334
|
if (inStack.has(neighbor)) {
|
|
329
|
-
recordCycle(neighbor,
|
|
335
|
+
recordCycle(neighbor, path10, acc);
|
|
330
336
|
} else if (!visited.has(neighbor)) {
|
|
331
337
|
stack.push(mkFrame(neighbor, graph));
|
|
332
338
|
}
|
|
333
339
|
}
|
|
334
|
-
function recordCycle(cycleStart,
|
|
335
|
-
const idx =
|
|
340
|
+
function recordCycle(cycleStart, path10, acc) {
|
|
341
|
+
const idx = path10.indexOf(cycleStart);
|
|
336
342
|
if (idx === -1) return;
|
|
337
|
-
const cycle = normalizeCycle(
|
|
343
|
+
const cycle = normalizeCycle(path10.slice(idx));
|
|
338
344
|
const key = cycle.join("\0");
|
|
339
345
|
if (!acc.seenKeys.has(key)) {
|
|
340
346
|
acc.seenKeys.add(key);
|
|
341
347
|
acc.cycles.push(cycle);
|
|
342
348
|
}
|
|
343
349
|
}
|
|
344
|
-
function dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph) {
|
|
345
|
-
const chain = [];
|
|
346
|
-
const visited = /* @__PURE__ */ new Set();
|
|
347
|
-
const stack = [...graph.get(deadRoot) ?? []];
|
|
348
|
-
let node;
|
|
349
|
-
while ((node = stack.pop()) !== void 0) {
|
|
350
|
-
if (visited.has(node) || node === deadRoot) continue;
|
|
351
|
-
visited.add(node);
|
|
352
|
-
if (deadFiles.has(node) || isOnlyImportedByDead(node, deadFiles, reverseGraph)) {
|
|
353
|
-
chain.push(node);
|
|
354
|
-
for (const next of graph.get(node) ?? []) {
|
|
355
|
-
if (!visited.has(next)) stack.push(next);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return chain;
|
|
360
|
-
}
|
|
361
|
-
function isOnlyImportedByDead(file, deadFiles, reverseGraph) {
|
|
362
|
-
const importers = reverseGraph.get(file) ?? /* @__PURE__ */ new Set();
|
|
363
|
-
return importers.size === 0 || [...importers].every((imp) => deadFiles.has(imp));
|
|
364
|
-
}
|
|
365
350
|
function buildReverseGraph(graph) {
|
|
366
351
|
const rev = /* @__PURE__ */ new Map();
|
|
367
352
|
for (const [file] of graph) {
|
|
@@ -488,16 +473,101 @@ function ensureDir(dir) {
|
|
|
488
473
|
}
|
|
489
474
|
|
|
490
475
|
// src/modules/dead-code.ts
|
|
476
|
+
var FRAMEWORK_FILE_PATTERNS = [
|
|
477
|
+
/^next\.config\.(js|ts|mjs|cjs)$/,
|
|
478
|
+
/^middleware\.(ts|js)$/,
|
|
479
|
+
/^instrumentation\.(ts|js)$/,
|
|
480
|
+
/^tailwind\.config\.(js|ts|mjs|cjs)$/,
|
|
481
|
+
/^postcss\.config\.(js|ts|mjs|cjs)$/,
|
|
482
|
+
/^jest\.config\.(js|ts|mjs|cjs)$/,
|
|
483
|
+
/^vitest\.config\.(js|ts|mjs|cjs)$/,
|
|
484
|
+
/^vite\.config\.(js|ts|mjs|cjs)$/,
|
|
485
|
+
/^webpack\.config\.(js|ts|mjs|cjs)$/,
|
|
486
|
+
/^babel\.config\.(js|ts|cjs|mjs|json)$/,
|
|
487
|
+
/^\.babelrc\.(js|cjs)$/,
|
|
488
|
+
/^\.eslintrc\.(js|cjs)$/,
|
|
489
|
+
/^eslint\.config\.(js|ts|mjs|cjs)$/,
|
|
490
|
+
/^prettier\.config\.(js|ts|mjs|cjs)$/,
|
|
491
|
+
/^tsup\.config\.(ts|js)$/,
|
|
492
|
+
/^rollup\.config\.(js|ts|mjs)$/,
|
|
493
|
+
/^esbuild\.config\.(js|ts|mjs)$/,
|
|
494
|
+
/^commitlint\.config\.(js|ts)$/,
|
|
495
|
+
/^lint-staged\.config\.(js|ts|mjs|cjs)$/,
|
|
496
|
+
/^sentry\.(client|server|edge)\.config\.(ts|js)$/
|
|
497
|
+
];
|
|
498
|
+
function isFrameworkFile(filePath, rootDir) {
|
|
499
|
+
const rel = import_node_path5.default.relative(rootDir, filePath);
|
|
500
|
+
const basename = import_node_path5.default.basename(rel);
|
|
501
|
+
return FRAMEWORK_FILE_PATTERNS.some((re) => re.test(basename));
|
|
502
|
+
}
|
|
491
503
|
function runDeadCodeModule(project, graph, entryPoints, rootDir) {
|
|
492
504
|
const allFiles = [...graph.keys()];
|
|
493
|
-
const
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
|
|
505
|
+
const entrySet = new Set(entryPoints);
|
|
506
|
+
const reverseGraph = buildReverseGraph(graph);
|
|
507
|
+
const excludedFiles = /* @__PURE__ */ new Set();
|
|
508
|
+
for (const file of allFiles) {
|
|
509
|
+
if (isFrameworkFile(file, rootDir)) excludedFiles.add(file);
|
|
510
|
+
}
|
|
511
|
+
const safeToDelete = allFiles.filter((f) => {
|
|
512
|
+
if (entrySet.has(f) || excludedFiles.has(f)) return false;
|
|
513
|
+
const importers = reverseGraph.get(f);
|
|
514
|
+
return !importers || importers.size === 0;
|
|
515
|
+
});
|
|
516
|
+
const deadSet = new Set(safeToDelete);
|
|
517
|
+
let changed = true;
|
|
518
|
+
while (changed) {
|
|
519
|
+
changed = false;
|
|
520
|
+
for (const file of allFiles) {
|
|
521
|
+
if (deadSet.has(file) || entrySet.has(file) || excludedFiles.has(file)) continue;
|
|
522
|
+
const importers = reverseGraph.get(file);
|
|
523
|
+
if (!importers || importers.size === 0) continue;
|
|
524
|
+
if ([...importers].every((imp) => deadSet.has(imp))) {
|
|
525
|
+
deadSet.add(file);
|
|
526
|
+
changed = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const transitivelyDead = [...deadSet].filter((f) => !safeToDelete.includes(f));
|
|
531
|
+
const chains = /* @__PURE__ */ new Map();
|
|
532
|
+
for (const root of safeToDelete) {
|
|
533
|
+
const chain = collectTransitiveChain(root, graph, deadSet);
|
|
534
|
+
chains.set(root, chain);
|
|
535
|
+
}
|
|
536
|
+
const liveFiles = new Set(allFiles.filter((f) => !deadSet.has(f)));
|
|
498
537
|
const deadExports = findDeadExports(project, liveFiles);
|
|
499
|
-
const report = buildDeadCodeReport(
|
|
500
|
-
|
|
538
|
+
const report = buildDeadCodeReport(
|
|
539
|
+
safeToDelete,
|
|
540
|
+
transitivelyDead,
|
|
541
|
+
chains,
|
|
542
|
+
deadExports,
|
|
543
|
+
rootDir
|
|
544
|
+
);
|
|
545
|
+
return {
|
|
546
|
+
safeToDelete,
|
|
547
|
+
transitivelyDead,
|
|
548
|
+
deadFiles: [...deadSet],
|
|
549
|
+
liveFiles,
|
|
550
|
+
chains,
|
|
551
|
+
deadExports,
|
|
552
|
+
report
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function collectTransitiveChain(root, graph, deadSet) {
|
|
556
|
+
const chain = [];
|
|
557
|
+
const visited = /* @__PURE__ */ new Set();
|
|
558
|
+
const stack = [...graph.get(root) ?? []];
|
|
559
|
+
let node;
|
|
560
|
+
while ((node = stack.pop()) !== void 0) {
|
|
561
|
+
if (visited.has(node) || node === root) continue;
|
|
562
|
+
visited.add(node);
|
|
563
|
+
if (deadSet.has(node)) {
|
|
564
|
+
chain.push(node);
|
|
565
|
+
for (const next of graph.get(node) ?? []) {
|
|
566
|
+
if (!visited.has(next)) stack.push(next);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return chain;
|
|
501
571
|
}
|
|
502
572
|
function findDeadExports(project, liveFiles) {
|
|
503
573
|
const importedNames = buildImportedNameMap(project, liveFiles);
|
|
@@ -530,51 +600,61 @@ function getFileSize(filePath) {
|
|
|
530
600
|
return 0;
|
|
531
601
|
}
|
|
532
602
|
}
|
|
533
|
-
function buildDeadCodeReport(
|
|
603
|
+
function buildDeadCodeReport(safeToDelete, transitivelyDead, chains, deadExports, rootDir) {
|
|
534
604
|
const rel = (p) => import_node_path5.default.relative(rootDir, p).replaceAll("\\", "/");
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
return sum + getFileSize(f) + chain.reduce((s, c) => s + getFileSize(c), 0);
|
|
538
|
-
}, 0);
|
|
605
|
+
const allDeadFiles = [...safeToDelete, ...transitivelyDead];
|
|
606
|
+
const totalBytes = allDeadFiles.reduce((sum, f) => sum + getFileSize(f), 0);
|
|
539
607
|
const totalKb = (totalBytes / 1024).toFixed(1);
|
|
540
608
|
const lines = [
|
|
541
609
|
"========================================",
|
|
542
610
|
" DEAD CODE REPORT",
|
|
543
|
-
`
|
|
544
|
-
`
|
|
545
|
-
`
|
|
611
|
+
` Safe to delete : ${safeToDelete.length}`,
|
|
612
|
+
` Transitively dead : ${transitivelyDead.length}`,
|
|
613
|
+
` Dead exports : ${deadExports.length}`,
|
|
614
|
+
` Recoverable : ~${totalKb} KB`,
|
|
546
615
|
"========================================",
|
|
547
616
|
""
|
|
548
617
|
];
|
|
549
|
-
if (
|
|
550
|
-
lines.push(
|
|
551
|
-
|
|
618
|
+
if (safeToDelete.length > 0) {
|
|
619
|
+
lines.push(
|
|
620
|
+
"\u2500\u2500 SAFE TO DELETE \u2500\u2500",
|
|
621
|
+
"(These files are not imported by any other file in the codebase)",
|
|
622
|
+
""
|
|
623
|
+
);
|
|
624
|
+
for (const filePath of safeToDelete) {
|
|
552
625
|
const chain = chains.get(filePath) ?? [];
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
626
|
+
const sizeKb = (getFileSize(filePath) / 1024).toFixed(1);
|
|
627
|
+
lines.push(` ${rel(filePath)} (~${sizeKb} KB)`);
|
|
628
|
+
if (chain.length > 0) {
|
|
629
|
+
lines.push(` \u2514\u2500 also makes dead: ${chain.map(rel).join(", ")}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
lines.push("");
|
|
633
|
+
}
|
|
634
|
+
if (transitivelyDead.length > 0) {
|
|
635
|
+
lines.push(
|
|
636
|
+
"\u2500\u2500 TRANSITIVELY DEAD \u2500\u2500",
|
|
637
|
+
"(These files are only imported by dead files \u2014 they become orphaned too)",
|
|
638
|
+
""
|
|
639
|
+
);
|
|
640
|
+
for (const filePath of transitivelyDead) {
|
|
641
|
+
const sizeKb = (getFileSize(filePath) / 1024).toFixed(1);
|
|
642
|
+
lines.push(` ${rel(filePath)} (~${sizeKb} KB)`);
|
|
566
643
|
}
|
|
644
|
+
lines.push("");
|
|
567
645
|
}
|
|
568
646
|
if (deadExports.length > 0) {
|
|
569
|
-
lines.push(
|
|
647
|
+
lines.push(
|
|
648
|
+
"\u2500\u2500 DEAD EXPORTS \u2500\u2500",
|
|
649
|
+
"(Exported but never imported by any other file)",
|
|
650
|
+
""
|
|
651
|
+
);
|
|
570
652
|
for (const entry of deadExports) {
|
|
571
653
|
lines.push(
|
|
572
|
-
`
|
|
573
|
-
`Reason: Exported but never imported`,
|
|
574
|
-
`Action: Remove the export (file itself is still live)`,
|
|
575
|
-
""
|
|
654
|
+
` ${rel(entry.filePath)} \u2192 ${entry.exportName} [line ${entry.line}]`
|
|
576
655
|
);
|
|
577
656
|
}
|
|
657
|
+
lines.push("");
|
|
578
658
|
}
|
|
579
659
|
return lines.join("\n");
|
|
580
660
|
}
|
|
@@ -591,7 +671,8 @@ async function runDeadCode(dir, opts) {
|
|
|
591
671
|
const entries = findEntryPoints(dir, packageJson);
|
|
592
672
|
const result = runDeadCodeModule(project, graph, entries, dir);
|
|
593
673
|
const dead = [
|
|
594
|
-
...result.
|
|
674
|
+
...result.safeToDelete.map((f) => ({ file: f, exportName: "(entire file \u2014 safe to delete)" })),
|
|
675
|
+
...result.transitivelyDead.map((f) => ({ file: f, exportName: "(entire file \u2014 transitively dead)" })),
|
|
595
676
|
...result.deadExports.map((e) => ({ file: e.filePath, exportName: e.exportName }))
|
|
596
677
|
];
|
|
597
678
|
spinner.succeed(import_chalk.default.green(`Dead code scan complete \u2014 ${dead.length} item(s) found`));
|
|
@@ -667,7 +748,11 @@ function writeDeadOutput(result, opts) {
|
|
|
667
748
|
if (!opts.output) return;
|
|
668
749
|
if (opts.json) {
|
|
669
750
|
writeJson(
|
|
670
|
-
{
|
|
751
|
+
{
|
|
752
|
+
safeToDelete: result.safeToDelete,
|
|
753
|
+
transitivelyDead: result.transitivelyDead,
|
|
754
|
+
deadExports: result.deadExports
|
|
755
|
+
},
|
|
671
756
|
opts.output
|
|
672
757
|
);
|
|
673
758
|
console.log(import_chalk.default.cyan(` JSON written to ${opts.output}`));
|
|
@@ -676,16 +761,21 @@ function writeDeadOutput(result, opts) {
|
|
|
676
761
|
writeMarkdown(
|
|
677
762
|
{
|
|
678
763
|
title: "Dead Code Report",
|
|
679
|
-
summary: `${result.
|
|
764
|
+
summary: `${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`,
|
|
680
765
|
sections: [
|
|
681
766
|
{
|
|
682
|
-
title: "
|
|
767
|
+
title: "Safe to Delete",
|
|
683
768
|
headers: ["File", "Chain"],
|
|
684
|
-
rows: result.
|
|
769
|
+
rows: result.safeToDelete.map((f) => [
|
|
685
770
|
f,
|
|
686
771
|
(result.chains.get(f) ?? []).join(" \u2192 ") || "\u2014"
|
|
687
772
|
])
|
|
688
773
|
},
|
|
774
|
+
{
|
|
775
|
+
title: "Transitively Dead",
|
|
776
|
+
headers: ["File"],
|
|
777
|
+
rows: result.transitivelyDead.map((f) => [f])
|
|
778
|
+
},
|
|
689
779
|
{
|
|
690
780
|
title: "Dead Exports",
|
|
691
781
|
headers: ["File", "Export", "Line"],
|
|
@@ -711,17 +801,30 @@ var import_node_crypto = __toESM(require("crypto"), 1);
|
|
|
711
801
|
var import_ts_morph3 = require("ts-morph");
|
|
712
802
|
|
|
713
803
|
// src/modules/dupe-finder.ts
|
|
804
|
+
var MIN_BLOCK_CHARS = 120;
|
|
714
805
|
async function runDupeFinder(dir, opts) {
|
|
715
|
-
const minLines = parseInt(opts.minLines ?? "
|
|
806
|
+
const minLines = parseInt(opts.minLines ?? "15", 10);
|
|
716
807
|
const spinner = (0, import_ora3.default)(import_chalk2.default.cyan(`Scanning for duplicate blocks (\u2265${minLines} lines)\u2026`)).start();
|
|
717
808
|
try {
|
|
718
|
-
const files = glob(dir, ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], [
|
|
809
|
+
const files = glob(dir, ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], [
|
|
810
|
+
"node_modules",
|
|
811
|
+
"dist",
|
|
812
|
+
".next",
|
|
813
|
+
"coverage",
|
|
814
|
+
"**/*.test.ts",
|
|
815
|
+
"**/*.test.tsx",
|
|
816
|
+
"**/*.spec.ts",
|
|
817
|
+
"**/*.spec.tsx",
|
|
818
|
+
"**/*.d.ts"
|
|
819
|
+
]);
|
|
719
820
|
const blockMap = /* @__PURE__ */ new Map();
|
|
720
821
|
for (const filePath of files) {
|
|
721
822
|
const content = import_node_fs6.default.readFileSync(filePath, "utf-8");
|
|
722
823
|
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
723
824
|
for (let i = 0; i <= lines.length - minLines; i++) {
|
|
724
825
|
const block = lines.slice(i, i + minLines).join("\n");
|
|
826
|
+
const meaningfulChars = block.replace(/[\s{}();,]/g, "").length;
|
|
827
|
+
if (meaningfulChars < MIN_BLOCK_CHARS) continue;
|
|
725
828
|
if (!blockMap.has(block)) blockMap.set(block, []);
|
|
726
829
|
blockMap.get(block).push({ file: filePath, startLine: i + 1 });
|
|
727
830
|
}
|
|
@@ -736,6 +839,8 @@ async function runDupeFinder(dir, opts) {
|
|
|
736
839
|
});
|
|
737
840
|
}
|
|
738
841
|
}
|
|
842
|
+
dupes.sort((a, b) => b.occurrences.length - a.occurrences.length);
|
|
843
|
+
if (dupes.length > 200) dupes.splice(200);
|
|
739
844
|
spinner.succeed(import_chalk2.default.green(`Duplicate scan complete \u2014 ${dupes.length} duplicate block(s) found`));
|
|
740
845
|
if (dupes.length === 0) {
|
|
741
846
|
console.log(import_chalk2.default.green(" No duplicate blocks detected."));
|
|
@@ -987,27 +1092,219 @@ async function runHealthReport(dir, opts) {
|
|
|
987
1092
|
);
|
|
988
1093
|
}
|
|
989
1094
|
|
|
1095
|
+
// src/modules/assets.ts
|
|
1096
|
+
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
1097
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1098
|
+
var import_ora6 = __toESM(require("ora"), 1);
|
|
1099
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
1100
|
+
var import_cli_table35 = __toESM(require("cli-table3"), 1);
|
|
1101
|
+
var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1102
|
+
".png",
|
|
1103
|
+
".jpg",
|
|
1104
|
+
".jpeg",
|
|
1105
|
+
".gif",
|
|
1106
|
+
".svg",
|
|
1107
|
+
".webp",
|
|
1108
|
+
".avif",
|
|
1109
|
+
".ico",
|
|
1110
|
+
".bmp",
|
|
1111
|
+
".woff",
|
|
1112
|
+
".woff2",
|
|
1113
|
+
".ttf",
|
|
1114
|
+
".eot",
|
|
1115
|
+
".otf",
|
|
1116
|
+
".mp4",
|
|
1117
|
+
".webm",
|
|
1118
|
+
".ogg",
|
|
1119
|
+
".mp3",
|
|
1120
|
+
".wav",
|
|
1121
|
+
".pdf"
|
|
1122
|
+
]);
|
|
1123
|
+
var SOURCE_PATTERNS2 = [
|
|
1124
|
+
"**/*.ts",
|
|
1125
|
+
"**/*.tsx",
|
|
1126
|
+
"**/*.js",
|
|
1127
|
+
"**/*.jsx",
|
|
1128
|
+
"**/*.css",
|
|
1129
|
+
"**/*.scss",
|
|
1130
|
+
"**/*.sass",
|
|
1131
|
+
"**/*.less",
|
|
1132
|
+
"**/*.html",
|
|
1133
|
+
"**/*.json"
|
|
1134
|
+
];
|
|
1135
|
+
var SOURCE_IGNORE = [
|
|
1136
|
+
"node_modules",
|
|
1137
|
+
"node_modules/**",
|
|
1138
|
+
"dist",
|
|
1139
|
+
"dist/**",
|
|
1140
|
+
".next",
|
|
1141
|
+
".next/**",
|
|
1142
|
+
"coverage",
|
|
1143
|
+
"coverage/**",
|
|
1144
|
+
"public",
|
|
1145
|
+
"public/**"
|
|
1146
|
+
];
|
|
1147
|
+
function runAssetCheckModule(rootDir) {
|
|
1148
|
+
const publicDir = import_node_path8.default.join(rootDir, "public");
|
|
1149
|
+
if (!import_node_fs8.default.existsSync(publicDir)) {
|
|
1150
|
+
return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
|
|
1151
|
+
}
|
|
1152
|
+
const assets = collectAssets(publicDir);
|
|
1153
|
+
if (assets.length === 0) {
|
|
1154
|
+
return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
|
|
1155
|
+
}
|
|
1156
|
+
const sourceFiles = glob(rootDir, SOURCE_PATTERNS2, SOURCE_IGNORE);
|
|
1157
|
+
const sourceContent = sourceFiles.reduce((acc, f) => {
|
|
1158
|
+
try {
|
|
1159
|
+
return acc + import_node_fs8.default.readFileSync(f, "utf-8") + "\n";
|
|
1160
|
+
} catch {
|
|
1161
|
+
return acc;
|
|
1162
|
+
}
|
|
1163
|
+
}, "");
|
|
1164
|
+
const unused = [];
|
|
1165
|
+
for (const assetAbs of assets) {
|
|
1166
|
+
const fileName = import_node_path8.default.basename(assetAbs);
|
|
1167
|
+
const relFromPublic = "/" + import_node_path8.default.relative(publicDir, assetAbs).replaceAll("\\", "/");
|
|
1168
|
+
const referenced = sourceContent.includes(fileName) || sourceContent.includes(relFromPublic);
|
|
1169
|
+
if (!referenced) {
|
|
1170
|
+
unused.push({
|
|
1171
|
+
filePath: assetAbs,
|
|
1172
|
+
relativePath: import_node_path8.default.relative(rootDir, assetAbs).replaceAll("\\", "/"),
|
|
1173
|
+
sizeBytes: getFileSize2(assetAbs)
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const report = buildAssetReport(unused, assets.length, rootDir);
|
|
1178
|
+
return { unusedAssets: unused, totalAssets: assets.length, report };
|
|
1179
|
+
}
|
|
1180
|
+
async function runAssetCheck(rootDir, opts) {
|
|
1181
|
+
const publicDir = import_node_path8.default.join(rootDir, "public");
|
|
1182
|
+
if (!import_node_fs8.default.existsSync(publicDir)) {
|
|
1183
|
+
console.log(import_chalk6.default.dim(" No public/ folder found \u2014 skipping asset check"));
|
|
1184
|
+
return [];
|
|
1185
|
+
}
|
|
1186
|
+
const spinner = (0, import_ora6.default)(import_chalk6.default.cyan("Scanning public/ for unused assets\u2026")).start();
|
|
1187
|
+
try {
|
|
1188
|
+
const result = runAssetCheckModule(rootDir);
|
|
1189
|
+
spinner.succeed(
|
|
1190
|
+
import_chalk6.default.green(
|
|
1191
|
+
`Asset scan complete \u2014 ${result.unusedAssets.length} unused / ${result.totalAssets} total`
|
|
1192
|
+
)
|
|
1193
|
+
);
|
|
1194
|
+
if (result.unusedAssets.length === 0) {
|
|
1195
|
+
console.log(import_chalk6.default.green(" All public assets are referenced in source."));
|
|
1196
|
+
return [];
|
|
1197
|
+
}
|
|
1198
|
+
const table = new import_cli_table35.default({ head: ["Asset", "Size"] });
|
|
1199
|
+
for (const asset of result.unusedAssets) {
|
|
1200
|
+
const kb = (asset.sizeBytes / 1024).toFixed(1);
|
|
1201
|
+
table.push([import_chalk6.default.gray(asset.relativePath), `${kb} KB`]);
|
|
1202
|
+
}
|
|
1203
|
+
console.log(table.toString());
|
|
1204
|
+
if (opts.output) {
|
|
1205
|
+
writeMarkdown(
|
|
1206
|
+
{
|
|
1207
|
+
title: "Unused Assets Report",
|
|
1208
|
+
summary: `${result.unusedAssets.length} unused asset(s) found in public/`,
|
|
1209
|
+
sections: [
|
|
1210
|
+
{
|
|
1211
|
+
title: "Unused Assets",
|
|
1212
|
+
headers: ["Asset", "Size (KB)"],
|
|
1213
|
+
rows: result.unusedAssets.map((a) => [
|
|
1214
|
+
a.relativePath,
|
|
1215
|
+
(a.sizeBytes / 1024).toFixed(1)
|
|
1216
|
+
])
|
|
1217
|
+
}
|
|
1218
|
+
],
|
|
1219
|
+
generatedAt: /* @__PURE__ */ new Date()
|
|
1220
|
+
},
|
|
1221
|
+
opts.output
|
|
1222
|
+
);
|
|
1223
|
+
console.log(import_chalk6.default.cyan(` Report written to ${opts.output}`));
|
|
1224
|
+
}
|
|
1225
|
+
return result.unusedAssets;
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
spinner.fail(import_chalk6.default.red("Asset scan failed"));
|
|
1228
|
+
throw err;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function collectAssets(dir) {
|
|
1232
|
+
const results = [];
|
|
1233
|
+
function walk(current) {
|
|
1234
|
+
let entries;
|
|
1235
|
+
try {
|
|
1236
|
+
entries = import_node_fs8.default.readdirSync(current, { withFileTypes: true });
|
|
1237
|
+
} catch {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
for (const entry of entries) {
|
|
1241
|
+
const full = import_node_path8.default.join(current, entry.name);
|
|
1242
|
+
if (entry.isDirectory()) {
|
|
1243
|
+
walk(full);
|
|
1244
|
+
} else if (entry.isFile() && ASSET_EXTENSIONS.has(import_node_path8.default.extname(entry.name).toLowerCase())) {
|
|
1245
|
+
results.push(full);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
walk(dir);
|
|
1250
|
+
return results;
|
|
1251
|
+
}
|
|
1252
|
+
function getFileSize2(filePath) {
|
|
1253
|
+
try {
|
|
1254
|
+
return import_node_fs8.default.statSync(filePath).size;
|
|
1255
|
+
} catch {
|
|
1256
|
+
return 0;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function buildAssetReport(unused, totalAssets, rootDir) {
|
|
1260
|
+
const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0);
|
|
1261
|
+
const totalKb = (totalBytes / 1024).toFixed(1);
|
|
1262
|
+
const lines = [
|
|
1263
|
+
"========================================",
|
|
1264
|
+
" UNUSED ASSETS REPORT",
|
|
1265
|
+
` Total assets : ${totalAssets}`,
|
|
1266
|
+
` Unused assets : ${unused.length}`,
|
|
1267
|
+
` Recoverable : ~${totalKb} KB`,
|
|
1268
|
+
"========================================",
|
|
1269
|
+
""
|
|
1270
|
+
];
|
|
1271
|
+
if (unused.length === 0) {
|
|
1272
|
+
lines.push(" All public assets are referenced in source.", "");
|
|
1273
|
+
return lines.join("\n");
|
|
1274
|
+
}
|
|
1275
|
+
lines.push("\u2500\u2500 UNUSED ASSETS \u2500\u2500", "");
|
|
1276
|
+
for (const asset of unused) {
|
|
1277
|
+
lines.push(
|
|
1278
|
+
`UNUSED \u2014 ${asset.relativePath}`,
|
|
1279
|
+
`Size: ~${(asset.sizeBytes / 1024).toFixed(1)} KB`,
|
|
1280
|
+
`Action: Safe to delete if not served directly via URL`,
|
|
1281
|
+
""
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
return lines.join("\n");
|
|
1285
|
+
}
|
|
1286
|
+
|
|
990
1287
|
// src/cli.ts
|
|
991
1288
|
var import_meta = {};
|
|
992
1289
|
function readPkgVersion() {
|
|
993
1290
|
try {
|
|
994
1291
|
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
995
|
-
const dir =
|
|
996
|
-
const pkgPath =
|
|
997
|
-
return JSON.parse(
|
|
1292
|
+
const dir = import_node_path9.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
1293
|
+
const pkgPath = import_node_path9.default.resolve(dir, "..", "package.json");
|
|
1294
|
+
return JSON.parse(import_node_fs9.default.readFileSync(pkgPath, "utf-8")).version;
|
|
998
1295
|
}
|
|
999
1296
|
} catch {
|
|
1000
1297
|
}
|
|
1001
1298
|
try {
|
|
1002
1299
|
const dir = globalThis.__dirname ?? __dirname;
|
|
1003
|
-
const pkgPath =
|
|
1004
|
-
return JSON.parse(
|
|
1300
|
+
const pkgPath = import_node_path9.default.resolve(dir, "..", "package.json");
|
|
1301
|
+
return JSON.parse(import_node_fs9.default.readFileSync(pkgPath, "utf-8")).version;
|
|
1005
1302
|
} catch {
|
|
1006
1303
|
return "0.0.0";
|
|
1007
1304
|
}
|
|
1008
1305
|
}
|
|
1009
1306
|
var PKG_VERSION = readPkgVersion();
|
|
1010
|
-
var ALL_MODULES = ["dead-code", "dupes", "circular", "deps"];
|
|
1307
|
+
var ALL_MODULES = ["dead-code", "dupes", "circular", "deps", "assets"];
|
|
1011
1308
|
var program = new import_commander.Command();
|
|
1012
1309
|
program.name("prunify").description("npm run clean. ship with confidence.").version(PKG_VERSION, "-v, --version").option("--dir <path>", "Root directory to analyze", process.cwd()).option("--entry <path>", "Override entry point").option("--only <modules>", "Comma-separated: dead-code,dupes,circular,deps,health").option(
|
|
1013
1310
|
"--ignore <pattern>",
|
|
@@ -1017,29 +1314,29 @@ program.name("prunify").description("npm run clean. ship with confidence.").vers
|
|
|
1017
1314
|
).option("--out <path>", "Output directory for reports").option("--html", "Also generate code_health.html").option("--delete", "Prompt to delete dead files after analysis").option("--ci", "CI mode: exit 1 if issues found, no interactive prompts").action(main);
|
|
1018
1315
|
program.parse();
|
|
1019
1316
|
async function main(opts) {
|
|
1020
|
-
const rootDir =
|
|
1021
|
-
if (!
|
|
1022
|
-
console.error(
|
|
1023
|
-
console.error(
|
|
1317
|
+
const rootDir = import_node_path9.default.resolve(opts.dir);
|
|
1318
|
+
if (!import_node_fs9.default.existsSync(import_node_path9.default.join(rootDir, "package.json"))) {
|
|
1319
|
+
console.error(import_chalk7.default.red(`\u2717 No package.json found in ${rootDir}`));
|
|
1320
|
+
console.error(import_chalk7.default.dim(" Use --dir <path> to point to your project root."));
|
|
1024
1321
|
process.exit(1);
|
|
1025
1322
|
}
|
|
1026
1323
|
const modules = resolveModules(opts.only);
|
|
1027
1324
|
console.log();
|
|
1028
|
-
console.log(
|
|
1325
|
+
console.log(import_chalk7.default.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
|
|
1029
1326
|
console.log();
|
|
1030
|
-
const parseSpinner = createSpinner(
|
|
1327
|
+
const parseSpinner = createSpinner(import_chalk7.default.cyan("Parsing codebase\u2026"));
|
|
1031
1328
|
const files = discoverFiles(rootDir, opts.ignore);
|
|
1032
|
-
parseSpinner.succeed(
|
|
1033
|
-
const graphSpinner = createSpinner(
|
|
1329
|
+
parseSpinner.succeed(import_chalk7.default.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
|
|
1330
|
+
const graphSpinner = createSpinner(import_chalk7.default.cyan("Building import graph\u2026"));
|
|
1034
1331
|
const project = buildProject(files);
|
|
1035
1332
|
const graph = buildGraph(files, (f) => {
|
|
1036
1333
|
const sf = project.getSourceFile(f);
|
|
1037
1334
|
return sf ? getImportsForFile(sf) : [];
|
|
1038
1335
|
});
|
|
1039
1336
|
const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
|
|
1040
|
-
graphSpinner.succeed(
|
|
1337
|
+
graphSpinner.succeed(import_chalk7.default.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
|
|
1041
1338
|
const packageJson = loadPackageJson2(rootDir);
|
|
1042
|
-
const entryPoints = opts.entry ? [
|
|
1339
|
+
const entryPoints = opts.entry ? [import_node_path9.default.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
|
|
1043
1340
|
const reportsDir = ensureReportsDir(rootDir, opts.out);
|
|
1044
1341
|
appendToGitignore(rootDir);
|
|
1045
1342
|
console.log();
|
|
@@ -1047,90 +1344,120 @@ async function main(opts) {
|
|
|
1047
1344
|
let dupeCount = 0;
|
|
1048
1345
|
let unusedPkgCount = 0;
|
|
1049
1346
|
let circularCount = 0;
|
|
1347
|
+
let unusedAssetCount = 0;
|
|
1050
1348
|
let deadReportFile = "";
|
|
1051
1349
|
let dupesReportFile = "";
|
|
1052
1350
|
let depsReportFile = "";
|
|
1053
1351
|
let circularReportFile = "";
|
|
1352
|
+
let assetsReportFile = "";
|
|
1054
1353
|
const deadFilePaths = [];
|
|
1055
1354
|
if (modules.includes("dead-code")) {
|
|
1056
|
-
const spinner = createSpinner(
|
|
1355
|
+
const spinner = createSpinner(import_chalk7.default.cyan("Analysing dead code\u2026"));
|
|
1057
1356
|
const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
|
|
1058
|
-
deadFileCount = result.
|
|
1059
|
-
deadFilePaths.push(...result.
|
|
1060
|
-
spinner.succeed(
|
|
1357
|
+
deadFileCount = result.safeToDelete.length + result.transitivelyDead.length + result.deadExports.length;
|
|
1358
|
+
deadFilePaths.push(...result.safeToDelete, ...result.transitivelyDead);
|
|
1359
|
+
spinner.succeed(
|
|
1360
|
+
import_chalk7.default.green(
|
|
1361
|
+
`Dead code analysis complete \u2014 ${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`
|
|
1362
|
+
)
|
|
1363
|
+
);
|
|
1061
1364
|
if (result.report) {
|
|
1062
1365
|
deadReportFile = "dead-code.txt";
|
|
1063
1366
|
writeReport(reportsDir, deadReportFile, result.report);
|
|
1064
1367
|
}
|
|
1065
1368
|
}
|
|
1066
1369
|
if (modules.includes("dupes")) {
|
|
1067
|
-
const outputPath =
|
|
1370
|
+
const outputPath = import_node_path9.default.join(reportsDir, "dupes.md");
|
|
1068
1371
|
const dupes = await runDupeFinder(rootDir, { output: outputPath });
|
|
1069
1372
|
dupeCount = dupes.length;
|
|
1070
1373
|
if (dupeCount > 0) dupesReportFile = "dupes.md";
|
|
1071
1374
|
}
|
|
1072
1375
|
if (modules.includes("circular")) {
|
|
1073
|
-
const spinner = createSpinner(
|
|
1376
|
+
const spinner = createSpinner(import_chalk7.default.cyan("Analysing circular imports\u2026"));
|
|
1074
1377
|
const cycles = detectCycles(graph);
|
|
1075
1378
|
circularCount = cycles.length;
|
|
1076
|
-
spinner.succeed(
|
|
1379
|
+
spinner.succeed(import_chalk7.default.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
|
|
1077
1380
|
if (circularCount > 0) {
|
|
1078
1381
|
circularReportFile = "circular.txt";
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1382
|
+
const rel = (p) => import_node_path9.default.relative(rootDir, p).replaceAll("\\", "/");
|
|
1383
|
+
const cycleLines = [
|
|
1384
|
+
"========================================",
|
|
1385
|
+
" CIRCULAR DEPENDENCIES",
|
|
1386
|
+
` Cycles found: ${cycles.length}`,
|
|
1387
|
+
"========================================",
|
|
1388
|
+
""
|
|
1389
|
+
];
|
|
1390
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
1391
|
+
const cycle = cycles[i];
|
|
1392
|
+
cycleLines.push(`Cycle ${i + 1} (${cycle.length} files):`);
|
|
1393
|
+
for (let j = 0; j < cycle.length; j++) {
|
|
1394
|
+
const arrow = j < cycle.length - 1 ? " \u2192 " : " \u2192 ";
|
|
1395
|
+
cycleLines.push(` ${rel(cycle[j])}`);
|
|
1396
|
+
}
|
|
1397
|
+
cycleLines.push(` \u21BB ${rel(cycle[0])} (back to start)`);
|
|
1398
|
+
cycleLines.push("");
|
|
1399
|
+
}
|
|
1400
|
+
writeReport(reportsDir, circularReportFile, cycleLines.join("\n"));
|
|
1081
1401
|
}
|
|
1082
1402
|
}
|
|
1083
1403
|
if (modules.includes("deps")) {
|
|
1084
|
-
const outputPath =
|
|
1404
|
+
const outputPath = import_node_path9.default.join(reportsDir, "deps.md");
|
|
1085
1405
|
const issues = await runDepCheck({ cwd: rootDir, output: outputPath });
|
|
1086
1406
|
unusedPkgCount = issues.filter((i) => i.type === "unused").length;
|
|
1087
1407
|
if (issues.length > 0) depsReportFile = "deps.md";
|
|
1088
1408
|
}
|
|
1409
|
+
if (modules.includes("assets")) {
|
|
1410
|
+
const outputPath = import_node_path9.default.join(reportsDir, "assets.md");
|
|
1411
|
+
const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
|
|
1412
|
+
unusedAssetCount = unusedAssets.length;
|
|
1413
|
+
if (unusedAssetCount > 0) assetsReportFile = "assets.md";
|
|
1414
|
+
}
|
|
1089
1415
|
if (modules.includes("health")) {
|
|
1090
|
-
const outputPath =
|
|
1416
|
+
const outputPath = import_node_path9.default.join(reportsDir, "health-report.md");
|
|
1091
1417
|
await runHealthReport(rootDir, { output: outputPath });
|
|
1092
1418
|
}
|
|
1093
1419
|
if (opts.html) {
|
|
1094
|
-
const htmlPath =
|
|
1420
|
+
const htmlPath = import_node_path9.default.join(reportsDir, "code_health.html");
|
|
1095
1421
|
writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount);
|
|
1096
|
-
console.log(
|
|
1422
|
+
console.log(import_chalk7.default.cyan(` HTML report written to ${htmlPath}`));
|
|
1097
1423
|
}
|
|
1098
1424
|
console.log();
|
|
1099
|
-
console.log(
|
|
1425
|
+
console.log(import_chalk7.default.bold("Summary"));
|
|
1100
1426
|
console.log();
|
|
1101
|
-
const table = new
|
|
1102
|
-
head: [
|
|
1427
|
+
const table = new import_cli_table36.default({
|
|
1428
|
+
head: [import_chalk7.default.bold("Check"), import_chalk7.default.bold("Found"), import_chalk7.default.bold("Output File")],
|
|
1103
1429
|
style: { head: [], border: [] }
|
|
1104
1430
|
});
|
|
1105
|
-
const fmt = (n) => n > 0 ?
|
|
1431
|
+
const fmt = (n) => n > 0 ? import_chalk7.default.yellow(String(n)) : import_chalk7.default.green("0");
|
|
1106
1432
|
table.push(
|
|
1107
|
-
["Dead
|
|
1433
|
+
["Dead Code (files + exports)", fmt(deadFileCount), deadReportFile || "\u2014"],
|
|
1434
|
+
["Circular Dependencies", fmt(circularCount), circularReportFile || "\u2014"],
|
|
1108
1435
|
["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
|
|
1109
1436
|
["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
|
|
1110
|
-
["
|
|
1437
|
+
["Unused Assets", fmt(unusedAssetCount), assetsReportFile || "\u2014"]
|
|
1111
1438
|
);
|
|
1112
1439
|
console.log(table.toString());
|
|
1113
1440
|
console.log();
|
|
1114
1441
|
if (opts.delete && deadFilePaths.length > 0) {
|
|
1115
|
-
console.log(
|
|
1442
|
+
console.log(import_chalk7.default.yellow(`Dead files (${deadFilePaths.length}):`));
|
|
1116
1443
|
for (const f of deadFilePaths) {
|
|
1117
|
-
console.log(
|
|
1444
|
+
console.log(import_chalk7.default.dim(` ${import_node_path9.default.relative(rootDir, f)}`));
|
|
1118
1445
|
}
|
|
1119
1446
|
console.log();
|
|
1120
1447
|
if (!opts.ci) {
|
|
1121
1448
|
const confirmed = await confirmPrompt("Delete these files? (y/N) ");
|
|
1122
1449
|
if (confirmed) {
|
|
1123
1450
|
for (const f of deadFilePaths) {
|
|
1124
|
-
|
|
1451
|
+
import_node_fs9.default.rmSync(f, { force: true });
|
|
1125
1452
|
}
|
|
1126
|
-
console.log(
|
|
1453
|
+
console.log(import_chalk7.default.green(` Deleted ${deadFilePaths.length} file(s).`));
|
|
1127
1454
|
} else {
|
|
1128
|
-
console.log(
|
|
1455
|
+
console.log(import_chalk7.default.dim(" Skipped."));
|
|
1129
1456
|
}
|
|
1130
1457
|
}
|
|
1131
1458
|
}
|
|
1132
1459
|
if (opts.ci) {
|
|
1133
|
-
const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0;
|
|
1460
|
+
const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
|
|
1134
1461
|
if (hasIssues) process.exit(1);
|
|
1135
1462
|
}
|
|
1136
1463
|
}
|
|
@@ -1141,7 +1468,7 @@ function resolveModules(only) {
|
|
|
1141
1468
|
}
|
|
1142
1469
|
function loadPackageJson2(dir) {
|
|
1143
1470
|
try {
|
|
1144
|
-
return JSON.parse(
|
|
1471
|
+
return JSON.parse(import_node_fs9.default.readFileSync(import_node_path9.default.join(dir, "package.json"), "utf-8"));
|
|
1145
1472
|
} catch {
|
|
1146
1473
|
return null;
|
|
1147
1474
|
}
|
|
@@ -1162,7 +1489,7 @@ function writeHtmlReport(outputPath, rootDir, deadFiles, circularCount, dupeCoun
|
|
|
1162
1489
|
["Circular Dependencies", String(circularCount)],
|
|
1163
1490
|
["Unused Packages", String(unusedPkgCount)]
|
|
1164
1491
|
].map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`).join("\n");
|
|
1165
|
-
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${
|
|
1492
|
+
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${import_node_path9.default.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
|
|
1166
1493
|
const html = `<!DOCTYPE html>
|
|
1167
1494
|
<html lang="en">
|
|
1168
1495
|
<head>
|
|
@@ -1189,7 +1516,7 @@ ${rows}
|
|
|
1189
1516
|
${deadList}
|
|
1190
1517
|
</body>
|
|
1191
1518
|
</html>`;
|
|
1192
|
-
|
|
1193
|
-
|
|
1519
|
+
import_node_fs9.default.mkdirSync(import_node_path9.default.dirname(outputPath), { recursive: true });
|
|
1520
|
+
import_node_fs9.default.writeFileSync(outputPath, html, "utf-8");
|
|
1194
1521
|
}
|
|
1195
1522
|
//# sourceMappingURL=cli.cjs.map
|