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