depshield
Version:
Smart Dependency Analyzer & Optimizer - Find unused npm packages, reduce bundle size, and improve project health with AST-based detection.
167 lines (166 loc) • 8 kB
JavaScript
import { Command } from 'commander';
import chalk from 'chalk';
const program = new Command();
program
.name('depshield')
.description('Smart Dependency Analyzer & Optimizer')
.version('1.0.0');
program
.command('scan')
.description('Scan the current project for unused dependencies')
.option('-p, --path <path>', 'Path to project root', process.cwd())
.option('-j, --json', 'Output results in JSON format')
.option('-w, --workspace', 'Scan all packages in a monorepo workspace')
.option('--audit', 'Run security audit alongside dependency scan')
.action(async (options) => {
const startTime = Date.now();
console.log(chalk.blue('🛡️ DepShield: Starting scan...'));
console.log(chalk.gray(`Scanning path: ${options.path}\n`));
try {
// Static imports for core modules
const { Scanner } = await import('./core/scanner.js');
const { Parser } = await import('./core/parser.js');
const { Analyzer } = await import('./core/analyzer.js');
const { Renderer } = await import('./cli/renderer.js');
const { Auditor } = await import('./core/auditor.js');
const { WorkspaceManager } = await import('./core/workspace.js');
const { getPackageSizes } = await import('./utils/package-size.js');
const ora = (await import('ora')).default;
const renderer = new Renderer();
if (options.workspace) {
const workspaceManager = new WorkspaceManager(options.path);
const packages = await workspaceManager.getPackages();
if (packages.length === 0) {
const spinner = ora('Scanning workspace...').start();
spinner.fail('No workspace packages found.');
process.exit(1);
}
console.log(chalk.green(`✔ Found ${packages.length} workspace packages.`));
let hasIssues = false;
const rootScanner = new Scanner({ path: options.path });
const rootConfig = await rootScanner.getConfig();
for (const pkg of packages) {
console.log(chalk.bold(`\n📦 Scanning workspace package: ${chalk.cyan(pkg.name)}`));
const pkgScanner = new Scanner({ path: pkg.path });
const pkgSpinner = ora(`Analyzing ${pkg.name}...`).start();
try {
const packageJson = await pkgScanner.readPackageJson();
const pkgConfig = await pkgScanner.getConfig();
const files = await pkgScanner.getSourceFiles();
if (files.length === 0) {
pkgSpinner.warn(`No source files found in ${pkg.name}. Skipping.`);
continue;
}
const parser = new Parser();
const usedDependencies = new Set();
for (const file of files) {
const deps = await parser.parseFile(file);
deps.forEach(d => usedDependencies.add(d));
}
const sizeMap = new Map();
const pkgAnalyzer = new Analyzer(pkgConfig);
const analysisResult = pkgAnalyzer.analyze(packageJson, usedDependencies, sizeMap);
pkgSpinner.succeed(`Analyzed ${pkg.name}`);
renderer.renderResults(analysisResult, packageJson, files.length, 0, { json: options.json });
if (analysisResult.unusedDependencies.length > 0 || analysisResult.missingDependencies.length > 0) {
hasIssues = true;
}
}
catch (err) {
pkgSpinner.fail(`Failed to scan ${pkg.name}: ${err.message}`);
}
}
if (rootConfig.strictMode && hasIssues) {
process.exit(1);
}
}
else {
const spinner = ora('Reading package.json...').start();
const scanner = new Scanner({ path: options.path });
const packageJson = await scanner.readPackageJson();
const config = await scanner.getConfig();
spinner.succeed(`Read package.json (${packageJson.name}{packageJson.version})`);
spinner.start('Finding source files...');
const files = await scanner.getSourceFiles();
spinner.succeed(`Found ${files.length} source files`);
spinner.start('Parsing files...');
const parser = new Parser();
const usedDependencies = new Set();
for (const file of files) {
const deps = await parser.parseFile(file);
deps.forEach(d => usedDependencies.add(d));
}
spinner.succeed(`Parsed files. Found ${usedDependencies.size} unique imports.`);
spinner.start('Analyzing dependencies...');
const analyzer = new Analyzer(config);
const prelimResult = analyzer.analyze(packageJson, usedDependencies);
// Fetch package sizes for unused dependencies
const unusedPackages = [
...prelimResult.unusedDependencies,
...prelimResult.unusedDevDependencies,
];
let sizeMap = new Map();
if (unusedPackages.length > 0) {
spinner.text = 'Fetching package sizes...';
const packagesToFetch = unusedPackages.map(dep => ({
name: dep.name,
version: dep.version.replace(/^[^0-9]*/, ''),
}));
const sizes = await getPackageSizes(packagesToFetch);
sizes.forEach((info, name) => {
sizeMap.set(name, info.size);
});
}
const analysisResult = analyzer.analyze(packageJson, usedDependencies, sizeMap);
spinner.succeed('Analysis complete.');
const duration = Date.now() - startTime;
renderer.renderResults(analysisResult, packageJson, files.length, duration, { json: options.json });
if (config.strictMode && (analysisResult.unusedDependencies.length > 0 || analysisResult.missingDependencies.length > 0)) {
process.exit(1);
}
}
if (options.audit) {
const auditor = new Auditor();
console.log(chalk.bold('\n🛡️ Running Security Audit...'));
const auditResult = await auditor.audit(options.path);
if (options.json) {
console.error(chalk.yellow('Note: Audit results are not yet included in JSON output for `scan --audit`. Use `depshield audit` for dedicated JSON report.'));
}
else {
renderer.renderAudit(auditResult);
}
}
}
catch (error) {
console.error(chalk.red('\n❌ Error during scan:'), error.message);
process.exit(1);
}
});
program
.command('audit')
.description('Run security vulnerability audit')
.option('-p, --path <path>', 'Path to project root', process.cwd())
.option('-j, --json', 'Output results in JSON format')
.action(async (options) => {
try {
const { Auditor } = await import('./core/auditor.js');
const { Renderer } = await import('./cli/renderer.js');
const ora = (await import('ora')).default;
const spinner = ora('Running security audit...').start();
const auditor = new Auditor();
const renderer = new Renderer();
const result = await auditor.audit(options.path);
spinner.succeed('Security audit complete.');
if (options.json) {
console.log(JSON.stringify(result, null, 2));
}
else {
renderer.renderAudit(result);
}
}
catch (error) {
console.error(chalk.red(`\n❌ Error during audit: ${error.message}`));
process.exit(1);
}
});
program.parse();