prunify 0.1.1 → 0.1.2
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 +294 -68
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +294 -68
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
5
|
+
import chalk7 from "chalk";
|
|
6
|
+
import Table6 from "cli-table3";
|
|
7
|
+
import fs9 from "fs";
|
|
8
|
+
import path9 from "path";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import readline from "readline";
|
|
11
11
|
|
|
@@ -57,7 +57,14 @@ var DEFAULT_IGNORE = [
|
|
|
57
57
|
"coverage",
|
|
58
58
|
"coverage/**",
|
|
59
59
|
"**/*.test.ts",
|
|
60
|
+
"**/*.test.tsx",
|
|
61
|
+
"**/*.test.js",
|
|
62
|
+
"**/*.test.jsx",
|
|
60
63
|
"**/*.spec.ts",
|
|
64
|
+
"**/*.spec.tsx",
|
|
65
|
+
"**/*.spec.js",
|
|
66
|
+
"**/*.spec.jsx",
|
|
67
|
+
"**/*.stories.ts",
|
|
61
68
|
"**/*.stories.tsx",
|
|
62
69
|
"**/*.d.ts"
|
|
63
70
|
];
|
|
@@ -202,14 +209,19 @@ function buildGraph(files, getImports) {
|
|
|
202
209
|
function findEntryPoints(rootDir, packageJson) {
|
|
203
210
|
const entries = [
|
|
204
211
|
...resolveNextJsEntries(rootDir),
|
|
205
|
-
...resolvePkgFieldEntries(rootDir, packageJson)
|
|
212
|
+
...resolvePkgFieldEntries(rootDir, packageJson),
|
|
213
|
+
...resolveFallbackEntries(rootDir)
|
|
206
214
|
];
|
|
207
|
-
if (entries.length === 0) {
|
|
208
|
-
const fallback = resolveFallbackEntry(rootDir);
|
|
209
|
-
if (fallback) entries.push(fallback);
|
|
210
|
-
}
|
|
211
215
|
return [...new Set(entries)];
|
|
212
216
|
}
|
|
217
|
+
function findRootFiles(graph) {
|
|
218
|
+
const imported = /* @__PURE__ */ new Set();
|
|
219
|
+
for (const deps of graph.values()) {
|
|
220
|
+
for (const dep of deps) imported.add(dep);
|
|
221
|
+
}
|
|
222
|
+
const roots = [...graph.keys()].filter((f) => !imported.has(f));
|
|
223
|
+
return roots.length > 0 ? roots : [...graph.keys()];
|
|
224
|
+
}
|
|
213
225
|
function runDFS(graph, entryPoints) {
|
|
214
226
|
const visited = /* @__PURE__ */ new Set();
|
|
215
227
|
const stack = [...entryPoints];
|
|
@@ -236,11 +248,11 @@ function detectCycles(graph) {
|
|
|
236
248
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
237
249
|
const visited = /* @__PURE__ */ new Set();
|
|
238
250
|
const inStack = /* @__PURE__ */ new Set();
|
|
239
|
-
const
|
|
251
|
+
const path10 = [];
|
|
240
252
|
const acc = { seenKeys, cycles };
|
|
241
253
|
for (const start of graph.keys()) {
|
|
242
254
|
if (!visited.has(start)) {
|
|
243
|
-
dfsForCycles(start, graph, visited, inStack,
|
|
255
|
+
dfsForCycles(start, graph, visited, inStack, path10, acc);
|
|
244
256
|
}
|
|
245
257
|
}
|
|
246
258
|
return cycles;
|
|
@@ -265,18 +277,31 @@ function resolvePkgFieldEntries(rootDir, packageJson) {
|
|
|
265
277
|
}
|
|
266
278
|
return entries;
|
|
267
279
|
}
|
|
268
|
-
function
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
280
|
+
function resolveFallbackEntries(rootDir) {
|
|
281
|
+
const candidates = [
|
|
282
|
+
"src/main.ts",
|
|
283
|
+
"src/main.tsx",
|
|
284
|
+
"src/main.js",
|
|
285
|
+
"src/main.jsx",
|
|
286
|
+
"src/index.ts",
|
|
287
|
+
"src/index.tsx",
|
|
288
|
+
"src/index.js",
|
|
289
|
+
"src/index.jsx",
|
|
290
|
+
"src/App.ts",
|
|
291
|
+
"src/App.tsx",
|
|
292
|
+
"src/App.js",
|
|
293
|
+
"src/App.jsx",
|
|
294
|
+
"index.ts",
|
|
295
|
+
"index.tsx",
|
|
296
|
+
"index.js",
|
|
297
|
+
"index.jsx"
|
|
298
|
+
];
|
|
299
|
+
return candidates.map((rel) => path3.join(rootDir, rel)).filter((abs) => fs3.existsSync(abs));
|
|
275
300
|
}
|
|
276
301
|
function mkFrame(node, graph) {
|
|
277
302
|
return { node, neighbors: (graph.get(node) ?? /* @__PURE__ */ new Set()).values(), entered: false };
|
|
278
303
|
}
|
|
279
|
-
function dfsForCycles(start, graph, visited, inStack,
|
|
304
|
+
function dfsForCycles(start, graph, visited, inStack, path10, acc) {
|
|
280
305
|
const stack = [mkFrame(start, graph)];
|
|
281
306
|
while (stack.length > 0) {
|
|
282
307
|
const frame = stack.at(-1);
|
|
@@ -288,30 +313,30 @@ function dfsForCycles(start, graph, visited, inStack, path9, acc) {
|
|
|
288
313
|
}
|
|
289
314
|
frame.entered = true;
|
|
290
315
|
inStack.add(frame.node);
|
|
291
|
-
|
|
316
|
+
path10.push(frame.node);
|
|
292
317
|
}
|
|
293
318
|
const { done, value: neighbor } = frame.neighbors.next();
|
|
294
319
|
if (done) {
|
|
295
320
|
stack.pop();
|
|
296
|
-
|
|
321
|
+
path10.pop();
|
|
297
322
|
inStack.delete(frame.node);
|
|
298
323
|
visited.add(frame.node);
|
|
299
324
|
} else {
|
|
300
|
-
handleCycleNeighbor(neighbor, stack,
|
|
325
|
+
handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph);
|
|
301
326
|
}
|
|
302
327
|
}
|
|
303
328
|
}
|
|
304
|
-
function handleCycleNeighbor(neighbor, stack,
|
|
329
|
+
function handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph) {
|
|
305
330
|
if (inStack.has(neighbor)) {
|
|
306
|
-
recordCycle(neighbor,
|
|
331
|
+
recordCycle(neighbor, path10, acc);
|
|
307
332
|
} else if (!visited.has(neighbor)) {
|
|
308
333
|
stack.push(mkFrame(neighbor, graph));
|
|
309
334
|
}
|
|
310
335
|
}
|
|
311
|
-
function recordCycle(cycleStart,
|
|
312
|
-
const idx =
|
|
336
|
+
function recordCycle(cycleStart, path10, acc) {
|
|
337
|
+
const idx = path10.indexOf(cycleStart);
|
|
313
338
|
if (idx === -1) return;
|
|
314
|
-
const cycle = normalizeCycle(
|
|
339
|
+
const cycle = normalizeCycle(path10.slice(idx));
|
|
315
340
|
const key = cycle.join("\0");
|
|
316
341
|
if (!acc.seenKeys.has(key)) {
|
|
317
342
|
acc.seenKeys.add(key);
|
|
@@ -467,7 +492,7 @@ function ensureDir(dir) {
|
|
|
467
492
|
// src/modules/dead-code.ts
|
|
468
493
|
function runDeadCodeModule(project, graph, entryPoints, rootDir) {
|
|
469
494
|
const allFiles = [...graph.keys()];
|
|
470
|
-
const effectiveEntries = entryPoints.length > 0 ? entryPoints :
|
|
495
|
+
const effectiveEntries = entryPoints.length > 0 ? entryPoints : findRootFiles(graph);
|
|
471
496
|
const liveFiles = runDFS(graph, effectiveEntries);
|
|
472
497
|
const deadFiles = allFiles.filter((f) => !liveFiles.has(f));
|
|
473
498
|
const deadSet = new Set(deadFiles);
|
|
@@ -967,26 +992,218 @@ async function runHealthReport(dir, opts) {
|
|
|
967
992
|
);
|
|
968
993
|
}
|
|
969
994
|
|
|
995
|
+
// src/modules/assets.ts
|
|
996
|
+
import fs8 from "fs";
|
|
997
|
+
import path8 from "path";
|
|
998
|
+
import ora6 from "ora";
|
|
999
|
+
import chalk6 from "chalk";
|
|
1000
|
+
import Table5 from "cli-table3";
|
|
1001
|
+
var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1002
|
+
".png",
|
|
1003
|
+
".jpg",
|
|
1004
|
+
".jpeg",
|
|
1005
|
+
".gif",
|
|
1006
|
+
".svg",
|
|
1007
|
+
".webp",
|
|
1008
|
+
".avif",
|
|
1009
|
+
".ico",
|
|
1010
|
+
".bmp",
|
|
1011
|
+
".woff",
|
|
1012
|
+
".woff2",
|
|
1013
|
+
".ttf",
|
|
1014
|
+
".eot",
|
|
1015
|
+
".otf",
|
|
1016
|
+
".mp4",
|
|
1017
|
+
".webm",
|
|
1018
|
+
".ogg",
|
|
1019
|
+
".mp3",
|
|
1020
|
+
".wav",
|
|
1021
|
+
".pdf"
|
|
1022
|
+
]);
|
|
1023
|
+
var SOURCE_PATTERNS2 = [
|
|
1024
|
+
"**/*.ts",
|
|
1025
|
+
"**/*.tsx",
|
|
1026
|
+
"**/*.js",
|
|
1027
|
+
"**/*.jsx",
|
|
1028
|
+
"**/*.css",
|
|
1029
|
+
"**/*.scss",
|
|
1030
|
+
"**/*.sass",
|
|
1031
|
+
"**/*.less",
|
|
1032
|
+
"**/*.html",
|
|
1033
|
+
"**/*.json"
|
|
1034
|
+
];
|
|
1035
|
+
var SOURCE_IGNORE = [
|
|
1036
|
+
"node_modules",
|
|
1037
|
+
"node_modules/**",
|
|
1038
|
+
"dist",
|
|
1039
|
+
"dist/**",
|
|
1040
|
+
".next",
|
|
1041
|
+
".next/**",
|
|
1042
|
+
"coverage",
|
|
1043
|
+
"coverage/**",
|
|
1044
|
+
"public",
|
|
1045
|
+
"public/**"
|
|
1046
|
+
];
|
|
1047
|
+
function runAssetCheckModule(rootDir) {
|
|
1048
|
+
const publicDir = path8.join(rootDir, "public");
|
|
1049
|
+
if (!fs8.existsSync(publicDir)) {
|
|
1050
|
+
return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
|
|
1051
|
+
}
|
|
1052
|
+
const assets = collectAssets(publicDir);
|
|
1053
|
+
if (assets.length === 0) {
|
|
1054
|
+
return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
|
|
1055
|
+
}
|
|
1056
|
+
const sourceFiles = glob(rootDir, SOURCE_PATTERNS2, SOURCE_IGNORE);
|
|
1057
|
+
const sourceContent = sourceFiles.reduce((acc, f) => {
|
|
1058
|
+
try {
|
|
1059
|
+
return acc + fs8.readFileSync(f, "utf-8") + "\n";
|
|
1060
|
+
} catch {
|
|
1061
|
+
return acc;
|
|
1062
|
+
}
|
|
1063
|
+
}, "");
|
|
1064
|
+
const unused = [];
|
|
1065
|
+
for (const assetAbs of assets) {
|
|
1066
|
+
const fileName = path8.basename(assetAbs);
|
|
1067
|
+
const relFromPublic = "/" + path8.relative(publicDir, assetAbs).replaceAll("\\", "/");
|
|
1068
|
+
const referenced = sourceContent.includes(fileName) || sourceContent.includes(relFromPublic);
|
|
1069
|
+
if (!referenced) {
|
|
1070
|
+
unused.push({
|
|
1071
|
+
filePath: assetAbs,
|
|
1072
|
+
relativePath: path8.relative(rootDir, assetAbs).replaceAll("\\", "/"),
|
|
1073
|
+
sizeBytes: getFileSize2(assetAbs)
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const report = buildAssetReport(unused, assets.length, rootDir);
|
|
1078
|
+
return { unusedAssets: unused, totalAssets: assets.length, report };
|
|
1079
|
+
}
|
|
1080
|
+
async function runAssetCheck(rootDir, opts) {
|
|
1081
|
+
const publicDir = path8.join(rootDir, "public");
|
|
1082
|
+
if (!fs8.existsSync(publicDir)) {
|
|
1083
|
+
console.log(chalk6.dim(" No public/ folder found \u2014 skipping asset check"));
|
|
1084
|
+
return [];
|
|
1085
|
+
}
|
|
1086
|
+
const spinner = ora6(chalk6.cyan("Scanning public/ for unused assets\u2026")).start();
|
|
1087
|
+
try {
|
|
1088
|
+
const result = runAssetCheckModule(rootDir);
|
|
1089
|
+
spinner.succeed(
|
|
1090
|
+
chalk6.green(
|
|
1091
|
+
`Asset scan complete \u2014 ${result.unusedAssets.length} unused / ${result.totalAssets} total`
|
|
1092
|
+
)
|
|
1093
|
+
);
|
|
1094
|
+
if (result.unusedAssets.length === 0) {
|
|
1095
|
+
console.log(chalk6.green(" All public assets are referenced in source."));
|
|
1096
|
+
return [];
|
|
1097
|
+
}
|
|
1098
|
+
const table = new Table5({ head: ["Asset", "Size"] });
|
|
1099
|
+
for (const asset of result.unusedAssets) {
|
|
1100
|
+
const kb = (asset.sizeBytes / 1024).toFixed(1);
|
|
1101
|
+
table.push([chalk6.gray(asset.relativePath), `${kb} KB`]);
|
|
1102
|
+
}
|
|
1103
|
+
console.log(table.toString());
|
|
1104
|
+
if (opts.output) {
|
|
1105
|
+
writeMarkdown(
|
|
1106
|
+
{
|
|
1107
|
+
title: "Unused Assets Report",
|
|
1108
|
+
summary: `${result.unusedAssets.length} unused asset(s) found in public/`,
|
|
1109
|
+
sections: [
|
|
1110
|
+
{
|
|
1111
|
+
title: "Unused Assets",
|
|
1112
|
+
headers: ["Asset", "Size (KB)"],
|
|
1113
|
+
rows: result.unusedAssets.map((a) => [
|
|
1114
|
+
a.relativePath,
|
|
1115
|
+
(a.sizeBytes / 1024).toFixed(1)
|
|
1116
|
+
])
|
|
1117
|
+
}
|
|
1118
|
+
],
|
|
1119
|
+
generatedAt: /* @__PURE__ */ new Date()
|
|
1120
|
+
},
|
|
1121
|
+
opts.output
|
|
1122
|
+
);
|
|
1123
|
+
console.log(chalk6.cyan(` Report written to ${opts.output}`));
|
|
1124
|
+
}
|
|
1125
|
+
return result.unusedAssets;
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
spinner.fail(chalk6.red("Asset scan failed"));
|
|
1128
|
+
throw err;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function collectAssets(dir) {
|
|
1132
|
+
const results = [];
|
|
1133
|
+
function walk(current) {
|
|
1134
|
+
let entries;
|
|
1135
|
+
try {
|
|
1136
|
+
entries = fs8.readdirSync(current, { withFileTypes: true });
|
|
1137
|
+
} catch {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
for (const entry of entries) {
|
|
1141
|
+
const full = path8.join(current, entry.name);
|
|
1142
|
+
if (entry.isDirectory()) {
|
|
1143
|
+
walk(full);
|
|
1144
|
+
} else if (entry.isFile() && ASSET_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())) {
|
|
1145
|
+
results.push(full);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
walk(dir);
|
|
1150
|
+
return results;
|
|
1151
|
+
}
|
|
1152
|
+
function getFileSize2(filePath) {
|
|
1153
|
+
try {
|
|
1154
|
+
return fs8.statSync(filePath).size;
|
|
1155
|
+
} catch {
|
|
1156
|
+
return 0;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function buildAssetReport(unused, totalAssets, rootDir) {
|
|
1160
|
+
const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0);
|
|
1161
|
+
const totalKb = (totalBytes / 1024).toFixed(1);
|
|
1162
|
+
const lines = [
|
|
1163
|
+
"========================================",
|
|
1164
|
+
" UNUSED ASSETS REPORT",
|
|
1165
|
+
` Total assets : ${totalAssets}`,
|
|
1166
|
+
` Unused assets : ${unused.length}`,
|
|
1167
|
+
` Recoverable : ~${totalKb} KB`,
|
|
1168
|
+
"========================================",
|
|
1169
|
+
""
|
|
1170
|
+
];
|
|
1171
|
+
if (unused.length === 0) {
|
|
1172
|
+
lines.push(" All public assets are referenced in source.", "");
|
|
1173
|
+
return lines.join("\n");
|
|
1174
|
+
}
|
|
1175
|
+
lines.push("\u2500\u2500 UNUSED ASSETS \u2500\u2500", "");
|
|
1176
|
+
for (const asset of unused) {
|
|
1177
|
+
lines.push(
|
|
1178
|
+
`UNUSED \u2014 ${asset.relativePath}`,
|
|
1179
|
+
`Size: ~${(asset.sizeBytes / 1024).toFixed(1)} KB`,
|
|
1180
|
+
`Action: Safe to delete if not served directly via URL`,
|
|
1181
|
+
""
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
return lines.join("\n");
|
|
1185
|
+
}
|
|
1186
|
+
|
|
970
1187
|
// src/cli.ts
|
|
971
1188
|
function readPkgVersion() {
|
|
972
1189
|
try {
|
|
973
1190
|
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
974
|
-
const dir =
|
|
975
|
-
const pkgPath =
|
|
976
|
-
return JSON.parse(
|
|
1191
|
+
const dir = path9.dirname(fileURLToPath(import.meta.url));
|
|
1192
|
+
const pkgPath = path9.resolve(dir, "..", "package.json");
|
|
1193
|
+
return JSON.parse(fs9.readFileSync(pkgPath, "utf-8")).version;
|
|
977
1194
|
}
|
|
978
1195
|
} catch {
|
|
979
1196
|
}
|
|
980
1197
|
try {
|
|
981
1198
|
const dir = globalThis.__dirname ?? __dirname;
|
|
982
|
-
const pkgPath =
|
|
983
|
-
return JSON.parse(
|
|
1199
|
+
const pkgPath = path9.resolve(dir, "..", "package.json");
|
|
1200
|
+
return JSON.parse(fs9.readFileSync(pkgPath, "utf-8")).version;
|
|
984
1201
|
} catch {
|
|
985
1202
|
return "0.0.0";
|
|
986
1203
|
}
|
|
987
1204
|
}
|
|
988
1205
|
var PKG_VERSION = readPkgVersion();
|
|
989
|
-
var ALL_MODULES = ["dead-code", "dupes", "circular", "deps"];
|
|
1206
|
+
var ALL_MODULES = ["dead-code", "dupes", "circular", "deps", "assets"];
|
|
990
1207
|
var program = new Command();
|
|
991
1208
|
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(
|
|
992
1209
|
"--ignore <pattern>",
|
|
@@ -996,29 +1213,29 @@ program.name("prunify").description("npm run clean. ship with confidence.").vers
|
|
|
996
1213
|
).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);
|
|
997
1214
|
program.parse();
|
|
998
1215
|
async function main(opts) {
|
|
999
|
-
const rootDir =
|
|
1000
|
-
if (!
|
|
1001
|
-
console.error(
|
|
1002
|
-
console.error(
|
|
1216
|
+
const rootDir = path9.resolve(opts.dir);
|
|
1217
|
+
if (!fs9.existsSync(path9.join(rootDir, "package.json"))) {
|
|
1218
|
+
console.error(chalk7.red(`\u2717 No package.json found in ${rootDir}`));
|
|
1219
|
+
console.error(chalk7.dim(" Use --dir <path> to point to your project root."));
|
|
1003
1220
|
process.exit(1);
|
|
1004
1221
|
}
|
|
1005
1222
|
const modules = resolveModules(opts.only);
|
|
1006
1223
|
console.log();
|
|
1007
|
-
console.log(
|
|
1224
|
+
console.log(chalk7.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
|
|
1008
1225
|
console.log();
|
|
1009
|
-
const parseSpinner = createSpinner(
|
|
1226
|
+
const parseSpinner = createSpinner(chalk7.cyan("Parsing codebase\u2026"));
|
|
1010
1227
|
const files = discoverFiles(rootDir, opts.ignore);
|
|
1011
|
-
parseSpinner.succeed(
|
|
1012
|
-
const graphSpinner = createSpinner(
|
|
1228
|
+
parseSpinner.succeed(chalk7.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
|
|
1229
|
+
const graphSpinner = createSpinner(chalk7.cyan("Building import graph\u2026"));
|
|
1013
1230
|
const project = buildProject(files);
|
|
1014
1231
|
const graph = buildGraph(files, (f) => {
|
|
1015
1232
|
const sf = project.getSourceFile(f);
|
|
1016
1233
|
return sf ? getImportsForFile(sf) : [];
|
|
1017
1234
|
});
|
|
1018
1235
|
const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
|
|
1019
|
-
graphSpinner.succeed(
|
|
1236
|
+
graphSpinner.succeed(chalk7.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
|
|
1020
1237
|
const packageJson = loadPackageJson2(rootDir);
|
|
1021
|
-
const entryPoints = opts.entry ? [
|
|
1238
|
+
const entryPoints = opts.entry ? [path9.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
|
|
1022
1239
|
const reportsDir = ensureReportsDir(rootDir, opts.out);
|
|
1023
1240
|
appendToGitignore(rootDir);
|
|
1024
1241
|
console.log();
|
|
@@ -1026,33 +1243,35 @@ async function main(opts) {
|
|
|
1026
1243
|
let dupeCount = 0;
|
|
1027
1244
|
let unusedPkgCount = 0;
|
|
1028
1245
|
let circularCount = 0;
|
|
1246
|
+
let unusedAssetCount = 0;
|
|
1029
1247
|
let deadReportFile = "";
|
|
1030
1248
|
let dupesReportFile = "";
|
|
1031
1249
|
let depsReportFile = "";
|
|
1032
1250
|
let circularReportFile = "";
|
|
1251
|
+
let assetsReportFile = "";
|
|
1033
1252
|
const deadFilePaths = [];
|
|
1034
1253
|
if (modules.includes("dead-code")) {
|
|
1035
|
-
const spinner = createSpinner(
|
|
1254
|
+
const spinner = createSpinner(chalk7.cyan("Analysing dead code\u2026"));
|
|
1036
1255
|
const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
|
|
1037
1256
|
deadFileCount = result.deadFiles.length + result.deadExports.length;
|
|
1038
1257
|
deadFilePaths.push(...result.deadFiles);
|
|
1039
|
-
spinner.succeed(
|
|
1258
|
+
spinner.succeed(chalk7.green(`Dead code analysis complete \u2014 ${deadFileCount} item(s) found`));
|
|
1040
1259
|
if (result.report) {
|
|
1041
1260
|
deadReportFile = "dead-code.txt";
|
|
1042
1261
|
writeReport(reportsDir, deadReportFile, result.report);
|
|
1043
1262
|
}
|
|
1044
1263
|
}
|
|
1045
1264
|
if (modules.includes("dupes")) {
|
|
1046
|
-
const outputPath =
|
|
1265
|
+
const outputPath = path9.join(reportsDir, "dupes.md");
|
|
1047
1266
|
const dupes = await runDupeFinder(rootDir, { output: outputPath });
|
|
1048
1267
|
dupeCount = dupes.length;
|
|
1049
1268
|
if (dupeCount > 0) dupesReportFile = "dupes.md";
|
|
1050
1269
|
}
|
|
1051
1270
|
if (modules.includes("circular")) {
|
|
1052
|
-
const spinner = createSpinner(
|
|
1271
|
+
const spinner = createSpinner(chalk7.cyan("Analysing circular imports\u2026"));
|
|
1053
1272
|
const cycles = detectCycles(graph);
|
|
1054
1273
|
circularCount = cycles.length;
|
|
1055
|
-
spinner.succeed(
|
|
1274
|
+
spinner.succeed(chalk7.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
|
|
1056
1275
|
if (circularCount > 0) {
|
|
1057
1276
|
circularReportFile = "circular.txt";
|
|
1058
1277
|
const cycleText = cycles.map((c, i) => `Cycle ${i + 1}: ${c.join(" \u2192 ")}`).join("\n");
|
|
@@ -1060,56 +1279,63 @@ async function main(opts) {
|
|
|
1060
1279
|
}
|
|
1061
1280
|
}
|
|
1062
1281
|
if (modules.includes("deps")) {
|
|
1063
|
-
const outputPath =
|
|
1282
|
+
const outputPath = path9.join(reportsDir, "deps.md");
|
|
1064
1283
|
const issues = await runDepCheck({ cwd: rootDir, output: outputPath });
|
|
1065
1284
|
unusedPkgCount = issues.filter((i) => i.type === "unused").length;
|
|
1066
1285
|
if (issues.length > 0) depsReportFile = "deps.md";
|
|
1067
1286
|
}
|
|
1287
|
+
if (modules.includes("assets")) {
|
|
1288
|
+
const outputPath = path9.join(reportsDir, "assets.md");
|
|
1289
|
+
const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
|
|
1290
|
+
unusedAssetCount = unusedAssets.length;
|
|
1291
|
+
if (unusedAssetCount > 0) assetsReportFile = "assets.md";
|
|
1292
|
+
}
|
|
1068
1293
|
if (modules.includes("health")) {
|
|
1069
|
-
const outputPath =
|
|
1294
|
+
const outputPath = path9.join(reportsDir, "health-report.md");
|
|
1070
1295
|
await runHealthReport(rootDir, { output: outputPath });
|
|
1071
1296
|
}
|
|
1072
1297
|
if (opts.html) {
|
|
1073
|
-
const htmlPath =
|
|
1298
|
+
const htmlPath = path9.join(reportsDir, "code_health.html");
|
|
1074
1299
|
writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount);
|
|
1075
|
-
console.log(
|
|
1300
|
+
console.log(chalk7.cyan(` HTML report written to ${htmlPath}`));
|
|
1076
1301
|
}
|
|
1077
1302
|
console.log();
|
|
1078
|
-
console.log(
|
|
1303
|
+
console.log(chalk7.bold("Summary"));
|
|
1079
1304
|
console.log();
|
|
1080
|
-
const table = new
|
|
1081
|
-
head: [
|
|
1305
|
+
const table = new Table6({
|
|
1306
|
+
head: [chalk7.bold("Check"), chalk7.bold("Found"), chalk7.bold("Output File")],
|
|
1082
1307
|
style: { head: [], border: [] }
|
|
1083
1308
|
});
|
|
1084
|
-
const fmt = (n) => n > 0 ?
|
|
1309
|
+
const fmt = (n) => n > 0 ? chalk7.yellow(String(n)) : chalk7.green("0");
|
|
1085
1310
|
table.push(
|
|
1086
1311
|
["Dead Files / Exports", fmt(deadFileCount), deadReportFile || "\u2014"],
|
|
1087
1312
|
["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
|
|
1088
1313
|
["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
|
|
1089
|
-
["Circular Deps", fmt(circularCount), circularReportFile || "\u2014"]
|
|
1314
|
+
["Circular Deps", fmt(circularCount), circularReportFile || "\u2014"],
|
|
1315
|
+
["Unused Assets", fmt(unusedAssetCount), assetsReportFile || "\u2014"]
|
|
1090
1316
|
);
|
|
1091
1317
|
console.log(table.toString());
|
|
1092
1318
|
console.log();
|
|
1093
1319
|
if (opts.delete && deadFilePaths.length > 0) {
|
|
1094
|
-
console.log(
|
|
1320
|
+
console.log(chalk7.yellow(`Dead files (${deadFilePaths.length}):`));
|
|
1095
1321
|
for (const f of deadFilePaths) {
|
|
1096
|
-
console.log(
|
|
1322
|
+
console.log(chalk7.dim(` ${path9.relative(rootDir, f)}`));
|
|
1097
1323
|
}
|
|
1098
1324
|
console.log();
|
|
1099
1325
|
if (!opts.ci) {
|
|
1100
1326
|
const confirmed = await confirmPrompt("Delete these files? (y/N) ");
|
|
1101
1327
|
if (confirmed) {
|
|
1102
1328
|
for (const f of deadFilePaths) {
|
|
1103
|
-
|
|
1329
|
+
fs9.rmSync(f, { force: true });
|
|
1104
1330
|
}
|
|
1105
|
-
console.log(
|
|
1331
|
+
console.log(chalk7.green(` Deleted ${deadFilePaths.length} file(s).`));
|
|
1106
1332
|
} else {
|
|
1107
|
-
console.log(
|
|
1333
|
+
console.log(chalk7.dim(" Skipped."));
|
|
1108
1334
|
}
|
|
1109
1335
|
}
|
|
1110
1336
|
}
|
|
1111
1337
|
if (opts.ci) {
|
|
1112
|
-
const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0;
|
|
1338
|
+
const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
|
|
1113
1339
|
if (hasIssues) process.exit(1);
|
|
1114
1340
|
}
|
|
1115
1341
|
}
|
|
@@ -1120,7 +1346,7 @@ function resolveModules(only) {
|
|
|
1120
1346
|
}
|
|
1121
1347
|
function loadPackageJson2(dir) {
|
|
1122
1348
|
try {
|
|
1123
|
-
return JSON.parse(
|
|
1349
|
+
return JSON.parse(fs9.readFileSync(path9.join(dir, "package.json"), "utf-8"));
|
|
1124
1350
|
} catch {
|
|
1125
1351
|
return null;
|
|
1126
1352
|
}
|
|
@@ -1141,7 +1367,7 @@ function writeHtmlReport(outputPath, rootDir, deadFiles, circularCount, dupeCoun
|
|
|
1141
1367
|
["Circular Dependencies", String(circularCount)],
|
|
1142
1368
|
["Unused Packages", String(unusedPkgCount)]
|
|
1143
1369
|
].map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`).join("\n");
|
|
1144
|
-
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${
|
|
1370
|
+
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${path9.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
|
|
1145
1371
|
const html = `<!DOCTYPE html>
|
|
1146
1372
|
<html lang="en">
|
|
1147
1373
|
<head>
|
|
@@ -1168,7 +1394,7 @@ ${rows}
|
|
|
1168
1394
|
${deadList}
|
|
1169
1395
|
</body>
|
|
1170
1396
|
</html>`;
|
|
1171
|
-
|
|
1172
|
-
|
|
1397
|
+
fs9.mkdirSync(path9.dirname(outputPath), { recursive: true });
|
|
1398
|
+
fs9.writeFileSync(outputPath, html, "utf-8");
|
|
1173
1399
|
}
|
|
1174
1400
|
//# sourceMappingURL=cli.js.map
|