dependency-insight
Version:
A CLI tool to audit and analyze your project's dependencies.
715 lines (628 loc) • 22.2 kB
JavaScript
/**
* Dependency Insight CLI
* A tool for analyzing and managing project dependencies
*/
// Core Node.js imports
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const https = require("https");
// Third-party dependencies
const chalk = require("chalk");
const depcheck = require("depcheck");
const importInquirer = async () => {
try {
const module = await import('inquirer');
return module.default;
} catch (error) {
console.error(chalk.red("Error: The 'inquirer' package is required but not installed."));
console.log(chalk.yellow("Please install it using: npm install inquirer"));
process.exit(1);
}
};
// =====================================
// Utility Functions
// =====================================
const execCommand = (command) => {
try {
const result = execSync(command, { encoding: "utf-8", stdio: "pipe" });
return command.includes("--json") ? JSON.parse(result) : result;
} catch (error) {
if (error.stdout && command.includes("--json")) {
try {
return JSON.parse(error.stdout);
} catch (e) {}
}
console.error(chalk.red(`Error executing: ${command}`));
console.error(error.message);
if (error.stdout) console.error(chalk.yellow(`Stdout: ${error.stdout}`));
if (error.stderr) console.error(chalk.yellow(`Stderr: ${error.stderr}`));
return null;
}
};
// =====================================
// API Interactions
// =====================================
/**
* Makes HTTP GET request with rate limit handling
* @param {string} url - API endpoint URL
* @returns {Promise<object|null>} Parsed JSON response
*/
const makeRequest = (url) => {
return new Promise((resolve, reject) => {
https
.get(
url,
{
headers: { "User-Agent": "dep-insight-cli" },
},
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
if (res.statusCode === 429) {
console.log(
chalk.red("Rate limit reached. Please try again later.")
);
resolve(null);
} else if (res.statusCode === 200) {
try {
resolve(JSON.parse(data));
} catch (e) {
resolve(null);
}
} else {
resolve(null);
}
});
}
)
.on("error", () => resolve(null));
});
};
const checkDownloads = async (pkg) => {
try {
const data = await makeRequest(
`https://api.npmjs.org/downloads/point/last-month/${pkg}`
);
return data?.downloads;
} catch (e) {
return null;
}
};
const checkGitHub = async (pkg) => {
try {
const pkgPath = path.join(
process.cwd(),
"node_modules",
pkg,
"package.json"
);
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
const repoUrl = pkgJson.repository?.url || pkgJson.repository;
if (repoUrl) {
const githubUrl = repoUrl
.replace("git+", "")
.replace(".git", "")
.replace("git:", "https:");
const apiUrl = githubUrl.replace("github.com", "api.github.com/repos");
await new Promise((resolve) => setTimeout(resolve, 100));
const data = await makeRequest(apiUrl);
if (!data) return null;
return {
stars: data.stargazers_count,
issues: data.open_issues_count,
updated: data.updated_at,
};
}
return null;
} catch (e) {
return null;
}
};
// =====================================
// Core Commands
// =====================================
// Command: Audit dependencies for vulnerabilities
const auditDependencies = () => {
console.log(chalk.blue("Auditing dependencies for vulnerabilities...\n"));
const result = execCommand("npm audit --json");
if (result) {
// Show summary counts first
const issues = result.metadata.vulnerabilities;
console.log(chalk.bold("Summary:"));
console.log(
chalk.green(
`Low: ${issues.low}, Moderate: ${issues.moderate}, High: ${issues.high}, Critical: ${issues.critical}\n`
)
);
// Show detailed vulnerabilities
if (result.advisories) {
console.log(chalk.bold("Details:"));
Object.values(result.advisories).forEach(advisory => {
console.log(chalk.dim("─".repeat(60)));
console.log(`${chalk.red(advisory.title)} (${chalk.yellow(advisory.severity)})`);
console.log(`Vulnerable package: ${chalk.cyan(advisory.module_name)}`);
console.log(`Patched in: ${chalk.green(advisory.patched_versions)}`);
console.log(`Path: ${advisory.findings[0]?.paths[0] || 'N/A'}`);
console.log(`More info: ${chalk.blue(advisory.url)}\n`);
});
}
// Show fix recommendations
if (result.metadata.vulnerabilities.total > 0) {
console.log(chalk.yellow("\nRecommended actions:"));
console.log(chalk.dim("Run 'npm audit fix' to automatically fix fixable vulnerabilities"));
console.log(chalk.dim("Run 'npm audit fix --force' to force fixes (may include breaking changes)"));
}
}
};
// Command: Check outdated dependencies
const checkOutdated = () => {
console.log(chalk.blue("Checking for outdated dependencies...\n"));
const result = execCommand("npm outdated --json");
if (result) {
if (Object.keys(result).length === 0) {
console.log(chalk.green("All dependencies are up to date!"));
} else {
console.log(chalk.yellow("Outdated dependencies: Current → Latest (Suggested)\n"));
Object.entries(result).forEach(([dep, info]) => {
console.log(
`${chalk.bold(dep)}: ${chalk.red(info.current)} → ${chalk.green(
info.latest
)} (${chalk.yellow(info.wanted)})`
);
});
}
}
};
// Command: Prune unused dependencies
const pruneDependencies = async () => {
console.log(chalk.blue("Checking for unused dependencies...\n"));
const packageJsonPath = path.join(process.cwd(), "package.json");
if (!fs.existsSync(packageJsonPath)) {
console.error(chalk.red("No package.json found in the current directory!"));
return;
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
const dependencies = Object.keys(packageJson.dependencies || {});
const devDependencies = Object.keys(packageJson.devDependencies || {});
console.log(chalk.yellow("Running analysis..."));
depcheck(process.cwd(), { ignoreDirs: ["node_modules"] }, async (unused) => {
const unusedDeps = unused.dependencies;
const unusedDevDeps = unused.devDependencies;
if (unusedDeps.length || unusedDevDeps.length) {
console.log(chalk.red("Unused dependencies found:\n"));
unusedDeps.forEach((dep) => console.log(chalk.red(`- ${dep}`)));
unusedDevDeps.forEach((dep) => console.log(chalk.red(`- ${dep} (dev)`)));
// Get inquirer instance
const inquirer = await importInquirer();
const { shouldUninstall } = await inquirer.prompt({
type: "confirm",
name: "shouldUninstall",
message: "Would you like to uninstall unused dependencies?",
default: false
});
if (shouldUninstall) {
const choices = [
...unusedDeps.map(dep => ({ name: dep, value: dep, type: "prod" })),
...unusedDevDeps.map(dep => ({ name: `${dep} (dev)`, value: dep, type: "dev" }))
];
const { selected } = await inquirer.prompt({
type: "checkbox",
name: "selected",
message: "Select dependencies to uninstall:",
choices
});
if (selected.length) {
console.log(chalk.blue("\nUninstalling dependencies...\n"));
const startTime = Date.now();
for (const dep of selected) {
process.stdout.write(chalk.yellow(`Uninstalling ${dep}... `));
try {
execCommand(`npm uninstall ${dep}`);
console.log(chalk.green("✓"));
} catch (error) {
console.log(chalk.red("✗"));
console.log(
chalk.red(`Error uninstalling ${dep}: ${error.message}`)
);
}
}
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(
chalk.green(
`\n✨ Successfully uninstalled ${selected.length} package(s) in ${duration}s`
)
);
} else {
console.log(
chalk.yellow("\nNo packages selected for uninstallation.")
);
}
}
} else {
console.log(chalk.green("No unused dependencies found!"));
}
});
};
// Command: Visualize dependency tree
const visualizeTree = () => {
console.log(chalk.blue("Visualizing dependency tree...\n"));
const packageJsonPath = path.join(process.cwd(), "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
// Use npm list to get dependency tree
const result = execCommand("npm list --json");
if (!result) return;
const printDependencyTree = (deps, level = 0) => {
const indent = " ".repeat(level);
Object.entries(deps).forEach(([name, info]) => {
if (name === "dependencies") {
printDependencyTree(info, level);
return;
}
const version = info.version || "unknown";
console.log(`${indent}${chalk.bold(name)}@${chalk.green(version)}`);
// Check for peer dependencies
const depPath = path.join(
process.cwd(),
"node_modules",
name,
"package.json"
);
try {
const depPackage = JSON.parse(fs.readFileSync(depPath, "utf-8"));
if (depPackage.peerDependencies) {
Object.entries(depPackage.peerDependencies).forEach(
([peer, range]) => {
console.log(
`${indent} ${chalk.yellow("└─")} ${chalk.dim(
`requires ${peer}@${range}`
)}`
);
}
);
}
} catch (e) {}
// Recurse into nested dependencies
if (info.dependencies) {
printDependencyTree(info.dependencies, level + 1);
}
});
};
console.log(
`${chalk.bold(packageJson.name)}@${chalk.green(packageJson.version)}`
);
printDependencyTree(result);
};
// =====================================
// Analysis Commands
// =====================================
// Command: Analyze bundle size
const analyzeSize = () => {
console.log(chalk.blue("Analyzing dependency sizes...\n"));
const result = execCommand("npm list --json");
if (!result) return;
// Get total size of directory recursively
const getTotalSizeInMB = (dirPath) => {
let totalSize = 0;
try {
const files = fs.readdirSync(dirPath);
for (const file of files) {
// Skip certain files/directories
if (file === "." || file === ".." || file === ".git") continue;
const filePath = path.join(dirPath, file);
const stats = fs.statSync(filePath);
if (stats.isSymbolicLink()) {
// Handle symlinks differently
continue;
} else if (stats.isDirectory()) {
const dirSize = getTotalSizeInMB(filePath);
totalSize += dirSize * 1024 * 1024; // Convert MB back to bytes
} else if (stats.isFile()) {
totalSize += stats.size;
}
}
} catch (e) {
console.error(chalk.red(`Error reading ${dirPath}: ${e.message}`));
return 0;
}
return (totalSize / (1024 * 1024)).toFixed(2); // Convert bytes to MB
};
// Get package sizes with validation
const packageSizes = Object.keys(result.dependencies)
.map((dep) => {
const depPath = path.join(process.cwd(), "node_modules", dep);
if (!fs.existsSync(depPath)) {
console.warn(chalk.yellow(`Warning: ${dep} not found in node_modules`));
return { name: dep, size: 0 };
}
const size = getTotalSizeInMB(depPath);
return { name: dep, size: parseFloat(size) };
})
.filter((pkg) => pkg.size > 0) // Remove zero-size packages
.sort((a, b) => b.size - a.size);
// Display results with improved formatting
packageSizes.forEach(({ name, size }) => {
let color = chalk.green; // < 1MB
if (size > 10) color = chalk.red; // > 10MB
else if (size > 5) color = chalk.yellow; // 5-10MB
const sizeString = size.toFixed(2).padStart(6);
console.log(`${chalk.bold(name.padEnd(30))} ${color(sizeString + " MB")}`);
});
// Show total size and package count
const totalSize = packageSizes.reduce((sum, pkg) => sum + pkg.size, 0);
console.log(`\n${chalk.blue("Total packages:")} ${packageSizes.length}`);
console.log(
`${chalk.blue("Total size:")} ${chalk.bold(totalSize.toFixed(2) + " MB")}`
);
};
// Suggesting lightweight alternatives for heavy dependencies
const suggestAlternatives = () => {
console.log(chalk.blue("Suggesting lightweight alternatives...\n"));
const largeDeps = {
moment: "date-fns",
luxon: "dayjs",
lodash: "lodash-es",
ramda: "ramda-adjunct",
axios: "fetch",
superagent: "undici",
"string.js": "string",
"sprintf-js": "tiny-sprintf",
jquery: "vanilla JS",
zepto: "vanilla JS",
"chart.js": "chartist",
d3: "chart.js",
validator: "is.js",
joi: "yup",
redux: "valtio",
mobx: "effector",
"moment-timezone": "timezone-mock",
fullcalendar: "flatpickr",
animejs: "gsap",
"socket.io": "ws",
"react-router": "wouter",
highcharts: "chart.js",
"plotly.js": "chartist",
dropzone: "fine-uploader",
mathjs: "decimal.js",
numeral: "vanilla JS",
sharp: "image-size",
fabric: "konva",
bootstrap: "bulma",
tailwindcss: "tachyons",
};
const result = execCommand("npm ls --json");
if (result) {
const root = result.dependencies;
const suggestions = [];
Object.entries(root).forEach(([dep]) => {
if (largeDeps[dep]) {
suggestions.push(`Consider using ${largeDeps[dep]} instead of ${dep}`);
}
});
if (suggestions.length === 0) {
console.log(chalk.green("No suggestions at the moment."));
} else {
suggestions.forEach((msg) => console.log(chalk.yellow(msg)));
}
}
};
// Command: Check project health
const checkHealth = async () => {
console.log(chalk.blue("Checking dependency health...\n"));
console.log(
chalk.yellow(
"Note: GitHub API has a rate limit of 60 requests per hour for unauthenticated requests.\n"
)
);
const packageJsonPath = path.join(process.cwd(), "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
// Column formatting
const COLS = {
label: 18,
value: 15,
};
const pad = (str, len) => str.toString().padEnd(len);
const padLeft = (str, len) => str.toString().padStart(len);
const formatNumber = (num) => {
if (!num) return "N/A".padStart(COLS.value);
return num.toLocaleString("en-IN").padStart(COLS.value);
};
console.log(chalk.yellow("Analyzing dependencies health metrics...\n"));
for (const [pkg, version] of Object.entries(dependencies)) {
console.log(
chalk.dim("──────────────────────────────────────────────────")
);
console.log(
`${chalk.bold.blue(pkg)} ${chalk.dim("@")}${chalk.cyan(version)}`
);
// Get downloads
const downloads = await checkDownloads(pkg);
if (downloads) {
const color =
downloads > 1000000
? chalk.green
: downloads > 100000
? chalk.yellow
: chalk.red;
console.log(
`${chalk.dim(pad("Monthly downloads:", COLS.label))}${color(
formatNumber(downloads)
)}`
);
} else {
console.log(
`${chalk.dim(pad("Monthly downloads:", COLS.label))}${chalk.dim("N/A")}`
);
}
// Get GitHub stats
const stats = await checkGitHub(pkg);
if (stats) {
const starsColor =
stats.stars > 10000
? chalk.green
: stats.stars > 1000
? chalk.yellow
: chalk.white;
const issuesColor =
stats.issues < 100
? chalk.green
: stats.issues < 500
? chalk.yellow
: chalk.red;
console.log(
`${chalk.dim(pad("GitHub stars:", COLS.label))}${starsColor(
formatNumber(stats.stars)
)}`
);
console.log(
`${chalk.dim(pad("Open issues:", COLS.label))}${issuesColor(
formatNumber(stats.issues)
)}`
);
const date = new Date(stats.updated).toLocaleDateString("en-GB", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
console.log(
`${chalk.dim(pad("Last updated:", COLS.label))}${chalk.cyan(
padLeft(date, COLS.value)
)}`
);
} else {
console.log(
`${chalk.dim(pad("GitHub stats:", COLS.label))}${chalk.dim("N/A")}`
);
}
console.log(""); // Empty line between packages
}
console.log(chalk.dim("──────────────────────────────────────────────────"));
};
// =====================================
// Maintenance & CLI Commands
// =====================================
// Command: Interactive update for dependencies
const interactiveUpdate = async () => {
// Get inquirer instance using the importInquirer helper
const inquirer = await importInquirer();
const outdated = execCommand("npm outdated --json");
if (!outdated || Object.keys(outdated).length === 0) {
console.log(chalk.green("All dependencies are up to date!"));
return;
}
const choices = Object.entries(outdated).map(([dep, info]) => ({
name: `${dep}: ${info.current} → ${info.latest}`,
value: { name: dep, version: info.latest },
}));
const { selected } = await inquirer.prompt({
type: "checkbox",
name: "selected",
message: "Select dependencies to update:",
choices,
});
if (selected.length) {
console.log(chalk.blue("\nUpdating dependencies...\n"));
const startTime = Date.now();
for (const dep of selected) {
process.stdout.write(
chalk.yellow(`Installing ${dep.name}@${dep.version}... `)
);
try {
execCommand(`npm install ${dep.name}@${dep.version}`);
console.log(chalk.green("✓"));
} catch (error) {
console.log(chalk.red("✗"));
console.log(chalk.red(`Error updating ${dep.name}: ${error.message}`));
}
}
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(
chalk.green(
`\n✨ Successfully updated ${selected.length} package(s) in ${duration}s`
)
);
} else {
console.log(chalk.yellow("No packages selected for update."));
}
};
// Command: Clear npm cache
const clearCache = async () => {
const inquirer = await importInquirer();
console.log(chalk.yellow("Warning: This will clear your npm cache completely.\n"));
const { confirmed } = await inquirer.prompt({
type: "confirm",
name: "confirmed",
message: "Are you sure you want to clear the npm cache?",
default: false
});
if (confirmed) {
console.log(chalk.blue("\nClearing npm cache..."));
execCommand("npm cache clean --force");
console.log(chalk.green("✨ Successfully cleared npm cache"));
} else {
console.log(chalk.yellow("\nOperation aborted"));
}
};
// CLI Options
const main = async () => {
const args = process.argv.slice(2);
switch (args[0]) {
case "audit":
auditDependencies();
break;
case "outdated":
checkOutdated();
break;
case "prune":
pruneDependencies();
break;
case "tree":
visualizeTree();
break;
case "suggest":
suggestAlternatives();
break;
case "size":
analyzeSize();
break;
case "health":
await checkHealth();
console.log(
chalk.yellow(
"Note: GitHub API has a rate limit of 60 requests per hour for unauthenticated requests.\n"
)
);
break;
case "update":
interactiveUpdate();
break;
case "clear-cache":
clearCache();
break;
default:
console.log(chalk.blue("Dependency Insight CLI"));
console.log("Usage:");
console.log(" audit - Audit dependencies for vulnerabilities");
console.log(" outdated - Check for outdated dependencies");
console.log(" update - Interactive update for dependencies");
console.log(" prune - Check for unused dependencies");
console.log(" tree - Visualize dependency tree");
console.log(
" suggest - Suggest lightweight alternatives for heavy dependencies"
);
console.log(" size - Analyze bundle size");
console.log(" health - Check project health");
console.log(" clear-cache - Clear npm cache");
break;
}
};
main().catch(error => {
console.error(chalk.red("Fatal error:"), error);
process.exit(1);
});