depdrift
Version:
A tool to analyze dependency drift in JavaScript projects
340 lines (296 loc) • 9.01 kB
JavaScript
/**
* Unified Reporter module for displaying and saving drift analysis results
* @module formatters/reporter
*/
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
import { formatDriftLevel, formatSecuritySeverity } from '../utils/formatters.js';
import * as tableFormatter from './tableFormatter.js';
import * as textFormatter from './textFormatter.js';
/**
* Report dependency drift to console and/or file
* @param {Object} analysis - The complete analysis results
* @param {Object} options - Report options
* @param {boolean} [options.json=false] - Whether to output as JSON
* @param {string} [options.output] - Path to output file (if saving to file)
* @param {string} [options.format='table'] - Output format ('table', 'text', 'json', 'csv', 'html')
* @param {boolean} [options.showAll=false] - Whether to show all dependencies or only issues
* @param {string} [options.sortBy='drift'] - How to sort dependencies ('drift', 'security', 'name', 'days')
* @param {boolean} [options.compact=false] - Use compact table format
* @param {boolean} [options.includeSecurity=true] - Include security information
* @returns {Promise<void>}
*/
async function reportAnalysis (analysis, options = {}) {
const {
json = false,
output,
format = 'table',
showAll = false,
sortBy = 'drift',
compact = false,
includeSecurity = true
} = options;
if (!analysis || !analysis.dependencies) {
console.log(chalk.yellow('No analysis results to report.'));
if (output) {
await fs.writeJson(output, { dependencies: [] }, { spaces: 2 });
console.log(chalk.gray(`Empty report written to ${output}`));
}
return;
}
// Format the analysis based on requested format
let formattedOutput;
switch (format.toLowerCase()) {
case 'json':
formattedOutput = formatAnalysisJson(analysis, { pretty: true });
break;
case 'text':
formattedOutput = formatAnalysisText(analysis, {
includeDetails: true,
includeSecurity,
sortBy
});
break;
case 'csv':
formattedOutput = formatAnalysisCSV(analysis);
break;
case 'html':
// For future support of HTML output
throw new Error('HTML output format not yet implemented');
case 'table':
default:
// Get formatted tables using the tableFormatter
formattedOutput = tableFormatter.formatAnalysisAsTables(analysis, {
compact,
includeSecurity,
sortBy,
showAll
});
}
// Display output to console if not suppressed
if (!options.silent) {
console.log(formattedOutput);
}
// Save to file if output path provided
if (output) {
await saveReport(formattedOutput, output, format);
console.log(chalk.green(`\nReport saved to ${output}`));
}
return formattedOutput;
}
/**
* Generate a summary of analysis results
* @param {Object} analysis - Complete analysis results
* @returns {Object} Summary object
*/
function generateSummary (analysis) {
if (!analysis || !analysis.dependencies || analysis.dependencies.length === 0) {
return {
total: 0,
outdated: 0,
vulnerable: 0,
driftLevels: {},
securitySeverities: {}
};
}
const summary = {
total: analysis.dependencies.length,
outdated: 0,
vulnerable: 0,
driftLevels: {
none: 0,
low: 0,
medium: 0,
high: 0,
critical: 0
},
securitySeverities: {
none: 0,
low: 0,
medium: 0,
high: 0,
critical: 0
}
};
// Count by drift level and security severity
analysis.dependencies.forEach(dep => {
const driftLevel = dep.driftLevel || 'none';
const securitySeverity = dep.security?.highestSeverity || 'none';
// Count drift levels
summary.driftLevels[driftLevel] = (summary.driftLevels[driftLevel] || 0) + 1;
// Count if outdated
if (driftLevel !== 'none') {
summary.outdated++;
}
// Count security severities
summary.securitySeverities[securitySeverity] =
(summary.securitySeverities[securitySeverity] || 0) + 1;
// Count if vulnerable
if (securitySeverity !== 'none') {
summary.vulnerable++;
}
});
return summary;
}
/**
* Save report to file
* @param {string} content - Report content
* @param {string} filePath - Path to save report
* @param {string} format - Report format
* @returns {Promise<void>}
*/
async function saveReport (content, filePath, format) {
try {
// Create directory if it doesn't exist
await fs.ensureDir(path.dirname(filePath));
switch (format.toLowerCase()) {
case 'json':
// For json string, parse and then write to ensure proper formatting
const jsonObj = typeof content === 'string' ? JSON.parse(content) : content;
await fs.writeJson(filePath, jsonObj, { spaces: 2 });
break;
case 'csv':
case 'text':
case 'table':
case 'html':
default:
// For all other formats, write as string
await fs.writeFile(filePath, content);
}
} catch (error) {
throw new Error(`Failed to save report: ${error.message}`);
}
}
/**
* Get color function for drift status
* @param {string} status - Drift status
* @returns {Function} Chalk color function
*/
function getStatusColor (status) {
switch (status.toLowerCase()) {
case 'missing':
case 'major':
case 'extra':
case 'critical':
case 'high':
case 'peer-missing':
return chalk.red;
case 'minor':
case 'medium':
return chalk.yellow;
case 'patch':
case 'low':
return chalk.cyan;
case 'safe':
case 'none':
case 'optional-missing':
return chalk.green;
case 'unknown':
default:
return chalk.gray;
}
}
/**
* Legacy function for backward compatibility
* Report dependency drift records to console and/or file
* @param {Array} records - Array of dependency records to report
* @param {Object} options - Report options
* @returns {Promise<void>}
*/
async function reportDrift (records, options = {}) {
const {
json = false,
jsonFile = 'drift-report.json',
showAll = false,
sortBy = 'status',
sortDirection = 'desc',
title = 'Dependency Drift Report'
} = options;
console.log(chalk.yellow('Warning: reportDrift is deprecated. Please use reportAnalysis instead.'));
if (!records || !Array.isArray(records) || records.length === 0) {
console.log(chalk.yellow('No dependency drift records to report.'));
if (json) {
await fs.writeJson(jsonFile, { records: [] }, { spaces: 2 });
console.log(chalk.gray(`Empty report written to ${jsonFile}`));
}
return;
}
// Convert old-style records to new analysis format
const analysis = {
projectName: 'Project',
projectVersion: '1.0.0',
packageJsonPath: '',
timestamp: Date.now(),
dependencies: records.map(record => ({
name: record.package,
currentVersion: record.installed,
latestVersion: record.expected,
driftLevel: record.status.toLowerCase(),
daysBehind: 0
}))
};
// Use new reporter with old options
await reportAnalysis(analysis, {
json,
output: json ? jsonFile : undefined,
format: 'table',
showAll,
sortBy: sortBy === 'status' ? 'drift' : sortBy,
compact: false,
includeSecurity: false
});
}
/**
* Display recommendations in a formatted way
* @param {Array} recommendations - Array of recommendation objects
*/
function displayRecommendations(recommendations) {
if (!recommendations || recommendations.length === 0) {
console.log('No recommendations available.');
return;
}
console.log('\nRecommendations:');
for (const rec of recommendations) {
console.log(` - ${rec.name}: Update from ${rec.currentVersion} to ${rec.recommendedVersion} (${rec.priority} priority)`);
if (rec.reason) {
console.log(` Reason: ${rec.reason}`);
}
}
}
/**
* Display vulnerabilities in a formatted way
* @param {Array} vulnerabilities - Array of vulnerability objects
*/
function displayVulnerabilities(vulnerabilities) {
if (!vulnerabilities || vulnerabilities.length === 0) {
console.log('No vulnerabilities found.');
return;
}
console.log('\nVulnerabilities:');
for (const vuln of vulnerabilities) {
console.log(` - ${vuln.name} (${vuln.version}): ${vuln.severity} severity`);
if (vuln.details) {
console.log(` Details: ${vuln.details}`);
}
if (vuln.fixedIn) {
console.log(` Fixed in: ${vuln.fixedIn}`);
}
}
}
// Import formatters from utils
import { formatAnalysisJson, formatAnalysisText, formatAnalysisCSV } from '../utils/formatters.js';
// Export all functions
export {
reportAnalysis,
generateSummary,
saveReport,
getStatusColor,
displayRecommendations,
displayVulnerabilities,
// Legacy exports for backward compatibility
reportDrift
};
// Legacy exports with renaming - exported separately
export const generateDriftSummary = generateSummary;
export const saveDriftReport = saveReport;