epd
Version:
Enhanced peer dependency resolution for npm, yarn, and pnpm
236 lines ⢠9.63 kB
JavaScript
import fs from "fs/promises";
import path from "path";
// Import glob with fallback
let globPromise;
try {
const { glob } = await import("glob");
globPromise = glob;
}
catch (error) {
// Fallback implementation without glob
globPromise = async (pattern, options) => {
const fs = await import('fs/promises');
const path = await import('path');
// Simple recursive file finder
async function findFiles(dir, ext) {
const files = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.includes('node_modules')) {
files.push(...await findFiles(fullPath, ext));
}
else if (entry.isFile() && entry.name.endsWith(ext)) {
files.push(fullPath);
}
}
}
catch (e) { }
return files;
}
const ext = pattern.replace('**/*', '');
return findFiles(options.cwd || process.cwd(), ext);
};
}
// Common file extensions to scan
const FILE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx", ".vue", ".svelte", ".mjs", ".cjs", ".mts", ".cts"];
// Files and directories to ignore
const IGNORE_PATTERNS = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/coverage/**", "**/.git/**"];
// Special dependencies that might not be directly imported
const SPECIAL_DEPENDENCIES = new Set([
"webpack", "babel", "eslint", "prettier", "typescript", "jest", "mocha", "chai"
]);
/**
* Scans the codebase to identify unused dependencies
*/
export async function scanUnusedDependencies(options = {}) {
const { directory = process.cwd(), includeDevDependencies = true, includePeerDependencies = false, includeOptionalDependencies = false, ignoreSpecialDependencies = true, verbose = false, } = options;
console.log("š Scanning codebase for unused dependencies...");
try {
// Step 1: Read package.json to get dependencies
const packageJson = await readPackageJsonFile(directory);
const declaredDependencies = getDeclaredDependenciesFromPackage(packageJson, includeDevDependencies, includePeerDependencies, includeOptionalDependencies);
if (verbose) {
console.log(`š¦ Found ${Object.keys(declaredDependencies).length} declared dependencies`);
}
// Step 2: Find all source files
const sourceFiles = await findAllSourceFiles(directory);
if (verbose) {
console.log(`š Found ${sourceFiles.length} source files to scan`);
}
// Step 3: Scan files for imports/requires
const usedDependencies = await findDependenciesInFiles(sourceFiles, directory);
if (verbose) {
console.log(`š Found ${usedDependencies.size} dependencies used in code`);
}
// Step 4: Check for dependencies used in scripts
const scriptDependencies = findDependenciesInScripts(packageJson);
scriptDependencies.forEach((dep) => usedDependencies.add(dep));
// Step 5: Identify unused dependencies
const unusedDependencies = {};
const potentiallyUnusedDependencies = {};
for (const [name, info] of Object.entries(declaredDependencies)) {
if (!usedDependencies.has(name)) {
if (ignoreSpecialDependencies && SPECIAL_DEPENDENCIES.has(name)) {
potentiallyUnusedDependencies[name] = info;
}
else {
unusedDependencies[name] = info;
}
}
}
return {
unused: unusedDependencies,
potentiallyUnused: potentiallyUnusedDependencies,
total: Object.keys(declaredDependencies).length,
scannedFiles: sourceFiles.length,
};
}
catch (error) {
console.error("ā Error scanning for unused dependencies:", error);
throw error;
}
}
async function readPackageJsonFile(directory) {
const packageJsonPath = path.join(directory, "package.json");
const content = await fs.readFile(packageJsonPath, "utf-8");
return JSON.parse(content);
}
function getDeclaredDependenciesFromPackage(packageJson, includeDevDependencies, includePeerDependencies, includeOptionalDependencies) {
const dependencies = {};
// Regular dependencies
if (packageJson.dependencies) {
for (const [name, version] of Object.entries(packageJson.dependencies)) {
dependencies[name] = { version, type: "dependency" };
}
}
// Dev dependencies
if (includeDevDependencies && packageJson.devDependencies) {
for (const [name, version] of Object.entries(packageJson.devDependencies)) {
dependencies[name] = { version, type: "devDependency" };
}
}
// Peer dependencies
if (includePeerDependencies && packageJson.peerDependencies) {
for (const [name, version] of Object.entries(packageJson.peerDependencies)) {
dependencies[name] = { version, type: "peerDependency" };
}
}
// Optional dependencies
if (includeOptionalDependencies && packageJson.optionalDependencies) {
for (const [name, version] of Object.entries(packageJson.optionalDependencies)) {
dependencies[name] = { version, type: "optionalDependency" };
}
}
return dependencies;
}
async function findAllSourceFiles(directory) {
const files = [];
for (const ext of FILE_EXTENSIONS) {
try {
const matches = await globPromise(`**/*${ext}`, {
cwd: directory,
ignore: IGNORE_PATTERNS,
absolute: true,
});
files.push(...matches);
}
catch (error) {
// Continue if pattern fails
}
}
return [...new Set(files)];
}
async function findDependenciesInFiles(files, directory) {
const dependencies = new Set();
for (const file of files) {
try {
const content = await fs.readFile(file, "utf-8");
const fileDeps = extractDependenciesFromContent(content);
fileDeps.forEach(dep => dependencies.add(dep));
}
catch (error) {
// Skip files that can't be read
}
}
return dependencies;
}
function extractDependenciesFromContent(content) {
const dependencies = [];
// Match import statements
const importRegex = /import\s+.*?\s+from\s+['"`]([^'"`]+)['"`]/g;
// Match require statements
const requireRegex = /require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
const dep = extractPackageName(match[1]);
if (dep)
dependencies.push(dep);
}
while ((match = requireRegex.exec(content)) !== null) {
const dep = extractPackageName(match[1]);
if (dep)
dependencies.push(dep);
}
return dependencies;
}
function extractPackageName(importPath) {
// Skip relative imports
if (importPath.startsWith('.') || importPath.startsWith('/')) {
return null;
}
// Handle scoped packages
if (importPath.startsWith('@')) {
const parts = importPath.split('/');
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
}
// Handle regular packages
return importPath.split('/')[0];
}
function findDependenciesInScripts(packageJson) {
const dependencies = new Set();
if (packageJson.scripts) {
for (const script of Object.values(packageJson.scripts)) {
// Simple extraction of command names from scripts
const commands = script.split(/[;&|]/).map(cmd => cmd.trim().split(' ')[0]);
commands.forEach(cmd => {
if (cmd && !cmd.startsWith('./') && !cmd.startsWith('npm') && !cmd.startsWith('node')) {
dependencies.add(cmd);
}
});
}
}
return dependencies;
}
export function generateUnusedDependenciesReport(result) {
const report = {
unusedCount: Object.keys(result.unused).length,
potentiallyUnusedCount: Object.keys(result.potentiallyUnused).length,
totalDependencies: result.total,
filesScanned: result.scannedFiles,
};
console.log(`\nš Dependency Scan Report:`);
console.log(` Total dependencies: ${report.totalDependencies}`);
console.log(` Files scanned: ${report.filesScanned}`);
console.log(` Unused dependencies: ${report.unusedCount}`);
console.log(` Potentially unused: ${report.potentiallyUnusedCount}`);
if (report.unusedCount > 0) {
console.log(`\nšļø Unused dependencies:`);
for (const [name, info] of Object.entries(result.unused)) {
console.log(` - ${name} (${info.type})`);
}
}
if (report.potentiallyUnusedCount > 0) {
console.log(`\nā ļø Potentially unused dependencies:`);
for (const [name, info] of Object.entries(result.potentiallyUnused)) {
console.log(` - ${name} (${info.type})`);
}
}
return report;
}
export async function calculateDiskSpaceSavings(unusedDeps, directory) {
// Simplified calculation - in reality would need to check node_modules sizes
return Object.keys(unusedDeps).length * 0.5; // Rough estimate of 0.5MB per package
}
//# sourceMappingURL=dependency-scanner.js.map