devghost
Version:
👻 Find dead code, dead imports, and dead dependencies before they haunt your project
210 lines • 7.29 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeFiles = analyzeFiles;
const path = __importStar(require("node:path"));
const ts = __importStar(require("typescript"));
const fs_1 = require("../utils/fs");
const tsparser_1 = require("../utils/tsparser");
/**
* Build a dependency graph of which files import which
*/
function buildDependencyGraph(files) {
const graph = new Map();
for (const file of files) {
const dependencies = new Set();
const sourceFile = (0, tsparser_1.parseFile)(file);
if (!sourceFile) {
continue;
}
// Extract all import paths
sourceFile.forEachChild((node) => {
if (ts.isImportDeclaration(node)) {
const moduleSpecifier = node.moduleSpecifier.text;
// Only track relative imports (actual project files)
if (moduleSpecifier.startsWith('.')) {
const importedFilePath = resolveImportPath(file, moduleSpecifier, files);
if (importedFilePath) {
dependencies.add(importedFilePath);
}
}
}
// Also check dynamic imports
if (ts.isCallExpression(node)) {
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
const arg = node.arguments[0];
if (ts.isStringLiteral(arg)) {
const moduleSpecifier = arg.text;
if (moduleSpecifier.startsWith('.')) {
const importedFilePath = resolveImportPath(file, moduleSpecifier, files);
if (importedFilePath) {
dependencies.add(importedFilePath);
}
}
}
}
}
});
graph.set(file, dependencies);
}
return graph;
}
/**
* Resolve an import path to an absolute file path
*/
function resolveImportPath(fromFile, importPath, allFiles) {
const fromDir = path.dirname(fromFile);
const extensions = [
'.ts',
'.tsx',
'.js',
'.jsx',
'/index.ts',
'/index.tsx',
'/index.js',
'/index.jsx',
];
// Try with original extension (if any)
let resolved = path.resolve(fromDir, importPath);
let normalized = path.normalize(resolved);
if (allFiles.includes(normalized)) {
return normalized;
}
// Try with different extensions
for (const ext of extensions) {
resolved = path.resolve(fromDir, importPath + ext);
normalized = path.normalize(resolved);
if (allFiles.includes(normalized)) {
return normalized;
}
}
return null;
}
/**
* Find all files that are imported (directly or transitively) from entry points
*/
function findReachableFiles(entryPoints, graph) {
const reachable = new Set();
const queue = [...entryPoints];
while (queue.length > 0) {
const current = queue.shift();
if (!current)
continue;
if (reachable.has(current)) {
continue;
}
reachable.add(current);
const dependencies = graph.get(current) || new Set();
for (const dep of dependencies) {
if (!reachable.has(dep)) {
queue.push(dep);
}
}
}
return reachable;
}
/**
* Identify entry point files
*/
function identifyEntryPoints(files, configuredEntryPoints, _projectRoot) {
if (configuredEntryPoints && configuredEntryPoints.length > 0) {
const entryPoints = [];
for (const ep of configuredEntryPoints) {
// If entry point is relative, try to find matching absolute path
for (const file of files) {
if (file.endsWith(ep) || file.endsWith(path.normalize(ep))) {
entryPoints.push(file);
break;
}
}
}
return entryPoints;
}
// Default entry points
const defaultEntryPoints = [
'src/index.ts',
'src/index.tsx',
'src/index.js',
'src/index.jsx',
'src/main.ts',
'src/main.tsx',
'src/app.ts',
'src/app.tsx',
'index.ts',
'index.js',
];
const entryPoints = [];
for (const file of files) {
for (const entryPoint of defaultEntryPoints) {
if (file.endsWith(entryPoint) || file.endsWith(entryPoint.replace('src/', ''))) {
entryPoints.push(file);
}
}
}
// If no entry points found, consider all files as potential entry points
// This is safer than marking everything as unused
if (entryPoints.length === 0) {
return files;
}
return entryPoints;
}
/**
* Analyze files to find unused/orphaned files
*/
async function analyzeFiles(files, configuredEntryPoints) {
const unusedFiles = [];
// Filter out ignored files
const validFiles = files.filter((f) => !(0, fs_1.isFileIgnored)(f));
// Build dependency graph
const graph = buildDependencyGraph(validFiles);
// Identify entry points
const entryPoints = identifyEntryPoints(validFiles, configuredEntryPoints);
// Find all reachable files from entry points
const reachable = findReachableFiles(entryPoints, graph);
// Any file not reachable is unused
for (const file of validFiles) {
if (!reachable.has(file) && !entryPoints.includes(file)) {
const stats = (0, fs_1.getFileStats)(file);
unusedFiles.push({
path: file,
reason: 'Not imported by any other file',
size: stats.size,
lines: stats.lines,
});
}
}
return unusedFiles;
}
//# sourceMappingURL=files.js.map