prunify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -0
- package/dist/cli.cjs +1195 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1174 -0
- package/dist/cli.js.map +1 -0
- package/package.json +53 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
29
|
+
var import_cli_table35 = __toESM(require("cli-table3"), 1);
|
|
30
|
+
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
31
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
32
|
+
var import_node_url = require("url");
|
|
33
|
+
var import_node_readline = __toESM(require("readline"), 1);
|
|
34
|
+
|
|
35
|
+
// src/core/parser.ts
|
|
36
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
37
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
38
|
+
var import_ts_morph = require("ts-morph");
|
|
39
|
+
|
|
40
|
+
// src/utils/file.ts
|
|
41
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
42
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
43
|
+
var import_minimatch = require("minimatch");
|
|
44
|
+
function glob(dir, patterns, ignore = []) {
|
|
45
|
+
const results = [];
|
|
46
|
+
collect(dir, dir, patterns, ignore, results);
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
function collect(base, current, patterns, ignore, results) {
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = import_node_fs.default.readdirSync(current, { withFileTypes: true });
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const fullPath = import_node_path.default.join(current, entry.name);
|
|
58
|
+
const relativePath = import_node_path.default.relative(base, fullPath).replace(/\\/g, "/");
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
const isIgnored = ignore.some((pattern) => (0, import_minimatch.minimatch)(relativePath, pattern));
|
|
61
|
+
if (!isIgnored) collect(base, fullPath, patterns, ignore, results);
|
|
62
|
+
} else if (entry.isFile()) {
|
|
63
|
+
const isIgnored = ignore.some((pattern) => (0, import_minimatch.minimatch)(relativePath, pattern));
|
|
64
|
+
if (!isIgnored) {
|
|
65
|
+
const matches = patterns.some((pattern) => (0, import_minimatch.minimatch)(relativePath, pattern));
|
|
66
|
+
if (matches) results.push(fullPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/core/parser.ts
|
|
73
|
+
var DEFAULT_IGNORE = [
|
|
74
|
+
"node_modules",
|
|
75
|
+
"node_modules/**",
|
|
76
|
+
"dist",
|
|
77
|
+
"dist/**",
|
|
78
|
+
".next",
|
|
79
|
+
".next/**",
|
|
80
|
+
"coverage",
|
|
81
|
+
"coverage/**",
|
|
82
|
+
"**/*.test.ts",
|
|
83
|
+
"**/*.spec.ts",
|
|
84
|
+
"**/*.stories.tsx",
|
|
85
|
+
"**/*.d.ts"
|
|
86
|
+
];
|
|
87
|
+
var SOURCE_PATTERNS = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"];
|
|
88
|
+
var RESOLVE_EXTENSIONS = [
|
|
89
|
+
"",
|
|
90
|
+
".ts",
|
|
91
|
+
".tsx",
|
|
92
|
+
".js",
|
|
93
|
+
".jsx"
|
|
94
|
+
];
|
|
95
|
+
var INDEX_FILES = ["index.ts", "index.tsx", "index.js", "index.jsx"];
|
|
96
|
+
function discoverFiles(rootDir, ignore = []) {
|
|
97
|
+
return glob(rootDir, SOURCE_PATTERNS, [...DEFAULT_IGNORE, ...ignore]);
|
|
98
|
+
}
|
|
99
|
+
function buildProject(files, tsconfigPath) {
|
|
100
|
+
const resolved = tsconfigPath ?? (files.length > 0 ? findTsconfig(files[0]) : void 0);
|
|
101
|
+
const project = resolved ? new import_ts_morph.Project({ tsConfigFilePath: resolved, skipAddingFilesFromTsConfig: true }) : new import_ts_morph.Project({
|
|
102
|
+
compilerOptions: {
|
|
103
|
+
allowJs: true,
|
|
104
|
+
resolveJsonModule: true
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
project.addSourceFilesAtPaths(files);
|
|
108
|
+
return project;
|
|
109
|
+
}
|
|
110
|
+
function getImportsForFile(sourceFile) {
|
|
111
|
+
const result = /* @__PURE__ */ new Set();
|
|
112
|
+
const fileDir = import_node_path2.default.dirname(sourceFile.getFilePath());
|
|
113
|
+
const project = sourceFile.getProject();
|
|
114
|
+
const compilerOptions = project.getCompilerOptions();
|
|
115
|
+
const pathAliases = compilerOptions.paths ?? {};
|
|
116
|
+
const baseUrl = compilerOptions.baseUrl;
|
|
117
|
+
function addResolved(sf) {
|
|
118
|
+
if (!sf) return;
|
|
119
|
+
const p = import_node_path2.default.normalize(sf.getFilePath());
|
|
120
|
+
if (!p.includes(`${import_node_path2.default.sep}node_modules${import_node_path2.default.sep}`) && !p.includes("/node_modules/") && import_node_path2.default.normalize(sourceFile.getFilePath()) !== p) {
|
|
121
|
+
result.add(p);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function resolveAndAdd(specifier) {
|
|
125
|
+
if (!specifier) return;
|
|
126
|
+
const isRelative = specifier.startsWith("./") || specifier.startsWith("../");
|
|
127
|
+
if (isRelative) {
|
|
128
|
+
const p = resolveRelativePath(fileDir, specifier, project);
|
|
129
|
+
if (p) result.add(p);
|
|
130
|
+
} else if (!specifier.startsWith("node:")) {
|
|
131
|
+
const p = resolvePathAlias(specifier, pathAliases, baseUrl, project);
|
|
132
|
+
if (p) result.add(p);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
136
|
+
const sf = decl.getModuleSpecifierSourceFile();
|
|
137
|
+
sf ? addResolved(sf) : resolveAndAdd(decl.getModuleSpecifierValue());
|
|
138
|
+
}
|
|
139
|
+
for (const decl of sourceFile.getExportDeclarations()) {
|
|
140
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
141
|
+
if (!specifier) continue;
|
|
142
|
+
const sf = decl.getModuleSpecifierSourceFile();
|
|
143
|
+
sf ? addResolved(sf) : resolveAndAdd(specifier);
|
|
144
|
+
}
|
|
145
|
+
for (const call of sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression)) {
|
|
146
|
+
if (call.getExpression().getKind() !== import_ts_morph.SyntaxKind.ImportKeyword) continue;
|
|
147
|
+
const args = call.getArguments();
|
|
148
|
+
if (args.length > 0 && import_ts_morph.Node.isStringLiteral(args[0])) {
|
|
149
|
+
resolveAndAdd(args[0].getLiteralValue());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...result];
|
|
153
|
+
}
|
|
154
|
+
function resolveRelativePath(fromDir, specifier, project) {
|
|
155
|
+
const base = import_node_path2.default.resolve(fromDir, specifier);
|
|
156
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
157
|
+
const sf = project.getSourceFile(base + ext);
|
|
158
|
+
if (sf) return import_node_path2.default.normalize(sf.getFilePath());
|
|
159
|
+
}
|
|
160
|
+
for (const index of INDEX_FILES) {
|
|
161
|
+
const sf = project.getSourceFile(import_node_path2.default.join(base, index));
|
|
162
|
+
if (sf) return import_node_path2.default.normalize(sf.getFilePath());
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function resolvePathAlias(specifier, pathAliases, baseUrl, project) {
|
|
167
|
+
for (const [alias, targets] of Object.entries(pathAliases)) {
|
|
168
|
+
const match = matchAlias(alias, specifier);
|
|
169
|
+
if (!match) continue;
|
|
170
|
+
const capture = match[1] ?? "";
|
|
171
|
+
for (const target of targets) {
|
|
172
|
+
const resolved = target.replaceAll("*", capture);
|
|
173
|
+
const absolute = baseUrl ? import_node_path2.default.resolve(baseUrl, resolved) : import_node_path2.default.resolve(resolved);
|
|
174
|
+
const hit = tryResolveAbsolute(absolute, project);
|
|
175
|
+
if (hit) return hit;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function matchAlias(alias, specifier) {
|
|
181
|
+
const escaped = alias.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
182
|
+
const pattern = escaped.replaceAll("*", "(.*)");
|
|
183
|
+
return new RegExp(`^${pattern}$`).exec(specifier);
|
|
184
|
+
}
|
|
185
|
+
function tryResolveAbsolute(absolute, project) {
|
|
186
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
187
|
+
const sf = project.getSourceFile(absolute + ext);
|
|
188
|
+
if (sf) return import_node_path2.default.normalize(sf.getFilePath());
|
|
189
|
+
}
|
|
190
|
+
for (const index of INDEX_FILES) {
|
|
191
|
+
const sf = project.getSourceFile(import_node_path2.default.join(absolute, index));
|
|
192
|
+
if (sf) return import_node_path2.default.normalize(sf.getFilePath());
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
function findTsconfig(fromFile) {
|
|
197
|
+
let dir = import_node_path2.default.dirname(fromFile);
|
|
198
|
+
const root = import_node_path2.default.parse(dir).root;
|
|
199
|
+
while (dir !== root) {
|
|
200
|
+
const candidate = import_node_path2.default.join(dir, "tsconfig.json");
|
|
201
|
+
if (import_node_fs2.default.existsSync(candidate)) return candidate;
|
|
202
|
+
const parent = import_node_path2.default.dirname(dir);
|
|
203
|
+
if (parent === dir) break;
|
|
204
|
+
dir = parent;
|
|
205
|
+
}
|
|
206
|
+
return void 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/core/graph.ts
|
|
210
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
211
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
212
|
+
function buildGraph(files, getImports) {
|
|
213
|
+
const graph = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
graph.set(file, /* @__PURE__ */ new Set());
|
|
216
|
+
}
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
for (const imported of getImports(file)) {
|
|
219
|
+
graph.get(file)?.add(imported);
|
|
220
|
+
if (!graph.has(imported)) graph.set(imported, /* @__PURE__ */ new Set());
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return graph;
|
|
224
|
+
}
|
|
225
|
+
function findEntryPoints(rootDir, packageJson) {
|
|
226
|
+
const entries = [
|
|
227
|
+
...resolveNextJsEntries(rootDir),
|
|
228
|
+
...resolvePkgFieldEntries(rootDir, packageJson)
|
|
229
|
+
];
|
|
230
|
+
if (entries.length === 0) {
|
|
231
|
+
const fallback = resolveFallbackEntry(rootDir);
|
|
232
|
+
if (fallback) entries.push(fallback);
|
|
233
|
+
}
|
|
234
|
+
return [...new Set(entries)];
|
|
235
|
+
}
|
|
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
|
+
function detectCycles(graph) {
|
|
258
|
+
const cycles = [];
|
|
259
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
260
|
+
const visited = /* @__PURE__ */ new Set();
|
|
261
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
262
|
+
const path9 = [];
|
|
263
|
+
const acc = { seenKeys, cycles };
|
|
264
|
+
for (const start of graph.keys()) {
|
|
265
|
+
if (!visited.has(start)) {
|
|
266
|
+
dfsForCycles(start, graph, visited, inStack, path9, acc);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return cycles;
|
|
270
|
+
}
|
|
271
|
+
function resolveNextJsEntries(rootDir) {
|
|
272
|
+
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
|
+
if (!isNext) return [];
|
|
274
|
+
const entries = [];
|
|
275
|
+
for (const dir of ["pages", "app"]) {
|
|
276
|
+
const dirPath = import_node_path3.default.join(rootDir, dir);
|
|
277
|
+
if (import_node_fs3.default.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath));
|
|
278
|
+
}
|
|
279
|
+
return entries;
|
|
280
|
+
}
|
|
281
|
+
function resolvePkgFieldEntries(rootDir, packageJson) {
|
|
282
|
+
const entries = [];
|
|
283
|
+
for (const field of ["main", "module"]) {
|
|
284
|
+
const value = packageJson?.[field];
|
|
285
|
+
if (typeof value !== "string") continue;
|
|
286
|
+
const abs = import_node_path3.default.resolve(rootDir, value);
|
|
287
|
+
if (import_node_fs3.default.existsSync(abs)) entries.push(abs);
|
|
288
|
+
}
|
|
289
|
+
return entries;
|
|
290
|
+
}
|
|
291
|
+
function resolveFallbackEntry(rootDir) {
|
|
292
|
+
const fallbacks = ["src/main.ts", "src/main.tsx", "src/index.ts", "src/index.tsx"];
|
|
293
|
+
for (const rel of fallbacks) {
|
|
294
|
+
const abs = import_node_path3.default.join(rootDir, rel);
|
|
295
|
+
if (import_node_fs3.default.existsSync(abs)) return abs;
|
|
296
|
+
}
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
function mkFrame(node, graph) {
|
|
300
|
+
return { node, neighbors: (graph.get(node) ?? /* @__PURE__ */ new Set()).values(), entered: false };
|
|
301
|
+
}
|
|
302
|
+
function dfsForCycles(start, graph, visited, inStack, path9, acc) {
|
|
303
|
+
const stack = [mkFrame(start, graph)];
|
|
304
|
+
while (stack.length > 0) {
|
|
305
|
+
const frame = stack.at(-1);
|
|
306
|
+
if (!frame) break;
|
|
307
|
+
if (!frame.entered) {
|
|
308
|
+
if (visited.has(frame.node)) {
|
|
309
|
+
stack.pop();
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
frame.entered = true;
|
|
313
|
+
inStack.add(frame.node);
|
|
314
|
+
path9.push(frame.node);
|
|
315
|
+
}
|
|
316
|
+
const { done, value: neighbor } = frame.neighbors.next();
|
|
317
|
+
if (done) {
|
|
318
|
+
stack.pop();
|
|
319
|
+
path9.pop();
|
|
320
|
+
inStack.delete(frame.node);
|
|
321
|
+
visited.add(frame.node);
|
|
322
|
+
} else {
|
|
323
|
+
handleCycleNeighbor(neighbor, stack, path9, inStack, visited, acc, graph);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function handleCycleNeighbor(neighbor, stack, path9, inStack, visited, acc, graph) {
|
|
328
|
+
if (inStack.has(neighbor)) {
|
|
329
|
+
recordCycle(neighbor, path9, acc);
|
|
330
|
+
} else if (!visited.has(neighbor)) {
|
|
331
|
+
stack.push(mkFrame(neighbor, graph));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function recordCycle(cycleStart, path9, acc) {
|
|
335
|
+
const idx = path9.indexOf(cycleStart);
|
|
336
|
+
if (idx === -1) return;
|
|
337
|
+
const cycle = normalizeCycle(path9.slice(idx));
|
|
338
|
+
const key = cycle.join("\0");
|
|
339
|
+
if (!acc.seenKeys.has(key)) {
|
|
340
|
+
acc.seenKeys.add(key);
|
|
341
|
+
acc.cycles.push(cycle);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
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
|
+
function buildReverseGraph(graph) {
|
|
366
|
+
const rev = /* @__PURE__ */ new Map();
|
|
367
|
+
for (const [file] of graph) {
|
|
368
|
+
if (!rev.has(file)) rev.set(file, /* @__PURE__ */ new Set());
|
|
369
|
+
}
|
|
370
|
+
for (const [file, imports] of graph) {
|
|
371
|
+
for (const imp of imports) {
|
|
372
|
+
if (!rev.has(imp)) rev.set(imp, /* @__PURE__ */ new Set());
|
|
373
|
+
rev.get(imp)?.add(file);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return rev;
|
|
377
|
+
}
|
|
378
|
+
function normalizeCycle(cycle) {
|
|
379
|
+
if (cycle.length === 0) return cycle;
|
|
380
|
+
const minIdx = cycle.reduce(
|
|
381
|
+
(best, cur, i) => cur < cycle[best] ? i : best,
|
|
382
|
+
0
|
|
383
|
+
);
|
|
384
|
+
return [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
|
|
385
|
+
}
|
|
386
|
+
function collectSourceFiles(dir) {
|
|
387
|
+
const results = [];
|
|
388
|
+
const SOURCE_RE = /\.(tsx?|jsx?)$/;
|
|
389
|
+
function walk(current) {
|
|
390
|
+
let entries;
|
|
391
|
+
try {
|
|
392
|
+
entries = import_node_fs3.default.readdirSync(current, { withFileTypes: true });
|
|
393
|
+
} catch {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
for (const entry of entries) {
|
|
397
|
+
const full = import_node_path3.default.join(current, entry.name);
|
|
398
|
+
if (entry.isDirectory()) {
|
|
399
|
+
walk(full);
|
|
400
|
+
} else if (entry.isFile() && SOURCE_RE.test(entry.name)) {
|
|
401
|
+
results.push(full);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
walk(dir);
|
|
406
|
+
return results;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/modules/dead-code.ts
|
|
410
|
+
var import_ora2 = __toESM(require("ora"), 1);
|
|
411
|
+
var import_chalk = __toESM(require("chalk"), 1);
|
|
412
|
+
var import_cli_table3 = __toESM(require("cli-table3"), 1);
|
|
413
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
414
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
415
|
+
var import_ts_morph2 = require("ts-morph");
|
|
416
|
+
|
|
417
|
+
// src/core/reporter.ts
|
|
418
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
419
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
420
|
+
var import_ora = __toESM(require("ora"), 1);
|
|
421
|
+
function createSpinner(text) {
|
|
422
|
+
return (0, import_ora.default)(text).start();
|
|
423
|
+
}
|
|
424
|
+
var REPORTS_DIR_NAME = "prunify-reports";
|
|
425
|
+
function ensureReportsDir(rootDir, outDir) {
|
|
426
|
+
const base = outDir ? import_node_path4.default.resolve(outDir) : import_node_path4.default.resolve(rootDir);
|
|
427
|
+
const reportsDir = import_node_path4.default.join(base, REPORTS_DIR_NAME);
|
|
428
|
+
if (!import_node_fs4.default.existsSync(reportsDir)) {
|
|
429
|
+
import_node_fs4.default.mkdirSync(reportsDir, { recursive: true });
|
|
430
|
+
}
|
|
431
|
+
return reportsDir;
|
|
432
|
+
}
|
|
433
|
+
function writeReport(reportsDir, filename, content) {
|
|
434
|
+
ensureDir(reportsDir);
|
|
435
|
+
const filePath = import_node_path4.default.join(reportsDir, filename);
|
|
436
|
+
import_node_fs4.default.writeFileSync(filePath, content, "utf-8");
|
|
437
|
+
console.log(` Report saved \u2192 ${filePath}`);
|
|
438
|
+
}
|
|
439
|
+
function appendToGitignore(rootDir) {
|
|
440
|
+
const gitignorePath = import_node_path4.default.join(rootDir, ".gitignore");
|
|
441
|
+
const entry = `${REPORTS_DIR_NAME}/`;
|
|
442
|
+
if (import_node_fs4.default.existsSync(gitignorePath)) {
|
|
443
|
+
const contents = import_node_fs4.default.readFileSync(gitignorePath, "utf-8");
|
|
444
|
+
if (contents.split("\n").some((line) => line.trim() === entry)) return;
|
|
445
|
+
import_node_fs4.default.appendFileSync(gitignorePath, `
|
|
446
|
+
${entry}
|
|
447
|
+
`, "utf-8");
|
|
448
|
+
} else {
|
|
449
|
+
import_node_fs4.default.writeFileSync(gitignorePath, `${entry}
|
|
450
|
+
`, "utf-8");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function writeMarkdown(report, outputPath) {
|
|
454
|
+
const lines = [
|
|
455
|
+
`# ${report.title}`,
|
|
456
|
+
"",
|
|
457
|
+
`> ${report.summary}`,
|
|
458
|
+
"",
|
|
459
|
+
`_Generated: ${report.generatedAt.toISOString()}_`,
|
|
460
|
+
""
|
|
461
|
+
];
|
|
462
|
+
for (const section of report.sections) {
|
|
463
|
+
lines.push(`## ${section.title}`, "");
|
|
464
|
+
if (section.rows.length === 0) {
|
|
465
|
+
lines.push("_Nothing found._", "");
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (section.headers && section.headers.length > 0) {
|
|
469
|
+
lines.push(`| ${section.headers.join(" | ")} |`);
|
|
470
|
+
lines.push(`| ${section.headers.map(() => "---").join(" | ")} |`);
|
|
471
|
+
}
|
|
472
|
+
for (const row of section.rows) {
|
|
473
|
+
lines.push(`| ${row.join(" | ")} |`);
|
|
474
|
+
}
|
|
475
|
+
lines.push("");
|
|
476
|
+
}
|
|
477
|
+
ensureDir(import_node_path4.default.dirname(outputPath));
|
|
478
|
+
import_node_fs4.default.writeFileSync(outputPath, lines.join("\n"), "utf-8");
|
|
479
|
+
}
|
|
480
|
+
function writeJson(data, outputPath) {
|
|
481
|
+
ensureDir(import_node_path4.default.dirname(outputPath));
|
|
482
|
+
import_node_fs4.default.writeFileSync(outputPath, JSON.stringify(data, null, 2), "utf-8");
|
|
483
|
+
}
|
|
484
|
+
function ensureDir(dir) {
|
|
485
|
+
if (dir && !import_node_fs4.default.existsSync(dir)) {
|
|
486
|
+
import_node_fs4.default.mkdirSync(dir, { recursive: true });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/modules/dead-code.ts
|
|
491
|
+
function runDeadCodeModule(project, graph, entryPoints, rootDir) {
|
|
492
|
+
const allFiles = [...graph.keys()];
|
|
493
|
+
const effectiveEntries = entryPoints.length > 0 ? entryPoints : allFiles.slice(0, 1);
|
|
494
|
+
const liveFiles = runDFS(graph, effectiveEntries);
|
|
495
|
+
const deadFiles = allFiles.filter((f) => !liveFiles.has(f));
|
|
496
|
+
const deadSet = new Set(deadFiles);
|
|
497
|
+
const chains = findDeadChains(graph, deadSet);
|
|
498
|
+
const deadExports = findDeadExports(project, liveFiles);
|
|
499
|
+
const report = buildDeadCodeReport(deadFiles, chains, deadExports, rootDir);
|
|
500
|
+
return { deadFiles, liveFiles, chains, deadExports, report };
|
|
501
|
+
}
|
|
502
|
+
function findDeadExports(project, liveFiles) {
|
|
503
|
+
const importedNames = buildImportedNameMap(project, liveFiles);
|
|
504
|
+
const dead = [];
|
|
505
|
+
for (const filePath of liveFiles) {
|
|
506
|
+
collectFileDeadExports(filePath, project, importedNames, dead);
|
|
507
|
+
}
|
|
508
|
+
return dead;
|
|
509
|
+
}
|
|
510
|
+
function collectFileDeadExports(filePath, project, importedNames, dead) {
|
|
511
|
+
const sf = project.getSourceFile(filePath);
|
|
512
|
+
if (!sf) return;
|
|
513
|
+
const usedNames = importedNames.get(filePath) ?? /* @__PURE__ */ new Set();
|
|
514
|
+
if (usedNames.has("*")) return;
|
|
515
|
+
for (const [exportName, declarations] of sf.getExportedDeclarations()) {
|
|
516
|
+
if (usedNames.has(exportName)) continue;
|
|
517
|
+
const line = getExportLine(declarations[0]);
|
|
518
|
+
dead.push({ filePath, exportName, line });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function getExportLine(decl) {
|
|
522
|
+
if (!decl) return 0;
|
|
523
|
+
if (!import_ts_morph2.Node.isNode(decl)) return 0;
|
|
524
|
+
return decl.getStartLineNumber();
|
|
525
|
+
}
|
|
526
|
+
function getFileSize(filePath) {
|
|
527
|
+
try {
|
|
528
|
+
return import_node_fs5.default.statSync(filePath).size;
|
|
529
|
+
} catch {
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function buildDeadCodeReport(deadFiles, chains, deadExports, rootDir) {
|
|
534
|
+
const rel = (p) => import_node_path5.default.relative(rootDir, p).replaceAll("\\", "/");
|
|
535
|
+
const totalBytes = deadFiles.reduce((sum, f) => {
|
|
536
|
+
const chain = chains.get(f) ?? [];
|
|
537
|
+
return sum + getFileSize(f) + chain.reduce((s, c) => s + getFileSize(c), 0);
|
|
538
|
+
}, 0);
|
|
539
|
+
const totalKb = (totalBytes / 1024).toFixed(1);
|
|
540
|
+
const lines = [
|
|
541
|
+
"========================================",
|
|
542
|
+
" DEAD CODE REPORT",
|
|
543
|
+
` Dead files : ${deadFiles.length}`,
|
|
544
|
+
` Dead exports: ${deadExports.length}`,
|
|
545
|
+
` Recoverable : ~${totalKb} KB`,
|
|
546
|
+
"========================================",
|
|
547
|
+
""
|
|
548
|
+
];
|
|
549
|
+
if (deadFiles.length > 0) {
|
|
550
|
+
lines.push("\u2500\u2500 DEAD FILES \u2500\u2500", "");
|
|
551
|
+
for (const filePath of deadFiles) {
|
|
552
|
+
const chain = chains.get(filePath) ?? [];
|
|
553
|
+
const allFiles = [filePath, ...chain];
|
|
554
|
+
const sizeBytes = allFiles.reduce((s, f) => s + getFileSize(f), 0);
|
|
555
|
+
const sizeKb = (sizeBytes / 1024).toFixed(1);
|
|
556
|
+
const chainStr = chain.length > 0 ? [rel(filePath), ...chain.map(rel)].join(" \u2192 ") : rel(filePath);
|
|
557
|
+
const plural = allFiles.length === 1 ? "" : "s";
|
|
558
|
+
lines.push(
|
|
559
|
+
`DEAD FILE \u2014 ${rel(filePath)}`,
|
|
560
|
+
`Reason: Not imported anywhere in the codebase`,
|
|
561
|
+
`Chain: ${chainStr}`,
|
|
562
|
+
`Size: ~${sizeKb} KB removable across ${allFiles.length} file${plural}`,
|
|
563
|
+
`Action: Safe to delete all ${allFiles.length} file${plural}`,
|
|
564
|
+
""
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (deadExports.length > 0) {
|
|
569
|
+
lines.push("\u2500\u2500 DEAD EXPORTS \u2500\u2500", "");
|
|
570
|
+
for (const entry of deadExports) {
|
|
571
|
+
lines.push(
|
|
572
|
+
`DEAD EXPORT \u2014 ${rel(entry.filePath)} \u2192 ${entry.exportName}() [line ${entry.line}]`,
|
|
573
|
+
`Reason: Exported but never imported`,
|
|
574
|
+
`Action: Remove the export (file itself is still live)`,
|
|
575
|
+
""
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return lines.join("\n");
|
|
580
|
+
}
|
|
581
|
+
async function runDeadCode(dir, opts) {
|
|
582
|
+
const spinner = (0, import_ora2.default)(import_chalk.default.cyan("Scanning for dead code\u2026")).start();
|
|
583
|
+
try {
|
|
584
|
+
const fileList = discoverFiles(dir, []);
|
|
585
|
+
const project = buildProject(fileList);
|
|
586
|
+
const graph = buildGraph(fileList, (f) => {
|
|
587
|
+
const sf = project.getSourceFile(f);
|
|
588
|
+
return sf ? getImportsForFile(sf) : [];
|
|
589
|
+
});
|
|
590
|
+
const packageJson = loadPackageJson(dir);
|
|
591
|
+
const entries = findEntryPoints(dir, packageJson);
|
|
592
|
+
const result = runDeadCodeModule(project, graph, entries, dir);
|
|
593
|
+
const dead = [
|
|
594
|
+
...result.deadFiles.map((f) => ({ file: f, exportName: "(entire file)" })),
|
|
595
|
+
...result.deadExports.map((e) => ({ file: e.filePath, exportName: e.exportName }))
|
|
596
|
+
];
|
|
597
|
+
spinner.succeed(import_chalk.default.green(`Dead code scan complete \u2014 ${dead.length} item(s) found`));
|
|
598
|
+
if (dead.length === 0) {
|
|
599
|
+
console.log(import_chalk.default.green(" No dead code detected."));
|
|
600
|
+
return dead;
|
|
601
|
+
}
|
|
602
|
+
printDeadTable(dead);
|
|
603
|
+
writeDeadOutput(result, opts);
|
|
604
|
+
return dead;
|
|
605
|
+
} catch (err) {
|
|
606
|
+
spinner.fail(import_chalk.default.red("Dead code scan failed"));
|
|
607
|
+
throw err;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function buildImportedNameMap(project, liveFiles) {
|
|
611
|
+
const importedNames = /* @__PURE__ */ new Map();
|
|
612
|
+
const touch = (file, name) => {
|
|
613
|
+
if (!importedNames.has(file)) importedNames.set(file, /* @__PURE__ */ new Set());
|
|
614
|
+
importedNames.get(file).add(name);
|
|
615
|
+
};
|
|
616
|
+
for (const filePath of liveFiles) {
|
|
617
|
+
const sf = project.getSourceFile(filePath);
|
|
618
|
+
if (!sf) continue;
|
|
619
|
+
processImports(sf, touch);
|
|
620
|
+
processReExports(sf, touch);
|
|
621
|
+
}
|
|
622
|
+
return importedNames;
|
|
623
|
+
}
|
|
624
|
+
function processImports(sf, touch) {
|
|
625
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
626
|
+
const resolved = decl.getModuleSpecifierSourceFile();
|
|
627
|
+
if (!resolved) continue;
|
|
628
|
+
const target = resolved.getFilePath();
|
|
629
|
+
if (decl.getNamespaceImport()) {
|
|
630
|
+
touch(target, "*");
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const defaultImport = decl.getDefaultImport();
|
|
634
|
+
if (defaultImport) touch(target, "default");
|
|
635
|
+
for (const named of decl.getNamedImports()) {
|
|
636
|
+
touch(target, named.getAliasNode()?.getText() ?? named.getName());
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function processReExports(sf, touch) {
|
|
641
|
+
for (const decl of sf.getExportDeclarations()) {
|
|
642
|
+
const resolved = decl.getModuleSpecifierSourceFile();
|
|
643
|
+
if (!resolved) continue;
|
|
644
|
+
const target = resolved.getFilePath();
|
|
645
|
+
if (decl.isNamespaceExport()) {
|
|
646
|
+
touch(target, "*");
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
for (const named of decl.getNamedExports()) {
|
|
650
|
+
touch(target, named.getName());
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function loadPackageJson(dir) {
|
|
655
|
+
const pkgPath = import_node_path5.default.join(dir, "package.json");
|
|
656
|
+
if (!import_node_fs5.default.existsSync(pkgPath)) return null;
|
|
657
|
+
return JSON.parse(import_node_fs5.default.readFileSync(pkgPath, "utf-8"));
|
|
658
|
+
}
|
|
659
|
+
function printDeadTable(dead) {
|
|
660
|
+
const table = new import_cli_table3.default({ head: ["File", "Export"] });
|
|
661
|
+
for (const row of dead) {
|
|
662
|
+
table.push([row.file, row.exportName]);
|
|
663
|
+
}
|
|
664
|
+
console.log(table.toString());
|
|
665
|
+
}
|
|
666
|
+
function writeDeadOutput(result, opts) {
|
|
667
|
+
if (!opts.output) return;
|
|
668
|
+
if (opts.json) {
|
|
669
|
+
writeJson(
|
|
670
|
+
{ deadFiles: result.deadFiles, deadExports: result.deadExports },
|
|
671
|
+
opts.output
|
|
672
|
+
);
|
|
673
|
+
console.log(import_chalk.default.cyan(` JSON written to ${opts.output}`));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
writeMarkdown(
|
|
677
|
+
{
|
|
678
|
+
title: "Dead Code Report",
|
|
679
|
+
summary: `${result.deadFiles.length} dead file(s), ${result.deadExports.length} dead export(s)`,
|
|
680
|
+
sections: [
|
|
681
|
+
{
|
|
682
|
+
title: "Dead Files",
|
|
683
|
+
headers: ["File", "Chain"],
|
|
684
|
+
rows: result.deadFiles.map((f) => [
|
|
685
|
+
f,
|
|
686
|
+
(result.chains.get(f) ?? []).join(" \u2192 ") || "\u2014"
|
|
687
|
+
])
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
title: "Dead Exports",
|
|
691
|
+
headers: ["File", "Export", "Line"],
|
|
692
|
+
rows: result.deadExports.map((e) => [e.filePath, e.exportName, String(e.line)])
|
|
693
|
+
}
|
|
694
|
+
],
|
|
695
|
+
generatedAt: /* @__PURE__ */ new Date()
|
|
696
|
+
},
|
|
697
|
+
opts.output
|
|
698
|
+
);
|
|
699
|
+
console.log(import_chalk.default.cyan(` Report written to ${opts.output}`));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/modules/dupe-finder.ts
|
|
703
|
+
var import_ora3 = __toESM(require("ora"), 1);
|
|
704
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
705
|
+
var import_cli_table32 = __toESM(require("cli-table3"), 1);
|
|
706
|
+
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
707
|
+
var import_ts_morph4 = require("ts-morph");
|
|
708
|
+
|
|
709
|
+
// src/utils/ast.ts
|
|
710
|
+
var import_node_crypto = __toESM(require("crypto"), 1);
|
|
711
|
+
var import_ts_morph3 = require("ts-morph");
|
|
712
|
+
|
|
713
|
+
// src/modules/dupe-finder.ts
|
|
714
|
+
async function runDupeFinder(dir, opts) {
|
|
715
|
+
const minLines = parseInt(opts.minLines ?? "5", 10);
|
|
716
|
+
const spinner = (0, import_ora3.default)(import_chalk2.default.cyan(`Scanning for duplicate blocks (\u2265${minLines} lines)\u2026`)).start();
|
|
717
|
+
try {
|
|
718
|
+
const files = glob(dir, ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], ["node_modules", "dist"]);
|
|
719
|
+
const blockMap = /* @__PURE__ */ new Map();
|
|
720
|
+
for (const filePath of files) {
|
|
721
|
+
const content = import_node_fs6.default.readFileSync(filePath, "utf-8");
|
|
722
|
+
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
723
|
+
for (let i = 0; i <= lines.length - minLines; i++) {
|
|
724
|
+
const block = lines.slice(i, i + minLines).join("\n");
|
|
725
|
+
if (!blockMap.has(block)) blockMap.set(block, []);
|
|
726
|
+
blockMap.get(block).push({ file: filePath, startLine: i + 1 });
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const dupes = [];
|
|
730
|
+
for (const [block, occurrences] of blockMap) {
|
|
731
|
+
if (occurrences.length > 1) {
|
|
732
|
+
dupes.push({
|
|
733
|
+
hash: hashString(block),
|
|
734
|
+
lines: minLines,
|
|
735
|
+
occurrences
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
spinner.succeed(import_chalk2.default.green(`Duplicate scan complete \u2014 ${dupes.length} duplicate block(s) found`));
|
|
740
|
+
if (dupes.length === 0) {
|
|
741
|
+
console.log(import_chalk2.default.green(" No duplicate blocks detected."));
|
|
742
|
+
return dupes;
|
|
743
|
+
}
|
|
744
|
+
const table = new import_cli_table32.default({ head: ["Hash", "Lines", "Count", "First Occurrence"] });
|
|
745
|
+
for (const d of dupes) {
|
|
746
|
+
table.push([
|
|
747
|
+
import_chalk2.default.gray(d.hash.slice(0, 8)),
|
|
748
|
+
String(d.lines),
|
|
749
|
+
import_chalk2.default.yellow(String(d.occurrences.length)),
|
|
750
|
+
`${d.occurrences[0].file}:${d.occurrences[0].startLine}`
|
|
751
|
+
]);
|
|
752
|
+
}
|
|
753
|
+
console.log(table.toString());
|
|
754
|
+
if (opts.output) {
|
|
755
|
+
const rows = dupes.flatMap(
|
|
756
|
+
(d) => d.occurrences.map((o) => [d.hash.slice(0, 8), String(d.lines), o.file, String(o.startLine)])
|
|
757
|
+
);
|
|
758
|
+
writeMarkdown(
|
|
759
|
+
{
|
|
760
|
+
title: "Duplicate Code Report",
|
|
761
|
+
summary: `${dupes.length} duplicate block(s) found (min lines: ${minLines})`,
|
|
762
|
+
sections: [{ title: "Duplicates", headers: ["Hash", "Lines", "File", "Start Line"], rows }],
|
|
763
|
+
generatedAt: /* @__PURE__ */ new Date()
|
|
764
|
+
},
|
|
765
|
+
opts.output
|
|
766
|
+
);
|
|
767
|
+
console.log(import_chalk2.default.cyan(` Report written to ${opts.output}`));
|
|
768
|
+
}
|
|
769
|
+
return dupes;
|
|
770
|
+
} catch (err) {
|
|
771
|
+
spinner.fail(import_chalk2.default.red("Duplicate scan failed"));
|
|
772
|
+
throw err;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function hashString(str) {
|
|
776
|
+
let hash = 5381;
|
|
777
|
+
for (let i = 0; i < str.length; i++) {
|
|
778
|
+
hash = (hash << 5) + hash ^ str.charCodeAt(i);
|
|
779
|
+
}
|
|
780
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/modules/dep-check.ts
|
|
784
|
+
var import_ora4 = __toESM(require("ora"), 1);
|
|
785
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
786
|
+
var import_cli_table33 = __toESM(require("cli-table3"), 1);
|
|
787
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
788
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
789
|
+
var import_ts_morph5 = require("ts-morph");
|
|
790
|
+
async function runDepCheck(opts) {
|
|
791
|
+
const spinner = (0, import_ora4.default)(import_chalk3.default.cyan("Auditing dependencies\u2026")).start();
|
|
792
|
+
try {
|
|
793
|
+
const pkgPath = import_node_path6.default.join(opts.cwd, "package.json");
|
|
794
|
+
if (!import_node_fs7.default.existsSync(pkgPath)) {
|
|
795
|
+
spinner.fail(import_chalk3.default.red(`No package.json found at ${opts.cwd}`));
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
const pkg = JSON.parse(import_node_fs7.default.readFileSync(pkgPath, "utf-8"));
|
|
799
|
+
const declared = /* @__PURE__ */ new Set([
|
|
800
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
801
|
+
...Object.keys(pkg.devDependencies ?? {})
|
|
802
|
+
]);
|
|
803
|
+
const srcDir = import_node_path6.default.join(opts.cwd, "src");
|
|
804
|
+
const files = glob(srcDir, ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], ["node_modules", "dist"]);
|
|
805
|
+
const usedPackages = /* @__PURE__ */ new Set();
|
|
806
|
+
for (const filePath of files) {
|
|
807
|
+
const content = import_node_fs7.default.readFileSync(filePath, "utf-8");
|
|
808
|
+
const importRegex = /from\s+['"]([^'"./][^'"]*)['"]/g;
|
|
809
|
+
let match;
|
|
810
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
811
|
+
const specifier = match[1];
|
|
812
|
+
const pkgName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
813
|
+
usedPackages.add(pkgName);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const issues = [];
|
|
817
|
+
for (const dep of declared) {
|
|
818
|
+
if (!usedPackages.has(dep)) {
|
|
819
|
+
issues.push({ name: dep, type: "unused" });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
for (const pkg2 of usedPackages) {
|
|
823
|
+
if (!declared.has(pkg2) && !isBuiltin(pkg2)) {
|
|
824
|
+
issues.push({ name: pkg2, type: "missing" });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
spinner.succeed(import_chalk3.default.green(`Dependency audit complete \u2014 ${issues.length} issue(s) found`));
|
|
828
|
+
if (issues.length === 0) {
|
|
829
|
+
console.log(import_chalk3.default.green(" All dependencies look healthy."));
|
|
830
|
+
return issues;
|
|
831
|
+
}
|
|
832
|
+
const table = new import_cli_table33.default({ head: ["Package", "Issue"] });
|
|
833
|
+
for (const issue of issues) {
|
|
834
|
+
const label = issue.type === "unused" ? import_chalk3.default.yellow("unused") : issue.type === "missing" ? import_chalk3.default.red("missing from package.json") : import_chalk3.default.magenta("unlisted dev dep");
|
|
835
|
+
table.push([import_chalk3.default.gray(issue.name), label]);
|
|
836
|
+
}
|
|
837
|
+
console.log(table.toString());
|
|
838
|
+
if (opts.output) {
|
|
839
|
+
writeMarkdown(
|
|
840
|
+
{
|
|
841
|
+
title: "Dependency Audit Report",
|
|
842
|
+
summary: `${issues.length} dependency issue(s) found`,
|
|
843
|
+
sections: [
|
|
844
|
+
{
|
|
845
|
+
title: "Issues",
|
|
846
|
+
headers: ["Package", "Type"],
|
|
847
|
+
rows: issues.map((i) => [i.name, i.type])
|
|
848
|
+
}
|
|
849
|
+
],
|
|
850
|
+
generatedAt: /* @__PURE__ */ new Date()
|
|
851
|
+
},
|
|
852
|
+
opts.output
|
|
853
|
+
);
|
|
854
|
+
console.log(import_chalk3.default.cyan(` Report written to ${opts.output}`));
|
|
855
|
+
}
|
|
856
|
+
return issues;
|
|
857
|
+
} catch (err) {
|
|
858
|
+
spinner.fail(import_chalk3.default.red("Dependency audit failed"));
|
|
859
|
+
throw err;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
var NODE_BUILTINS = /* @__PURE__ */ new Set([
|
|
863
|
+
"node:fs",
|
|
864
|
+
"node:path",
|
|
865
|
+
"node:os",
|
|
866
|
+
"node:url",
|
|
867
|
+
"node:crypto",
|
|
868
|
+
"node:util",
|
|
869
|
+
"node:stream",
|
|
870
|
+
"node:events",
|
|
871
|
+
"node:child_process",
|
|
872
|
+
"node:process",
|
|
873
|
+
"fs",
|
|
874
|
+
"path",
|
|
875
|
+
"os",
|
|
876
|
+
"url",
|
|
877
|
+
"crypto",
|
|
878
|
+
"util",
|
|
879
|
+
"stream",
|
|
880
|
+
"events",
|
|
881
|
+
"child_process"
|
|
882
|
+
]);
|
|
883
|
+
function isBuiltin(name) {
|
|
884
|
+
return NODE_BUILTINS.has(name) || name.startsWith("node:");
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/modules/health-report.ts
|
|
888
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
889
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
890
|
+
|
|
891
|
+
// src/modules/circular.ts
|
|
892
|
+
var import_ora5 = __toESM(require("ora"), 1);
|
|
893
|
+
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
894
|
+
var import_cli_table34 = __toESM(require("cli-table3"), 1);
|
|
895
|
+
async function runCircular(dir, opts) {
|
|
896
|
+
const spinner = (0, import_ora5.default)(import_chalk4.default.cyan("Scanning for circular imports\u2026")).start();
|
|
897
|
+
try {
|
|
898
|
+
const fileList = discoverFiles(dir, []);
|
|
899
|
+
const project = buildProject(fileList);
|
|
900
|
+
const graph = buildGraph(fileList, (f) => {
|
|
901
|
+
const sf = project.getSourceFile(f);
|
|
902
|
+
return sf ? getImportsForFile(sf) : [];
|
|
903
|
+
});
|
|
904
|
+
const cycles = detectCycles(graph);
|
|
905
|
+
spinner.succeed(
|
|
906
|
+
import_chalk4.default.green(`Circular import scan complete \u2014 ${cycles.length} cycle(s) found`)
|
|
907
|
+
);
|
|
908
|
+
if (cycles.length === 0) {
|
|
909
|
+
console.log(import_chalk4.default.green(" No circular imports detected."));
|
|
910
|
+
return cycles;
|
|
911
|
+
}
|
|
912
|
+
const table = new import_cli_table34.default({ head: ["Cycle #", "Files involved"] });
|
|
913
|
+
cycles.forEach((cycle, i) => {
|
|
914
|
+
table.push([import_chalk4.default.yellow(String(i + 1)), cycle.map((f) => import_chalk4.default.gray(f)).join("\n \u2192 ")]);
|
|
915
|
+
});
|
|
916
|
+
console.log(table.toString());
|
|
917
|
+
if (opts.output) {
|
|
918
|
+
writeMarkdown(
|
|
919
|
+
{
|
|
920
|
+
title: "Circular Imports Report",
|
|
921
|
+
summary: `${cycles.length} circular import chain(s) found`,
|
|
922
|
+
sections: [
|
|
923
|
+
{
|
|
924
|
+
title: "Circular Chains",
|
|
925
|
+
headers: ["Cycle #", "Files"],
|
|
926
|
+
rows: cycles.map((cycle, i) => [String(i + 1), cycle.join(" \u2192 ")])
|
|
927
|
+
}
|
|
928
|
+
],
|
|
929
|
+
generatedAt: /* @__PURE__ */ new Date()
|
|
930
|
+
},
|
|
931
|
+
opts.output
|
|
932
|
+
);
|
|
933
|
+
console.log(import_chalk4.default.cyan(` Report written to ${opts.output}`));
|
|
934
|
+
}
|
|
935
|
+
return cycles;
|
|
936
|
+
} catch (err) {
|
|
937
|
+
spinner.fail(import_chalk4.default.red("Circular import scan failed"));
|
|
938
|
+
throw err;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/modules/health-report.ts
|
|
943
|
+
async function runHealthReport(dir, opts) {
|
|
944
|
+
console.log(import_chalk5.default.bold.cyan("\n prunify \u2014 Codebase Health Report\n"));
|
|
945
|
+
const [deadExports, dupes, cycles, depIssues] = await Promise.all([
|
|
946
|
+
runDeadCode(dir, {}).catch(() => []),
|
|
947
|
+
runDupeFinder(dir, {}).catch(() => []),
|
|
948
|
+
runCircular(dir, {}).catch(() => []),
|
|
949
|
+
runDepCheck({ cwd: import_node_path7.default.resolve(dir, ".."), output: void 0 }).catch(() => [])
|
|
950
|
+
]);
|
|
951
|
+
const sections = [
|
|
952
|
+
{
|
|
953
|
+
title: "\u{1F6A8} Dead Exports",
|
|
954
|
+
headers: ["File", "Export"],
|
|
955
|
+
rows: deadExports.map((d) => [d.file, d.exportName])
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
title: "\u{1F501} Duplicate Blocks",
|
|
959
|
+
headers: ["Hash", "Lines", "Occurrences"],
|
|
960
|
+
rows: dupes.map((d) => [d.hash.slice(0, 8), String(d.lines), String(d.occurrences.length)])
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
title: "\u267B\uFE0F Circular Imports",
|
|
964
|
+
headers: ["Cycle #", "Chain"],
|
|
965
|
+
rows: cycles.map((cycle, i) => [String(i + 1), cycle.join(" \u2192 ")])
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
title: "\u{1F4E6} Dependency Issues",
|
|
969
|
+
headers: ["Package", "Issue"],
|
|
970
|
+
rows: depIssues.map((i) => [i.name, i.type])
|
|
971
|
+
}
|
|
972
|
+
];
|
|
973
|
+
const totalIssues = deadExports.length + dupes.length + cycles.length + depIssues.length;
|
|
974
|
+
const report = {
|
|
975
|
+
title: "Codebase Health Report",
|
|
976
|
+
summary: `Analysed: ${import_node_path7.default.resolve(dir)} | Total issues: ${totalIssues}`,
|
|
977
|
+
sections,
|
|
978
|
+
generatedAt: /* @__PURE__ */ new Date()
|
|
979
|
+
};
|
|
980
|
+
writeMarkdown(report, opts.output);
|
|
981
|
+
console.log(
|
|
982
|
+
import_chalk5.default.bold(`
|
|
983
|
+
Health report written to `) + import_chalk5.default.cyan(opts.output)
|
|
984
|
+
);
|
|
985
|
+
console.log(
|
|
986
|
+
import_chalk5.default.dim(` Total issues found: `) + (totalIssues > 0 ? import_chalk5.default.red(String(totalIssues)) : import_chalk5.default.green("0"))
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/cli.ts
|
|
991
|
+
var import_meta = {};
|
|
992
|
+
function readPkgVersion() {
|
|
993
|
+
try {
|
|
994
|
+
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
995
|
+
const dir = import_node_path8.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
996
|
+
const pkgPath = import_node_path8.default.resolve(dir, "..", "package.json");
|
|
997
|
+
return JSON.parse(import_node_fs8.default.readFileSync(pkgPath, "utf-8")).version;
|
|
998
|
+
}
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
try {
|
|
1002
|
+
const dir = globalThis.__dirname ?? __dirname;
|
|
1003
|
+
const pkgPath = import_node_path8.default.resolve(dir, "..", "package.json");
|
|
1004
|
+
return JSON.parse(import_node_fs8.default.readFileSync(pkgPath, "utf-8")).version;
|
|
1005
|
+
} catch {
|
|
1006
|
+
return "0.0.0";
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
var PKG_VERSION = readPkgVersion();
|
|
1010
|
+
var ALL_MODULES = ["dead-code", "dupes", "circular", "deps"];
|
|
1011
|
+
var program = new import_commander.Command();
|
|
1012
|
+
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
|
+
"--ignore <pattern>",
|
|
1014
|
+
"Glob pattern to ignore (repeatable)",
|
|
1015
|
+
(val, acc) => [...acc, val],
|
|
1016
|
+
[]
|
|
1017
|
+
).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
|
+
program.parse();
|
|
1019
|
+
async function main(opts) {
|
|
1020
|
+
const rootDir = import_node_path8.default.resolve(opts.dir);
|
|
1021
|
+
if (!import_node_fs8.default.existsSync(import_node_path8.default.join(rootDir, "package.json"))) {
|
|
1022
|
+
console.error(import_chalk6.default.red(`\u2717 No package.json found in ${rootDir}`));
|
|
1023
|
+
console.error(import_chalk6.default.dim(" Use --dir <path> to point to your project root."));
|
|
1024
|
+
process.exit(1);
|
|
1025
|
+
}
|
|
1026
|
+
const modules = resolveModules(opts.only);
|
|
1027
|
+
console.log();
|
|
1028
|
+
console.log(import_chalk6.default.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
|
|
1029
|
+
console.log();
|
|
1030
|
+
const parseSpinner = createSpinner(import_chalk6.default.cyan("Parsing codebase\u2026"));
|
|
1031
|
+
const files = discoverFiles(rootDir, opts.ignore);
|
|
1032
|
+
parseSpinner.succeed(import_chalk6.default.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
|
|
1033
|
+
const graphSpinner = createSpinner(import_chalk6.default.cyan("Building import graph\u2026"));
|
|
1034
|
+
const project = buildProject(files);
|
|
1035
|
+
const graph = buildGraph(files, (f) => {
|
|
1036
|
+
const sf = project.getSourceFile(f);
|
|
1037
|
+
return sf ? getImportsForFile(sf) : [];
|
|
1038
|
+
});
|
|
1039
|
+
const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
|
|
1040
|
+
graphSpinner.succeed(import_chalk6.default.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
|
|
1041
|
+
const packageJson = loadPackageJson2(rootDir);
|
|
1042
|
+
const entryPoints = opts.entry ? [import_node_path8.default.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
|
|
1043
|
+
const reportsDir = ensureReportsDir(rootDir, opts.out);
|
|
1044
|
+
appendToGitignore(rootDir);
|
|
1045
|
+
console.log();
|
|
1046
|
+
let deadFileCount = 0;
|
|
1047
|
+
let dupeCount = 0;
|
|
1048
|
+
let unusedPkgCount = 0;
|
|
1049
|
+
let circularCount = 0;
|
|
1050
|
+
let deadReportFile = "";
|
|
1051
|
+
let dupesReportFile = "";
|
|
1052
|
+
let depsReportFile = "";
|
|
1053
|
+
let circularReportFile = "";
|
|
1054
|
+
const deadFilePaths = [];
|
|
1055
|
+
if (modules.includes("dead-code")) {
|
|
1056
|
+
const spinner = createSpinner(import_chalk6.default.cyan("Analysing dead code\u2026"));
|
|
1057
|
+
const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
|
|
1058
|
+
deadFileCount = result.deadFiles.length + result.deadExports.length;
|
|
1059
|
+
deadFilePaths.push(...result.deadFiles);
|
|
1060
|
+
spinner.succeed(import_chalk6.default.green(`Dead code analysis complete \u2014 ${deadFileCount} item(s) found`));
|
|
1061
|
+
if (result.report) {
|
|
1062
|
+
deadReportFile = "dead-code.txt";
|
|
1063
|
+
writeReport(reportsDir, deadReportFile, result.report);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (modules.includes("dupes")) {
|
|
1067
|
+
const outputPath = import_node_path8.default.join(reportsDir, "dupes.md");
|
|
1068
|
+
const dupes = await runDupeFinder(rootDir, { output: outputPath });
|
|
1069
|
+
dupeCount = dupes.length;
|
|
1070
|
+
if (dupeCount > 0) dupesReportFile = "dupes.md";
|
|
1071
|
+
}
|
|
1072
|
+
if (modules.includes("circular")) {
|
|
1073
|
+
const spinner = createSpinner(import_chalk6.default.cyan("Analysing circular imports\u2026"));
|
|
1074
|
+
const cycles = detectCycles(graph);
|
|
1075
|
+
circularCount = cycles.length;
|
|
1076
|
+
spinner.succeed(import_chalk6.default.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
|
|
1077
|
+
if (circularCount > 0) {
|
|
1078
|
+
circularReportFile = "circular.txt";
|
|
1079
|
+
const cycleText = cycles.map((c, i) => `Cycle ${i + 1}: ${c.join(" \u2192 ")}`).join("\n");
|
|
1080
|
+
writeReport(reportsDir, circularReportFile, cycleText);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (modules.includes("deps")) {
|
|
1084
|
+
const outputPath = import_node_path8.default.join(reportsDir, "deps.md");
|
|
1085
|
+
const issues = await runDepCheck({ cwd: rootDir, output: outputPath });
|
|
1086
|
+
unusedPkgCount = issues.filter((i) => i.type === "unused").length;
|
|
1087
|
+
if (issues.length > 0) depsReportFile = "deps.md";
|
|
1088
|
+
}
|
|
1089
|
+
if (modules.includes("health")) {
|
|
1090
|
+
const outputPath = import_node_path8.default.join(reportsDir, "health-report.md");
|
|
1091
|
+
await runHealthReport(rootDir, { output: outputPath });
|
|
1092
|
+
}
|
|
1093
|
+
if (opts.html) {
|
|
1094
|
+
const htmlPath = import_node_path8.default.join(reportsDir, "code_health.html");
|
|
1095
|
+
writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount);
|
|
1096
|
+
console.log(import_chalk6.default.cyan(` HTML report written to ${htmlPath}`));
|
|
1097
|
+
}
|
|
1098
|
+
console.log();
|
|
1099
|
+
console.log(import_chalk6.default.bold("Summary"));
|
|
1100
|
+
console.log();
|
|
1101
|
+
const table = new import_cli_table35.default({
|
|
1102
|
+
head: [import_chalk6.default.bold("Check"), import_chalk6.default.bold("Found"), import_chalk6.default.bold("Output File")],
|
|
1103
|
+
style: { head: [], border: [] }
|
|
1104
|
+
});
|
|
1105
|
+
const fmt = (n) => n > 0 ? import_chalk6.default.yellow(String(n)) : import_chalk6.default.green("0");
|
|
1106
|
+
table.push(
|
|
1107
|
+
["Dead Files / Exports", fmt(deadFileCount), deadReportFile || "\u2014"],
|
|
1108
|
+
["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
|
|
1109
|
+
["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
|
|
1110
|
+
["Circular Deps", fmt(circularCount), circularReportFile || "\u2014"]
|
|
1111
|
+
);
|
|
1112
|
+
console.log(table.toString());
|
|
1113
|
+
console.log();
|
|
1114
|
+
if (opts.delete && deadFilePaths.length > 0) {
|
|
1115
|
+
console.log(import_chalk6.default.yellow(`Dead files (${deadFilePaths.length}):`));
|
|
1116
|
+
for (const f of deadFilePaths) {
|
|
1117
|
+
console.log(import_chalk6.default.dim(` ${import_node_path8.default.relative(rootDir, f)}`));
|
|
1118
|
+
}
|
|
1119
|
+
console.log();
|
|
1120
|
+
if (!opts.ci) {
|
|
1121
|
+
const confirmed = await confirmPrompt("Delete these files? (y/N) ");
|
|
1122
|
+
if (confirmed) {
|
|
1123
|
+
for (const f of deadFilePaths) {
|
|
1124
|
+
import_node_fs8.default.rmSync(f, { force: true });
|
|
1125
|
+
}
|
|
1126
|
+
console.log(import_chalk6.default.green(` Deleted ${deadFilePaths.length} file(s).`));
|
|
1127
|
+
} else {
|
|
1128
|
+
console.log(import_chalk6.default.dim(" Skipped."));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (opts.ci) {
|
|
1133
|
+
const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0;
|
|
1134
|
+
if (hasIssues) process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function resolveModules(only) {
|
|
1138
|
+
if (!only) return ALL_MODULES;
|
|
1139
|
+
const valid = /* @__PURE__ */ new Set([...ALL_MODULES, "health"]);
|
|
1140
|
+
return only.split(",").map((s) => s.trim()).filter((m) => valid.has(m));
|
|
1141
|
+
}
|
|
1142
|
+
function loadPackageJson2(dir) {
|
|
1143
|
+
try {
|
|
1144
|
+
return JSON.parse(import_node_fs8.default.readFileSync(import_node_path8.default.join(dir, "package.json"), "utf-8"));
|
|
1145
|
+
} catch {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function confirmPrompt(question) {
|
|
1150
|
+
return new Promise((resolve) => {
|
|
1151
|
+
const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
1152
|
+
rl.question(question, (answer) => {
|
|
1153
|
+
rl.close();
|
|
1154
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
function writeHtmlReport(outputPath, rootDir, deadFiles, circularCount, dupeCount, unusedPkgCount) {
|
|
1159
|
+
const rows = [
|
|
1160
|
+
["Dead Files / Exports", String(deadFiles.length)],
|
|
1161
|
+
["Duplicate Clusters", String(dupeCount)],
|
|
1162
|
+
["Circular Dependencies", String(circularCount)],
|
|
1163
|
+
["Unused Packages", String(unusedPkgCount)]
|
|
1164
|
+
].map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`).join("\n");
|
|
1165
|
+
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${import_node_path8.default.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
|
|
1166
|
+
const html = `<!DOCTYPE html>
|
|
1167
|
+
<html lang="en">
|
|
1168
|
+
<head>
|
|
1169
|
+
<meta charset="UTF-8">
|
|
1170
|
+
<title>prunify \u2014 Code Health Report</title>
|
|
1171
|
+
<style>
|
|
1172
|
+
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
|
|
1173
|
+
h1 { color: #0ea5e9; }
|
|
1174
|
+
table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }
|
|
1175
|
+
th, td { border: 1px solid #e2e8f0; padding: .5rem 1rem; text-align: left; }
|
|
1176
|
+
th { background: #f8fafc; }
|
|
1177
|
+
small { color: #94a3b8; }
|
|
1178
|
+
</style>
|
|
1179
|
+
</head>
|
|
1180
|
+
<body>
|
|
1181
|
+
<h1>\u{1F9F9} prunify \u2014 Code Health Report</h1>
|
|
1182
|
+
<small>Generated ${(/* @__PURE__ */ new Date()).toISOString()}</small>
|
|
1183
|
+
<h2>Summary</h2>
|
|
1184
|
+
<table>
|
|
1185
|
+
<tr><th>Check</th><th>Found</th></tr>
|
|
1186
|
+
${rows}
|
|
1187
|
+
</table>
|
|
1188
|
+
<h2>Dead Files</h2>
|
|
1189
|
+
${deadList}
|
|
1190
|
+
</body>
|
|
1191
|
+
</html>`;
|
|
1192
|
+
import_node_fs8.default.mkdirSync(import_node_path8.default.dirname(outputPath), { recursive: true });
|
|
1193
|
+
import_node_fs8.default.writeFileSync(outputPath, html, "utf-8");
|
|
1194
|
+
}
|
|
1195
|
+
//# sourceMappingURL=cli.cjs.map
|