devcompass
Advanced tools
| // src/services/dynamic-license.js | ||
| const registryClient = require('./registry-client'); | ||
| // License risk levels | ||
| const LICENSE_RISKS = { | ||
| // Critical - Viral copyleft, requires entire codebase to be open source | ||
| 'AGPL-3.0': { level: 'critical', risk: 'Requires disclosing source code of network services' }, | ||
| 'AGPL-3.0-only': { level: 'critical', risk: 'Requires disclosing source code of network services' }, | ||
| 'AGPL-3.0-or-later': { level: 'critical', risk: 'Requires disclosing source code of network services' }, | ||
| // High - Strong copyleft | ||
| 'GPL-3.0': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-3.0-only': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-3.0-or-later': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-2.0': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-2.0-only': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| 'GPL-2.0-or-later': { level: 'high', risk: 'Requires derivative works to be GPL licensed' }, | ||
| // Medium - Weak copyleft | ||
| 'LGPL-3.0': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-3.0-only': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-3.0-or-later': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-2.1': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-2.1-only': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'LGPL-2.1-or-later': { level: 'medium', risk: 'Modifications to the library must be shared' }, | ||
| 'MPL-2.0': { level: 'medium', risk: 'File-level copyleft, changes to MPL files must be shared' }, | ||
| 'EPL-1.0': { level: 'medium', risk: 'Weak copyleft, modifications must be shared' }, | ||
| 'EPL-2.0': { level: 'medium', risk: 'Weak copyleft, modifications must be shared' }, | ||
| 'CDDL-1.0': { level: 'medium', risk: 'File-level copyleft' }, | ||
| // Low - Permissive licenses (safe for commercial use) | ||
| 'MIT': { level: 'low', risk: 'Permissive - commercial friendly' }, | ||
| 'ISC': { level: 'low', risk: 'Permissive - commercial friendly' }, | ||
| 'BSD-2-Clause': { level: 'low', risk: 'Permissive - commercial friendly' }, | ||
| 'BSD-3-Clause': { level: 'low', risk: 'Permissive - commercial friendly' }, | ||
| 'Apache-2.0': { level: 'low', risk: 'Permissive with patent grant' }, | ||
| '0BSD': { level: 'low', risk: 'Public domain equivalent' }, | ||
| 'Unlicense': { level: 'low', risk: 'Public domain' }, | ||
| 'CC0-1.0': { level: 'low', risk: 'Public domain' }, | ||
| 'WTFPL': { level: 'low', risk: 'Permissive' } | ||
| }; | ||
| // Known alternatives for GPL packages | ||
| const GPL_ALTERNATIVES = { | ||
| 'readline-sync': { replacement: 'prompts', reason: 'MIT licensed alternative' }, | ||
| 'gnu-which': { replacement: 'which', reason: 'ISC licensed alternative' }, | ||
| 'node-forge': { replacement: 'forge', reason: 'BSD-3-Clause licensed' }, | ||
| 'gpl-package': { replacement: 'mit-alternative', reason: 'MIT licensed alternative' } | ||
| }; | ||
| function normalizeLicense(license) { | ||
| if (!license) return 'UNKNOWN'; | ||
| // Handle common variations | ||
| const normalized = license | ||
| .replace(/\s+/g, '-') | ||
| .replace(/^Apache 2\.0$/i, 'Apache-2.0') | ||
| .replace(/^Apache-2$/i, 'Apache-2.0') | ||
| .replace(/^MIT$/i, 'MIT') | ||
| .replace(/^ISC$/i, 'ISC') | ||
| .replace(/^BSD$/i, 'BSD-3-Clause') | ||
| .replace(/^GPLv3$/i, 'GPL-3.0') | ||
| .replace(/^GPLv2$/i, 'GPL-2.0') | ||
| .replace(/^LGPLv3$/i, 'LGPL-3.0') | ||
| .replace(/^LGPLv2\.1$/i, 'LGPL-2.1') | ||
| .replace(/^AGPLv3$/i, 'AGPL-3.0'); | ||
| return normalized; | ||
| } | ||
| function getLicenseRisk(license) { | ||
| const normalized = normalizeLicense(license); | ||
| if (LICENSE_RISKS[normalized]) { | ||
| return { | ||
| license: normalized, | ||
| ...LICENSE_RISKS[normalized] | ||
| }; | ||
| } | ||
| // Check for partial matches | ||
| for (const [key, value] of Object.entries(LICENSE_RISKS)) { | ||
| if (normalized.toUpperCase().includes(key.toUpperCase())) { | ||
| return { license: normalized, ...value }; | ||
| } | ||
| } | ||
| // Unknown license | ||
| return { | ||
| license: normalized, | ||
| level: 'unknown', | ||
| risk: 'Unknown license - manual review recommended' | ||
| }; | ||
| } | ||
| async function analyzePackage(packageName) { | ||
| const result = { | ||
| name: packageName, | ||
| license: null, | ||
| riskLevel: 'unknown', | ||
| riskMessage: null, | ||
| hasIssue: false, | ||
| alternative: null, | ||
| source: 'live' | ||
| }; | ||
| try { | ||
| const data = await registryClient.fetchPackage(packageName); | ||
| if (!data) { | ||
| result.license = 'UNKNOWN'; | ||
| result.riskMessage = 'Package not found'; | ||
| return result; | ||
| } | ||
| // Get license from latest version | ||
| const latestVersion = data['dist-tags']?.latest; | ||
| const latestData = latestVersion ? data.versions?.[latestVersion] : null; | ||
| const license = latestData?.license || data.license; | ||
| if (!license) { | ||
| result.license = 'UNLICENSED'; | ||
| result.riskLevel = 'high'; | ||
| result.riskMessage = 'No license specified'; | ||
| result.hasIssue = true; | ||
| return result; | ||
| } | ||
| // Analyze the license | ||
| const riskInfo = getLicenseRisk(license); | ||
| result.license = riskInfo.license; | ||
| result.riskLevel = riskInfo.level; | ||
| result.riskMessage = riskInfo.risk; | ||
| result.hasIssue = ['critical', 'high', 'medium'].includes(riskInfo.level); | ||
| // Check for known alternatives | ||
| if (GPL_ALTERNATIVES[packageName]) { | ||
| result.alternative = GPL_ALTERNATIVES[packageName].replacement; | ||
| } | ||
| return result; | ||
| } catch (error) { | ||
| result.license = 'UNKNOWN'; | ||
| result.riskMessage = 'Failed to fetch license info'; | ||
| return result; | ||
| } | ||
| } | ||
| async function analyzeBatch(packageNames) { | ||
| if (!Array.isArray(packageNames)) { | ||
| return new Map(); | ||
| } | ||
| const results = new Map(); | ||
| const packageData = await registryClient.fetchBatch(packageNames); | ||
| for (const name of packageNames) { | ||
| if (!name || typeof name !== 'string') continue; | ||
| const data = packageData.get(name); | ||
| const result = { | ||
| name, | ||
| license: null, | ||
| riskLevel: 'unknown', | ||
| riskMessage: null, | ||
| hasIssue: false, | ||
| alternative: GPL_ALTERNATIVES[name]?.replacement || null, | ||
| source: data ? 'live' : 'none' | ||
| }; | ||
| if (data) { | ||
| const latestVersion = data['dist-tags']?.latest; | ||
| const latestData = latestVersion ? data.versions?.[latestVersion] : null; | ||
| const license = latestData?.license || data.license; | ||
| if (!license) { | ||
| result.license = 'UNLICENSED'; | ||
| result.riskLevel = 'high'; | ||
| result.riskMessage = 'No license specified'; | ||
| result.hasIssue = true; | ||
| } else { | ||
| const riskInfo = getLicenseRisk(license); | ||
| result.license = riskInfo.license; | ||
| result.riskLevel = riskInfo.level; | ||
| result.riskMessage = riskInfo.risk; | ||
| result.hasIssue = ['critical', 'high', 'medium'].includes(riskInfo.level); | ||
| } | ||
| } else { | ||
| result.license = 'UNKNOWN'; | ||
| result.riskMessage = 'Package not found'; | ||
| } | ||
| results.set(name, result); | ||
| } | ||
| return results; | ||
| } | ||
| async function getLicenseConflicts(packageNames, projectLicense = 'MIT') { | ||
| const results = await analyzeBatch(packageNames); | ||
| const conflicts = { | ||
| total: packageNames.length, | ||
| clean: 0, | ||
| critical: [], | ||
| high: [], | ||
| medium: [], | ||
| unknown: [] | ||
| }; | ||
| for (const [name, data] of results) { | ||
| switch (data.riskLevel) { | ||
| case 'critical': | ||
| conflicts.critical.push({ | ||
| package: name, | ||
| license: data.license, | ||
| message: data.riskMessage, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| case 'high': | ||
| conflicts.high.push({ | ||
| package: name, | ||
| license: data.license, | ||
| message: data.riskMessage, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| case 'medium': | ||
| conflicts.medium.push({ | ||
| package: name, | ||
| license: data.license, | ||
| message: data.riskMessage, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| case 'unknown': | ||
| conflicts.unknown.push({ | ||
| package: name, | ||
| license: data.license, | ||
| message: data.riskMessage | ||
| }); | ||
| break; | ||
| default: | ||
| conflicts.clean++; | ||
| } | ||
| } | ||
| return conflicts; | ||
| } | ||
| function isCommercialCompatible(license) { | ||
| const risk = getLicenseRisk(license); | ||
| return risk.level === 'low'; | ||
| } | ||
| module.exports = { | ||
| analyzePackage, | ||
| analyzeBatch, | ||
| getLicenseConflicts, | ||
| getLicenseRisk, | ||
| isCommercialCompatible, | ||
| LICENSE_RISKS, | ||
| GPL_ALTERNATIVES | ||
| }; |
| // src/services/dynamic-quality.js | ||
| const registryClient = require('./registry-client'); | ||
| // Minimal fallback alternatives for offline mode | ||
| // Only most common replacements - live data covers everything else | ||
| const FALLBACK_ALTERNATIVES = { | ||
| 'request': { replacement: 'axios', reason: 'request is deprecated' }, | ||
| 'moment': { replacement: 'dayjs', reason: 'moment is in maintenance mode' }, | ||
| 'underscore': { replacement: 'lodash', reason: 'lodash is more actively maintained' }, | ||
| 'colors': { replacement: 'chalk', reason: 'colors had a malicious release' }, | ||
| 'faker': { replacement: '@faker-js/faker', reason: 'faker was corrupted, use community fork' }, | ||
| 'left-pad': { replacement: 'string.prototype.padstart', reason: 'Use native String methods' }, | ||
| 'node-uuid': { replacement: 'uuid', reason: 'node-uuid is deprecated' }, | ||
| 'querystring': { replacement: 'qs', reason: 'querystring is legacy' }, | ||
| 'node-fetch': { replacement: 'undici', reason: 'undici is faster and maintained by Node.js' } | ||
| }; | ||
| // Thresholds for maintenance status | ||
| const ABANDONED_THRESHOLD_MONTHS = 36; | ||
| const STALE_THRESHOLD_MONTHS = 24; | ||
| function monthsSince(date) { | ||
| if (!date) return Infinity; | ||
| const then = new Date(date); | ||
| const now = new Date(); | ||
| return Math.floor((now - then) / (1000 * 60 * 60 * 24 * 30)); | ||
| } | ||
| async function analyzePackage(packageName) { | ||
| const result = { | ||
| name: packageName, | ||
| status: 'HEALTHY', | ||
| deprecated: false, | ||
| abandoned: false, | ||
| stale: false, | ||
| lastPublish: null, | ||
| monthsSinceUpdate: null, | ||
| deprecationMessage: null, | ||
| alternative: null, | ||
| source: 'live' // 'live' or 'fallback' | ||
| }; | ||
| try { | ||
| const data = await registryClient.fetchPackage(packageName); | ||
| if (!data) { | ||
| // Package not found - check fallback | ||
| if (FALLBACK_ALTERNATIVES[packageName]) { | ||
| const fallback = FALLBACK_ALTERNATIVES[packageName]; | ||
| result.status = 'DEPRECATED'; | ||
| result.deprecated = true; | ||
| result.alternative = fallback.replacement; | ||
| result.deprecationMessage = fallback.reason; | ||
| result.source = 'fallback'; | ||
| } | ||
| return result; | ||
| } | ||
| // Check deprecation status from registry | ||
| const latestVersion = data['dist-tags']?.latest; | ||
| const latestData = latestVersion ? data.versions?.[latestVersion] : null; | ||
| if (latestData?.deprecated || data.deprecated) { | ||
| result.deprecated = true; | ||
| result.status = 'DEPRECATED'; | ||
| result.deprecationMessage = latestData?.deprecated || data.deprecated || 'Package is deprecated'; | ||
| } | ||
| // Check maintenance status | ||
| const timeData = data.time || {}; | ||
| const lastModified = timeData.modified || timeData[latestVersion]; | ||
| if (lastModified) { | ||
| result.lastPublish = lastModified; | ||
| result.monthsSinceUpdate = monthsSince(lastModified); | ||
| if (result.monthsSinceUpdate >= ABANDONED_THRESHOLD_MONTHS) { | ||
| result.abandoned = true; | ||
| if (result.status === 'HEALTHY') { | ||
| result.status = 'ABANDONED'; | ||
| } | ||
| } else if (result.monthsSinceUpdate >= STALE_THRESHOLD_MONTHS) { | ||
| result.stale = true; | ||
| if (result.status === 'HEALTHY') { | ||
| result.status = 'STALE'; | ||
| } | ||
| } | ||
| } | ||
| // Check fallback for known alternatives | ||
| if (FALLBACK_ALTERNATIVES[packageName]) { | ||
| result.alternative = FALLBACK_ALTERNATIVES[packageName].replacement; | ||
| } | ||
| return result; | ||
| } catch (error) { | ||
| // Network error - check fallback | ||
| if (FALLBACK_ALTERNATIVES[packageName]) { | ||
| const fallback = FALLBACK_ALTERNATIVES[packageName]; | ||
| result.status = 'DEPRECATED'; | ||
| result.deprecated = true; | ||
| result.alternative = fallback.replacement; | ||
| result.deprecationMessage = fallback.reason; | ||
| result.source = 'fallback'; | ||
| } | ||
| return result; | ||
| } | ||
| } | ||
| async function analyzeBatch(packageNames) { | ||
| if (!Array.isArray(packageNames)) { | ||
| return new Map(); | ||
| } | ||
| const results = new Map(); | ||
| // Fetch all packages in parallel | ||
| const packageData = await registryClient.fetchBatch(packageNames); | ||
| // Analyze each package | ||
| for (const name of packageNames) { | ||
| if (!name || typeof name !== 'string') continue; | ||
| const data = packageData.get(name); | ||
| if (data) { | ||
| // We have live data | ||
| const result = { | ||
| name, | ||
| status: 'HEALTHY', | ||
| deprecated: false, | ||
| abandoned: false, | ||
| stale: false, | ||
| lastPublish: null, | ||
| monthsSinceUpdate: null, | ||
| deprecationMessage: null, | ||
| alternative: FALLBACK_ALTERNATIVES[name]?.replacement || null, | ||
| source: 'live' | ||
| }; | ||
| // Check deprecation | ||
| const latestVersion = data['dist-tags']?.latest; | ||
| const latestData = latestVersion ? data.versions?.[latestVersion] : null; | ||
| if (latestData?.deprecated || data.deprecated) { | ||
| result.deprecated = true; | ||
| result.status = 'DEPRECATED'; | ||
| result.deprecationMessage = latestData?.deprecated || data.deprecated || 'Package is deprecated'; | ||
| } | ||
| // Check maintenance | ||
| const timeData = data.time || {}; | ||
| const lastModified = timeData.modified || timeData[latestVersion]; | ||
| if (lastModified) { | ||
| result.lastPublish = lastModified; | ||
| result.monthsSinceUpdate = monthsSince(lastModified); | ||
| if (result.monthsSinceUpdate >= ABANDONED_THRESHOLD_MONTHS) { | ||
| result.abandoned = true; | ||
| if (result.status === 'HEALTHY') result.status = 'ABANDONED'; | ||
| } else if (result.monthsSinceUpdate >= STALE_THRESHOLD_MONTHS) { | ||
| result.stale = true; | ||
| if (result.status === 'HEALTHY') result.status = 'STALE'; | ||
| } | ||
| } | ||
| results.set(name, result); | ||
| } else { | ||
| // Check fallback | ||
| if (FALLBACK_ALTERNATIVES[name]) { | ||
| const fallback = FALLBACK_ALTERNATIVES[name]; | ||
| results.set(name, { | ||
| name, | ||
| status: 'DEPRECATED', | ||
| deprecated: true, | ||
| abandoned: false, | ||
| stale: false, | ||
| lastPublish: null, | ||
| monthsSinceUpdate: null, | ||
| deprecationMessage: fallback.reason, | ||
| alternative: fallback.replacement, | ||
| source: 'fallback' | ||
| }); | ||
| } else { | ||
| // Unknown package | ||
| results.set(name, { | ||
| name, | ||
| status: 'UNKNOWN', | ||
| deprecated: false, | ||
| abandoned: false, | ||
| stale: false, | ||
| lastPublish: null, | ||
| monthsSinceUpdate: null, | ||
| deprecationMessage: null, | ||
| alternative: null, | ||
| source: 'none' | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| async function getProjectQualitySummary(packageNames) { | ||
| const results = await analyzeBatch(packageNames); | ||
| const summary = { | ||
| total: packageNames.length, | ||
| healthy: 0, | ||
| deprecated: 0, | ||
| abandoned: 0, | ||
| stale: 0, | ||
| unknown: 0, | ||
| issues: [] | ||
| }; | ||
| for (const [name, data] of results) { | ||
| switch (data.status) { | ||
| case 'HEALTHY': | ||
| summary.healthy++; | ||
| break; | ||
| case 'DEPRECATED': | ||
| summary.deprecated++; | ||
| summary.issues.push({ | ||
| package: name, | ||
| type: 'deprecated', | ||
| message: data.deprecationMessage, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| case 'ABANDONED': | ||
| summary.abandoned++; | ||
| summary.issues.push({ | ||
| package: name, | ||
| type: 'abandoned', | ||
| message: `No updates in ${data.monthsSinceUpdate} months`, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| case 'STALE': | ||
| summary.stale++; | ||
| summary.issues.push({ | ||
| package: name, | ||
| type: 'stale', | ||
| message: `No updates in ${data.monthsSinceUpdate} months`, | ||
| alternative: data.alternative | ||
| }); | ||
| break; | ||
| default: | ||
| summary.unknown++; | ||
| } | ||
| } | ||
| return summary; | ||
| } | ||
| function getAlternative(packageName) { | ||
| return FALLBACK_ALTERNATIVES[packageName]?.replacement || null; | ||
| } | ||
| module.exports = { | ||
| analyzePackage, | ||
| analyzeBatch, | ||
| getProjectQualitySummary, | ||
| getAlternative, | ||
| FALLBACK_ALTERNATIVES | ||
| }; |
| // src/services/dynamic-security.js | ||
| const { execSync } = require('child_process'); | ||
| const path = require('path'); | ||
| // Popular packages for typosquatting comparison | ||
| const POPULAR_PACKAGES = [ | ||
| 'express', 'react', 'react-dom', 'lodash', 'axios', 'moment', 'webpack', | ||
| 'typescript', 'jquery', 'vue', 'angular', 'next', 'gatsby', 'nuxt', | ||
| 'babel', 'eslint', 'prettier', 'jest', 'mocha', 'chai', 'karma', | ||
| 'underscore', 'request', 'async', 'bluebird', 'chalk', 'commander', | ||
| 'inquirer', 'ora', 'yargs', 'minimist', 'glob', 'mkdirp', 'rimraf', | ||
| 'fs-extra', 'uuid', 'semver', 'debug', 'dotenv', 'cors', 'helmet', | ||
| 'mongoose', 'sequelize', 'knex', 'prisma', 'graphql', 'apollo', | ||
| 'socket.io', 'redis', 'mysql', 'pg', 'sqlite3', 'mongodb', | ||
| 'aws-sdk', 'firebase', 'stripe', 'paypal', 'twilio', | ||
| 'nodemailer', 'pug', 'ejs', 'handlebars', 'mustache', | ||
| 'body-parser', 'cookie-parser', 'multer', 'passport', 'bcrypt', | ||
| 'jsonwebtoken', 'validator', 'yup', 'joi', 'zod' | ||
| ]; | ||
| // Whitelist - legitimate packages that might look suspicious | ||
| const WHITELIST = new Set([ | ||
| 'colors', 'chalk', 'ora', 'inquirer', 'prompts', | ||
| 'cross-env', 'cross-spawn', 'execa', 'shelljs', | ||
| 'node-fetch', 'axios', 'got', 'request', 'superagent', | ||
| 'nodemailer', 'sendgrid', 'mailgun', | ||
| 'bcrypt', 'bcryptjs', 'argon2', 'crypto-js', | ||
| 'jsonwebtoken', 'jose', 'passport', | ||
| 'puppeteer', 'playwright', 'selenium-webdriver', | ||
| 'sharp', 'jimp', 'canvas', 'pdf-lib', | ||
| 'esbuild', 'rollup', 'vite', 'parcel', | ||
| 'husky', 'lint-staged', 'commitlint' | ||
| ]); | ||
| // Suspicious patterns in install scripts | ||
| const SUSPICIOUS_PATTERNS = [ | ||
| /curl\s+[^\|]+\|\s*sh/i, // curl | sh | ||
| /wget\s+[^\|]+\|\s*sh/i, // wget | sh | ||
| /eval\s*\(\s*["']?http/i, // eval with http | ||
| /base64\s*-d/i, // base64 decode | ||
| /\\x[0-9a-f]{2}/i, // hex escapes | ||
| /child_process.*exec/i, // exec with http | ||
| /require\s*\(\s*['"]https?:/i, // require http | ||
| /\.env|process\.env\.(AWS|STRIPE|API_KEY|SECRET|PASSWORD|TOKEN)/i, // credential access | ||
| /exfiltrate|steal|harvest/i, // obvious malicious | ||
| /bitcoin|btc|wallet|miner/i, // crypto mining | ||
| /keylog|screenshot|record/i // spyware | ||
| ]; | ||
| function levenshteinDistance(a, b) { | ||
| if (!a || !b) return Infinity; | ||
| const matrix = Array(b.length + 1).fill(null).map(() => | ||
| Array(a.length + 1).fill(null) | ||
| ); | ||
| for (let i = 0; i <= a.length; i++) matrix[0][i] = i; | ||
| for (let j = 0; j <= b.length; j++) matrix[j][0] = j; | ||
| for (let j = 1; j <= b.length; j++) { | ||
| for (let i = 1; i <= a.length; i++) { | ||
| const cost = a[i - 1] === b[j - 1] ? 0 : 1; | ||
| matrix[j][i] = Math.min( | ||
| matrix[j][i - 1] + 1, // insertion | ||
| matrix[j - 1][i] + 1, // deletion | ||
| matrix[j - 1][i - 1] + cost // substitution | ||
| ); | ||
| } | ||
| } | ||
| return matrix[b.length][a.length]; | ||
| } | ||
| function checkTyposquatting(packageName, threshold = 2) { | ||
| if (!packageName || WHITELIST.has(packageName)) return null; | ||
| const name = packageName.toLowerCase().replace(/^@[^/]+\//, ''); // Remove scope | ||
| for (const popular of POPULAR_PACKAGES) { | ||
| const distance = levenshteinDistance(name, popular); | ||
| // Exact match is fine | ||
| if (distance === 0) return null; | ||
| // Within threshold and not too short | ||
| if (distance <= threshold && name.length >= 3 && popular.length >= 3) { | ||
| // Additional heuristics | ||
| const isSuspicious = ( | ||
| name.includes(popular) || // lodash-extra | ||
| popular.includes(name) || // lod (subset) | ||
| name.replace(/[-_]/g, '') === popular.replace(/[-_]/g, '') || // lodash_js vs lodashjs | ||
| name.startsWith(popular.slice(0, 3)) || // Same prefix | ||
| name.endsWith(popular.slice(-3)) // Same suffix | ||
| ); | ||
| if (isSuspicious || distance === 1) { | ||
| return { | ||
| package: packageName, | ||
| similarTo: popular, | ||
| distance, | ||
| warning: `Package name "${packageName}" is similar to popular package "${popular}"` | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function checkInstallScripts(packageData) { | ||
| if (!packageData) return null; | ||
| const scripts = packageData.scripts || {}; | ||
| const installScripts = ['preinstall', 'install', 'postinstall']; | ||
| const suspicious = []; | ||
| for (const scriptName of installScripts) { | ||
| const script = scripts[scriptName]; | ||
| if (!script) continue; | ||
| for (const pattern of SUSPICIOUS_PATTERNS) { | ||
| if (pattern.test(script)) { | ||
| suspicious.push({ | ||
| script: scriptName, | ||
| command: script.slice(0, 100) + (script.length > 100 ? '...' : ''), | ||
| pattern: pattern.source | ||
| }); | ||
| break; // One match per script is enough | ||
| } | ||
| } | ||
| } | ||
| return suspicious.length > 0 ? { | ||
| package: packageData.name, | ||
| suspicious, | ||
| warning: `Package has suspicious install scripts` | ||
| } : null; | ||
| } | ||
| function runNpmAudit(projectPath) { | ||
| try { | ||
| // Run npm audit in JSON mode | ||
| const result = execSync('npm audit --json 2>/dev/null', { | ||
| cwd: projectPath, | ||
| encoding: 'utf8', | ||
| maxBuffer: 10 * 1024 * 1024 // 10MB | ||
| }); | ||
| const audit = JSON.parse(result); | ||
| return parseAuditResult(audit); | ||
| } catch (error) { | ||
| // npm audit exits with non-zero if vulnerabilities found | ||
| if (error.stdout) { | ||
| try { | ||
| const audit = JSON.parse(error.stdout); | ||
| return parseAuditResult(audit); | ||
| } catch (e) { | ||
| // Silent fail | ||
| } | ||
| } | ||
| return { | ||
| vulnerabilities: [], | ||
| summary: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 } | ||
| }; | ||
| } | ||
| } | ||
| function parseAuditResult(audit) { | ||
| const vulnerabilities = []; | ||
| const summary = { | ||
| total: 0, | ||
| critical: 0, | ||
| high: 0, | ||
| moderate: 0, | ||
| low: 0, | ||
| info: 0 | ||
| }; | ||
| // Handle npm v7+ format | ||
| if (audit.vulnerabilities) { | ||
| for (const [name, data] of Object.entries(audit.vulnerabilities)) { | ||
| if (!data.via || !Array.isArray(data.via)) continue; | ||
| for (const via of data.via) { | ||
| if (typeof via === 'object' && via.title) { | ||
| vulnerabilities.push({ | ||
| package: name, | ||
| severity: via.severity || 'unknown', | ||
| title: via.title, | ||
| url: via.url, | ||
| range: via.range || data.range | ||
| }); | ||
| const sev = (via.severity || 'low').toLowerCase(); | ||
| if (summary[sev] !== undefined) summary[sev]++; | ||
| summary.total++; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Handle npm v6 format | ||
| if (audit.advisories) { | ||
| for (const [id, advisory] of Object.entries(audit.advisories)) { | ||
| vulnerabilities.push({ | ||
| package: advisory.module_name, | ||
| severity: advisory.severity, | ||
| title: advisory.title, | ||
| url: advisory.url, | ||
| range: advisory.vulnerable_versions | ||
| }); | ||
| const sev = (advisory.severity || 'low').toLowerCase(); | ||
| if (summary[sev] !== undefined) summary[sev]++; | ||
| summary.total++; | ||
| } | ||
| } | ||
| return { vulnerabilities, summary }; | ||
| } | ||
| async function analyzeProject(projectPath, dependencies = []) { | ||
| const result = { | ||
| typosquatting: [], | ||
| vulnerabilities: [], | ||
| suspiciousScripts: [], | ||
| summary: { | ||
| typosquattingCount: 0, | ||
| vulnerabilityCount: 0, | ||
| suspiciousScriptCount: 0, | ||
| criticalCount: 0, | ||
| highCount: 0 | ||
| } | ||
| }; | ||
| // Check for typosquatting | ||
| for (const dep of dependencies) { | ||
| if (!dep || WHITELIST.has(dep)) continue; | ||
| const typosquat = checkTyposquatting(dep); | ||
| if (typosquat) { | ||
| result.typosquatting.push(typosquat); | ||
| result.summary.typosquattingCount++; | ||
| } | ||
| } | ||
| // Run npm audit | ||
| if (projectPath) { | ||
| const audit = runNpmAudit(projectPath); | ||
| result.vulnerabilities = audit.vulnerabilities; | ||
| result.summary.vulnerabilityCount = audit.summary.total; | ||
| result.summary.criticalCount = audit.summary.critical; | ||
| result.summary.highCount = audit.summary.high; | ||
| } | ||
| return result; | ||
| } | ||
| function addToWhitelist(packageName) { | ||
| WHITELIST.add(packageName); | ||
| } | ||
| function removeFromWhitelist(packageName) { | ||
| WHITELIST.delete(packageName); | ||
| } | ||
| function isWhitelisted(packageName) { | ||
| return WHITELIST.has(packageName); | ||
| } | ||
| module.exports = { | ||
| checkTyposquatting, | ||
| checkInstallScripts, | ||
| runNpmAudit, | ||
| analyzeProject, | ||
| addToWhitelist, | ||
| removeFromWhitelist, | ||
| isWhitelisted, | ||
| POPULAR_PACKAGES, | ||
| WHITELIST | ||
| }; |
| // src/services/index.js | ||
| const registryClient = require('./registry-client'); | ||
| const dynamicQuality = require('./dynamic-quality'); | ||
| const dynamicLicense = require('./dynamic-license'); | ||
| const dynamicSecurity = require('./dynamic-security'); | ||
| /** | ||
| * DynamicAnalyzer class - unified interface for all dynamic services | ||
| */ | ||
| class DynamicAnalyzer { | ||
| constructor() { | ||
| this.quality = dynamicQuality; | ||
| this.license = dynamicLicense; | ||
| this.security = dynamicSecurity; | ||
| this.registry = registryClient; | ||
| } | ||
| async analyzePackage(packageName) { | ||
| const [qualityResult, licenseResult] = await Promise.all([ | ||
| this.quality.analyzePackage(packageName), | ||
| this.license.analyzePackage(packageName) | ||
| ]); | ||
| const typosquatResult = this.security.checkTyposquatting(packageName); | ||
| return { | ||
| name: packageName, | ||
| quality: qualityResult, | ||
| license: licenseResult, | ||
| security: { | ||
| typosquatting: typosquatResult, | ||
| isWhitelisted: this.security.isWhitelisted(packageName) | ||
| }, | ||
| hasIssues: ( | ||
| qualityResult.status !== 'HEALTHY' || | ||
| licenseResult.hasIssue || | ||
| typosquatResult !== null | ||
| ) | ||
| }; | ||
| } | ||
| async analyzePackages(packageNames) { | ||
| const results = new Map(); | ||
| const [qualityResults, licenseResults] = await Promise.all([ | ||
| this.quality.analyzeBatch(packageNames), | ||
| this.license.analyzeBatch(packageNames) | ||
| ]); | ||
| for (const name of packageNames) { | ||
| const quality = qualityResults.get(name) || { status: 'UNKNOWN' }; | ||
| const license = licenseResults.get(name) || { hasIssue: false }; | ||
| const typosquat = this.security.checkTyposquatting(name); | ||
| results.set(name, { | ||
| name, | ||
| quality, | ||
| license, | ||
| security: { | ||
| typosquatting: typosquat, | ||
| isWhitelisted: this.security.isWhitelisted(name) | ||
| }, | ||
| hasIssues: ( | ||
| quality.status !== 'HEALTHY' || | ||
| license.hasIssue || | ||
| typosquat !== null | ||
| ) | ||
| }); | ||
| } | ||
| return results; | ||
| } | ||
| async analyzeProject(projectPath, packageJson = {}) { | ||
| const deps = [ | ||
| ...Object.keys(packageJson.dependencies || {}), | ||
| ...Object.keys(packageJson.devDependencies || {}) | ||
| ]; | ||
| const [packageResults, securityResults, qualitySummary, licenseConflicts] = await Promise.all([ | ||
| this.analyzePackages(deps), | ||
| this.security.analyzeProject(projectPath, deps), | ||
| this.quality.getProjectQualitySummary(deps), | ||
| this.license.getLicenseConflicts(deps) | ||
| ]); | ||
| // Compile warnings | ||
| const warnings = []; | ||
| // Quality warnings | ||
| for (const issue of qualitySummary.issues) { | ||
| warnings.push({ | ||
| type: issue.type.toUpperCase(), | ||
| package: issue.package, | ||
| message: issue.message, | ||
| alternative: issue.alternative, | ||
| category: 'quality' | ||
| }); | ||
| } | ||
| // License warnings | ||
| for (const conflict of [...licenseConflicts.critical, ...licenseConflicts.high]) { | ||
| warnings.push({ | ||
| type: 'LICENSE', | ||
| package: conflict.package, | ||
| message: `${conflict.license}: ${conflict.message}`, | ||
| alternative: conflict.alternative, | ||
| category: 'license' | ||
| }); | ||
| } | ||
| // Security warnings | ||
| for (const typosquat of securityResults.typosquatting) { | ||
| warnings.push({ | ||
| type: 'TYPOSQUAT', | ||
| package: typosquat.package, | ||
| message: typosquat.warning, | ||
| similarTo: typosquat.similarTo, | ||
| category: 'security' | ||
| }); | ||
| } | ||
| for (const vuln of securityResults.vulnerabilities) { | ||
| warnings.push({ | ||
| type: vuln.severity.toUpperCase(), | ||
| package: vuln.package, | ||
| message: vuln.title, | ||
| url: vuln.url, | ||
| category: 'security' | ||
| }); | ||
| } | ||
| return { | ||
| projectPath, | ||
| totalDependencies: deps.length, | ||
| summary: { | ||
| quality: qualitySummary, | ||
| license: licenseConflicts, | ||
| security: securityResults.summary | ||
| }, | ||
| warnings, | ||
| details: packageResults | ||
| }; | ||
| } | ||
| getAutofixRecommendations(analysisResult) { | ||
| const recommendations = []; | ||
| for (const warning of analysisResult.warnings || []) { | ||
| if (warning.alternative) { | ||
| recommendations.push({ | ||
| action: 'replace', | ||
| package: warning.package, | ||
| replacement: warning.alternative, | ||
| reason: warning.message, | ||
| category: warning.category, | ||
| priority: warning.type === 'CRITICAL' ? 1 : | ||
| warning.type === 'HIGH' ? 2 : | ||
| warning.type === 'DEPRECATED' ? 3 : 4 | ||
| }); | ||
| } else if (warning.type === 'TYPOSQUAT') { | ||
| recommendations.push({ | ||
| action: 'remove', | ||
| package: warning.package, | ||
| reason: warning.message, | ||
| category: 'security', | ||
| priority: 1 | ||
| }); | ||
| } else if (warning.type === 'CRITICAL' || warning.type === 'HIGH') { | ||
| recommendations.push({ | ||
| action: 'update', | ||
| package: warning.package, | ||
| reason: warning.message, | ||
| category: 'security', | ||
| priority: warning.type === 'CRITICAL' ? 1 : 2 | ||
| }); | ||
| } | ||
| } | ||
| // Sort by priority | ||
| recommendations.sort((a, b) => a.priority - b.priority); | ||
| return recommendations; | ||
| } | ||
| /** | ||
| * Clear all caches | ||
| */ | ||
| clearCache() { | ||
| this.registry.clearCache(); | ||
| } | ||
| /** | ||
| * Get cache statistics | ||
| * @returns {Object} | ||
| */ | ||
| getCacheStats() { | ||
| return this.registry.getCacheStats(); | ||
| } | ||
| } | ||
| // Export singleton instance | ||
| const analyzer = new DynamicAnalyzer(); | ||
| module.exports = { | ||
| DynamicAnalyzer, | ||
| analyzer, | ||
| // Re-export individual services | ||
| registryClient, | ||
| dynamicQuality, | ||
| dynamicLicense, | ||
| dynamicSecurity | ||
| }; |
| //src/services/registry-client.js | ||
| const https = require('https'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const os = require('os'); | ||
| // Cache configuration | ||
| const CACHE_DIR = path.join(os.homedir(), '.depcompass', 'cache'); | ||
| const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours | ||
| const MAX_CONCURRENCY = 5; | ||
| const REQUEST_TIMEOUT = 10000; // 10 seconds | ||
| const MAX_RETRIES = 3; | ||
| const INITIAL_RETRY_DELAY = 1000; | ||
| // Memory cache for current session | ||
| const memoryCache = new Map(); | ||
| /** | ||
| * Ensure cache directory exists | ||
| */ | ||
| function ensureCacheDir() { | ||
| try { | ||
| if (!fs.existsSync(CACHE_DIR)) { | ||
| fs.mkdirSync(CACHE_DIR, { recursive: true }); | ||
| } | ||
| } catch (error) { | ||
| // Silent fail - cache is optional | ||
| } | ||
| } | ||
| function getCachePath(packageName) { | ||
| // Sanitize package name for filesystem | ||
| const safeName = packageName.replace(/\//g, '__').replace(/@/g, '_at_'); | ||
| return path.join(CACHE_DIR, `${safeName}.json`); | ||
| } | ||
| function readFromDiskCache(packageName) { | ||
| try { | ||
| const cachePath = getCachePath(packageName); | ||
| if (!fs.existsSync(cachePath)) return null; | ||
| const stats = fs.statSync(cachePath); | ||
| const ageMs = Date.now() - stats.mtimeMs; | ||
| // Check if cache is expired | ||
| if (ageMs > CACHE_TTL_MS) { | ||
| fs.unlinkSync(cachePath); // Delete expired cache | ||
| return null; | ||
| } | ||
| const data = fs.readFileSync(cachePath, 'utf8'); | ||
| return JSON.parse(data); | ||
| } catch (error) { | ||
| return null; | ||
| } | ||
| } | ||
| function writeToDiskCache(packageName, data) { | ||
| try { | ||
| ensureCacheDir(); | ||
| const cachePath = getCachePath(packageName); | ||
| fs.writeFileSync(cachePath, JSON.stringify(data), 'utf8'); | ||
| } catch (error) { | ||
| // Silent fail - cache is optional | ||
| } | ||
| } | ||
| function httpsGet(url, retryCount = 0) { | ||
| return new Promise((resolve, reject) => { | ||
| const request = https.get(url, { | ||
| timeout: REQUEST_TIMEOUT, | ||
| headers: { | ||
| 'Accept': 'application/json', | ||
| 'User-Agent': 'depcompass-cli' | ||
| } | ||
| }, (response) => { | ||
| // Handle rate limiting (429) | ||
| if (response.statusCode === 429) { | ||
| if (retryCount < MAX_RETRIES) { | ||
| const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount); | ||
| setTimeout(() => { | ||
| httpsGet(url, retryCount + 1).then(resolve).catch(reject); | ||
| }, delay); | ||
| return; | ||
| } | ||
| reject(new Error('Rate limited')); | ||
| return; | ||
| } | ||
| // Handle 404 (package not found) | ||
| if (response.statusCode === 404) { | ||
| resolve(null); | ||
| return; | ||
| } | ||
| // Handle other errors | ||
| if (response.statusCode !== 200) { | ||
| reject(new Error(`HTTP ${response.statusCode}`)); | ||
| return; | ||
| } | ||
| let data = ''; | ||
| response.on('data', chunk => data += chunk); | ||
| response.on('end', () => { | ||
| try { | ||
| resolve(JSON.parse(data)); | ||
| } catch (e) { | ||
| reject(new Error('Invalid JSON response')); | ||
| } | ||
| }); | ||
| }); | ||
| request.on('error', (error) => { | ||
| if (retryCount < MAX_RETRIES) { | ||
| const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount); | ||
| setTimeout(() => { | ||
| httpsGet(url, retryCount + 1).then(resolve).catch(reject); | ||
| }, delay); | ||
| return; | ||
| } | ||
| reject(error); | ||
| }); | ||
| request.on('timeout', () => { | ||
| request.destroy(); | ||
| if (retryCount < MAX_RETRIES) { | ||
| const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount); | ||
| setTimeout(() => { | ||
| httpsGet(url, retryCount + 1).then(resolve).catch(reject); | ||
| }, delay); | ||
| return; | ||
| } | ||
| reject(new Error('Request timeout')); | ||
| }); | ||
| }); | ||
| } | ||
| async function fetchPackage(packageName, useCache = true) { | ||
| if (!packageName || typeof packageName !== 'string') { | ||
| return null; | ||
| } | ||
| // Check memory cache first | ||
| if (useCache && memoryCache.has(packageName)) { | ||
| return memoryCache.get(packageName); | ||
| } | ||
| // Check disk cache | ||
| if (useCache) { | ||
| const cached = readFromDiskCache(packageName); | ||
| if (cached) { | ||
| memoryCache.set(packageName, cached); | ||
| return cached; | ||
| } | ||
| } | ||
| try { | ||
| // Encode package name for URL (handles scoped packages like @org/pkg) | ||
| const encodedName = encodeURIComponent(packageName).replace('%40', '@'); | ||
| const url = `https://registry.npmjs.org/${encodedName}`; | ||
| const data = await httpsGet(url); | ||
| if (data) { | ||
| // Cache the result | ||
| memoryCache.set(packageName, data); | ||
| writeToDiskCache(packageName, data); | ||
| } | ||
| return data; | ||
| } catch (error) { | ||
| // Return null on error (package may not exist or network issue) | ||
| return null; | ||
| } | ||
| } | ||
| async function fetchBatch(packageNames, useCache = true) { | ||
| if (!Array.isArray(packageNames)) { | ||
| return new Map(); | ||
| } | ||
| const results = new Map(); | ||
| const uniqueNames = [...new Set(packageNames.filter(n => n && typeof n === 'string'))]; | ||
| // Process in batches to limit concurrency | ||
| for (let i = 0; i < uniqueNames.length; i += MAX_CONCURRENCY) { | ||
| const batch = uniqueNames.slice(i, i + MAX_CONCURRENCY); | ||
| const batchResults = await Promise.all( | ||
| batch.map(name => fetchPackage(name, useCache).then(data => ({ name, data }))) | ||
| ); | ||
| for (const { name, data } of batchResults) { | ||
| if (data) { | ||
| results.set(name, data); | ||
| } | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| /** | ||
| * Clear all caches (memory and disk) | ||
| */ | ||
| function clearCache() { | ||
| memoryCache.clear(); | ||
| try { | ||
| if (fs.existsSync(CACHE_DIR)) { | ||
| const files = fs.readdirSync(CACHE_DIR); | ||
| for (const file of files) { | ||
| if (file.endsWith('.json')) { | ||
| fs.unlinkSync(path.join(CACHE_DIR, file)); | ||
| } | ||
| } | ||
| } | ||
| } catch (error) { | ||
| // Silent fail | ||
| } | ||
| } | ||
| /** | ||
| * Get cache statistics | ||
| * @returns {Object} | ||
| */ | ||
| function getCacheStats() { | ||
| let diskCount = 0; | ||
| let diskSize = 0; | ||
| try { | ||
| if (fs.existsSync(CACHE_DIR)) { | ||
| const files = fs.readdirSync(CACHE_DIR); | ||
| for (const file of files) { | ||
| if (file.endsWith('.json')) { | ||
| diskCount++; | ||
| const stats = fs.statSync(path.join(CACHE_DIR, file)); | ||
| diskSize += stats.size; | ||
| } | ||
| } | ||
| } | ||
| } catch (error) { | ||
| // Silent fail | ||
| } | ||
| return { | ||
| memoryCount: memoryCache.size, | ||
| diskCount, | ||
| diskSizeMB: (diskSize / (1024 * 1024)).toFixed(2), | ||
| cacheDir: CACHE_DIR | ||
| }; | ||
| } | ||
| module.exports = { | ||
| fetchPackage, | ||
| fetchBatch, | ||
| clearCache, | ||
| getCacheStats | ||
| }; |
+16
-4
| { | ||
| "name": "devcompass", | ||
| "version": "3.1.3", | ||
| "description": "Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, advanced interactive dependency graph visualization with multiple layouts, dynamic issue detection, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes, backup & rollback, search & filter capabilities, and interactive dependency exploration.", | ||
| "version": "3.1.4", | ||
| "description": "Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, unified interactive dependency graph with dynamic layout switching, real-time filtering, advanced zoom controls, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes, backup & rollback, and professional dependency exploration - all in a single interactive HTML file.", | ||
| "main": "src/index.js", | ||
@@ -84,2 +84,6 @@ "bin": { | ||
| "graph-visualization", | ||
| "unified-graph", | ||
| "interactive-graph", | ||
| "dynamic-layout-switching", | ||
| "real-time-filtering", | ||
| "force-directed-graph", | ||
@@ -89,3 +93,2 @@ "radial-layout", | ||
| "conflict-view", | ||
| "interactive-graph", | ||
| "graph-export", | ||
@@ -97,2 +100,11 @@ "graph-search", | ||
| "visual-analysis", | ||
| "zoom-controls", | ||
| "fit-to-screen", | ||
| "center-view", | ||
| "export-png", | ||
| "export-json", | ||
| "fullscreen-mode", | ||
| "single-file-graph", | ||
| "no-page-reload", | ||
| "live-statistics", | ||
| "dynamic-issues", | ||
@@ -122,2 +134,2 @@ "real-time-vulnerabilities" | ||
| "homepage": "https://github.com/AjayBThorat-20/devcompass#readme" | ||
| } | ||
| } |
+403
-421
| # 🧭 DevCompass | ||
| **Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, advanced interactive dependency graph visualization with multiple layouts, dynamic issue detection, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes with granular control, backup & rollback, parallel processing, search & filter capabilities, and enhanced fix command with dry-run mode, progress tracking, and automatic backups.** | ||
| **Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, unified interactive dependency graph with dynamic layout switching, real-time filtering, advanced zoom controls, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes with granular control, backup & rollback, and professional dependency exploration - all in a single interactive HTML file.** | ||
@@ -9,327 +9,338 @@ [](https://www.npmjs.com/package/devcompass) | ||
| Analyze your JavaScript projects to find unused dependencies, outdated packages, **detect security vulnerabilities**, **monitor GitHub issues in real-time for 500+ packages**, **visualize dependency graphs with multiple interactive layouts**, **search and filter packages**, **check bundle sizes**, **verify licenses**, **detect and auto-fix supply chain attacks**, **resolve license conflicts automatically**, **replace abandoned/deprecated packages automatically**, **analyze package quality**, **batch fix with granular control**, **manage backups and rollback changes**, and **automatically fix issues with dry-run, progress tracking, and backups**. Perfect for **CI/CD pipelines** with JSON output and exit codes. | ||
| Analyze your JavaScript projects to find unused dependencies, outdated packages, **detect security vulnerabilities**, **monitor GitHub issues in real-time for 500+ packages**, **visualize dependency graphs with unified interactive interface**, **instant layout switching**, **real-time filtering**, **advanced zoom controls**, **check bundle sizes**, **verify licenses**, **detect and auto-fix supply chain attacks**, **resolve license conflicts automatically**, **replace abandoned/deprecated packages automatically**, **analyze package quality**, **batch fix with granular control**, **manage backups and rollback changes**, and **automatically fix issues with dry-run, progress tracking, and backups**. Perfect for **CI/CD pipelines** with JSON output and exit codes. | ||
| > **✨ LATEST v3.1.2:** Graph Layout Fixes & Dynamic Issues - Tree/Radial layouts fixed, real-time vulnerability detection! 🎯 | ||
| > **🛡️ PREVIOUS v3.1.1:** Production Safety & Stability - Comprehensive bug fixes and hardening! 🛡️ | ||
| > **🎨 v3.1.0:** Advanced Graph Visualization - Force-directed layouts, radial views, search & filter! 🎨 | ||
| > **🎨 LATEST v3.1.4:** Unified Interactive Graph System - 40+ files → 1 file with dynamic controls! 🎨 | ||
| > **✨ PREVIOUS v3.1.3:** Cleanup & Code Improvements - Removed unused dependencies! 🧹 | ||
| > **🎯 v3.1.2:** Graph Layout Fixes & Dynamic Issues - Tree/Radial layouts fixed! 🎯 | ||
| ## 🎉 Latest Release: v3.1.2 (2026-04-17) | ||
| ## 🎉 Latest Release: v3.1.4 (2026-04-20) | ||
| **Graph layout fixes and dynamic issue detection!** | ||
| **Unified Interactive Graph System - Complete Redesign!** | ||
| ### What's New in v3.1.2: | ||
| ### 🌟 What's New in v3.1.4: | ||
| - 🌳 **Tree Layout Fix** - Proper horizontal spreading (was vertical line) | ||
| - 🎯 **Radial Layout Fix** - Labels positioned outside nodes (no overlap) | ||
| - 📊 **Panel Separation** - Controls/Statistics panels no longer overlap | ||
| - 🔄 **Dynamic Issues Analyzer** - Real-time npm audit integration (replaces hardcoded issues-db.json) | ||
| - ⚡ **Async Graph Generation** - Proper async/await handling | ||
| - 🎨 **Conflict Layout Improved** - Card-based UI organized by severity | ||
| - 🔗 **Unified Visualizer** - Single entry point for all layout types | ||
| #### **97% File Reduction - Revolutionary Simplification!** | ||
| ### Critical Fixes: | ||
| **Before v3.1.4:** | ||
| ```bash | ||
| # v3.1.1 (Had Layout Issues) | ||
| ❌ Tree layout: All nodes in single vertical line | ||
| ❌ Radial layout: Labels overlapping nodes | ||
| ❌ Tree layout: Controls/Statistics panels overlapping | ||
| ❌ Issues: Hardcoded issues-db.json (only 5 packages) | ||
| ❌ Graph generation: Missing await on async generate() | ||
| 40+ separate HTML files: | ||
| # v3.1.2 (All Fixed) ⭐ | ||
| ✅ Tree layout: Proper horizontal spreading with D3.js d3.tree() | ||
| ✅ Tree layout: Curved links with d3.linkHorizontal() | ||
| ✅ Tree layout: Correct sibling separation | ||
| ✅ Radial layout: Smart label positioning outside nodes | ||
| ✅ Radial layout: Text truncation (15 char max) | ||
| ✅ Radial layout: Staggered angles per depth level | ||
| ✅ Panel layout: Right-sidebar with flexbox (16px gap) | ||
| ✅ Dynamic issues: Real-time npm audit + registry checks | ||
| ✅ Dynamic issues: Works for ANY package (not just hardcoded list) | ||
| ✅ Conflict layout: Card-based UI with collapsible sections | ||
| ✅ Async fix: Proper await on generator.generate() | ||
| ``` | ||
| graph-tree.html | ||
| graph-force.html | ||
| graph-radial.html | ||
| graph-filter-vulnerable.html | ||
| combo-tree-vulnerable.html | ||
| combo-force-outdated.html | ||
| ... (34+ more files) | ||
| Total: ~4-5 MB | ||
| ### Upgrade Now: | ||
| **After v3.1.4:** | ||
| ```bash | ||
| npm install -g devcompass@3.1.2 | ||
| ``` | ||
| 1 unified HTML file: | ||
| ### Migration from v3.1.1: | ||
| dependency-graph.html (107 KB) | ||
| ✨ Contains ALL: | ||
| **No changes required - drop-in replacement!** All existing functionality works exactly the same, with fixed layouts and dynamic issue detection. | ||
| 4 layouts (switchable) | ||
| 5 filters (switchable) | ||
| Depth control | ||
| Search | ||
| Export options | ||
| Total: 107 KB (97% reduction!) | ||
| --- | ||
| ## 🎨 Graph Visualization Features | ||
| DevCompass v3.1.0+ includes **powerful, interactive dependency graph visualization** with multiple layouts! | ||
| ### 🎯 Available Layouts | ||
| #### 1. **Tree Layout** (Default) - FIXED in v3.1.2! 🆕 | ||
| Classic hierarchical view with clear parent-child relationships. | ||
| #### **🎨 Unified Interactive Features** | ||
| ```bash | ||
| devcompass graph --layout tree | ||
| ``` | ||
| - **Dynamic Layout Switcher** - Tree/Force/Radial/Conflict buttons (no page reload!) | ||
| - **Real-time Filters** - All/Vulnerable/Outdated/Deprecated/Unused (instant updates) | ||
| - **Depth Slider** - 1-10 with ∞ option (live filtering) | ||
| - **Live Search** - Instant package name filtering | ||
| - **Advanced Zoom Controls:** | ||
| - 🔍+ Zoom In (1.3x magnification) | ||
| - 🔍− Zoom Out (0.7x reduction) | ||
| - ⟲ Reset Zoom (return to 1:1) | ||
| - ⛶ Fit to Screen (auto-scale entire graph) | ||
| - ⊙ Center View (smart bounding box centering) | ||
| - **Export Capabilities:** | ||
| - 📸 Save as PNG (download current view) | ||
| - 💾 Save as JSON (export filtered data) | ||
| - **🖵 Fullscreen Mode** - Immersive graph exploration | ||
| - **📊 Live Statistics** - Real-time node/link counts | ||
| - ✅ Best for: Understanding dependency hierarchy | ||
| - ✅ **FIXED:** Proper horizontal spreading (root left, children right) | ||
| - ✅ **FIXED:** Curved links with d3.linkHorizontal() | ||
| - ✅ **FIXED:** Correct sibling vertical separation | ||
| - ✅ **FIXED:** Controls/Statistics panels separated | ||
| #### **🔧 Enhanced Tree Layout** | ||
| #### 2. **Force-Directed Layout** | ||
| - **Fixed label overlap** - Intelligent positioning (above parents, below leaves) | ||
| - **Increased spacing** - 1.5x-2x separation for clarity | ||
| - **Auto-fit on render** - Graph scales automatically | ||
| - **Text truncation** - 15 character limit with ellipsis | ||
| - **Better readability** - Improved font sizing | ||
| Interactive physics-based network visualization with ultimate UI! | ||
| #### **⚡ Performance Improvements** | ||
| | Operation | Time | Speed | | ||
| |-----------|------|-------| | ||
| | Layout switch | <100ms | No page reload! | | ||
| | Filter update | <50ms | Real-time | | ||
| | Search | <20ms | Instant | | ||
| | Initial render | <100ms | Lightning fast | | ||
| | Export PNG | ~1-2s | Professional quality | | ||
| ### 🚀 Upgrade Now: | ||
| ```bash | ||
| devcompass graph --layout force --open | ||
| npm install -g devcompass@3.1.4 | ||
| ``` | ||
| - ✅ Best for: Exploring complex relationships | ||
| - ✅ Drag individual nodes to reposition | ||
| - ✅ Natural clustering of related packages | ||
| - ✅ Real-time physics simulation | ||
| - ✅ Zoom controls (buttons + keyboard + mouse) | ||
| - ✅ Click to highlight connections | ||
| - ✅ Search and filter functionality | ||
| - ✅ Fullscreen mode | ||
| - ✅ Modern dark theme | ||
| - ✅ Interactive and engaging | ||
| ### 📈 Migration from v3.1.3: | ||
| #### 3. **Radial/Circular Layout** - FIXED in v3.1.2! 🆕 | ||
| **No changes required - drop-in replacement!** All existing commands work identically. The unified graph is now the default behavior. | ||
| Concentric circles based on dependency depth. | ||
| **Workflow Comparison:** | ||
| **Before v3.1.4:** | ||
| ```bash | ||
| devcompass graph --layout radial | ||
| # Generate multiple files | ||
| devcompass graph --layout tree --output graph-tree.html | ||
| devcompass graph --layout force --output graph-force.html | ||
| devcompass graph --filter vulnerable --output graph-vulnerable.html | ||
| # Result: 3 commands, 3 files, 3 browser tabs | ||
| ``` | ||
| - ✅ Best for: Understanding dependency levels | ||
| - ✅ Root package at center | ||
| - ✅ Dependencies radiating outward | ||
| - ✅ **FIXED:** Labels positioned OUTSIDE nodes | ||
| - ✅ **FIXED:** Smart text-anchor based on angle | ||
| - ✅ **FIXED:** Long names truncated (15 char max) | ||
| - ✅ **FIXED:** Staggered angles to avoid overlap | ||
| - ✅ Concentric depth circles visible | ||
| #### 4. **Conflict-Only View** - IMPROVED in v3.1.2! 🆕 | ||
| Shows only packages with issues - instantly identify problems! | ||
| **After v3.1.4:** | ||
| ```bash | ||
| devcompass graph --layout conflict --open | ||
| # Generate one unified file | ||
| devcompass graph | ||
| # Result: 1 command, 1 file, instant switching via buttons! | ||
| ``` | ||
| - ✅ Best for: Quick issue identification | ||
| - ✅ Filters out healthy dependencies automatically | ||
| - ✅ **IMPROVED:** Card-based layout organized by severity | ||
| - ✅ **IMPROVED:** Collapsible sections | ||
| - ✅ **IMPROVED:** Summary cards with counts | ||
| - ✅ **IMPROVED:** Beautiful 'No Conflicts' success state | ||
| - ✅ Color-coded by severity level | ||
| --- | ||
| ### 🔄 Dynamic Issue Detection (NEW in v3.1.2!) 🆕 | ||
| ## 🎨 Unified Graph Visualization (v3.1.4) | ||
| DevCompass now detects issues in **real-time** for ANY package! | ||
| DevCompass now features a **revolutionary unified interactive graph** - all layouts, filters, and controls in one beautiful interface! | ||
| ### 🎯 Single Command, Infinite Possibilities | ||
| ```bash | ||
| # Replaces hardcoded issues-db.json | ||
| devcompass analyze | ||
| # Generate unified interactive graph | ||
| devcompass graph | ||
| # Open in browser | ||
| devcompass graph --open | ||
| ``` | ||
| **Sources:** | ||
| - 🔐 **npm audit** - Real security vulnerabilities | ||
| - 📦 **npm registry** - Deprecation status | ||
| - 📅 **npm registry** - Maintenance status (unmaintained if 2+ years) | ||
| **What you get in ONE file:** | ||
| - ✅ All 4 layouts (Tree, Force, Radial, Conflict) - switchable via buttons | ||
| - ✅ All 5 filters (All, Vulnerable, Outdated, Deprecated, Unused) - instant filtering | ||
| - ✅ Depth control slider (1-10, ∞) | ||
| - ✅ Live search functionality | ||
| - ✅ Advanced zoom controls (In/Out/Reset/Fit/Center) | ||
| - ✅ Export options (PNG/JSON) | ||
| - ✅ Fullscreen mode | ||
| - ✅ Real-time statistics | ||
| - ✅ Professional UI/UX | ||
| - ✅ **No page reload needed!** | ||
| **Before v3.1.2:** Only detected issues for 5 hardcoded packages (axios, lodash, moment, request, express) | ||
| ### 🎮 Interactive Controls | ||
| **After v3.1.2:** Detects issues for ALL packages dynamically! | ||
| #### **Layout Switcher** | ||
| Click buttons to instantly switch between layouts: | ||
| ### 📤 Export Formats | ||
| 1. **🌳 Tree Layout** - Hierarchical view (root left, children right) | ||
| - Best for: Understanding dependency hierarchy | ||
| - Fixed: Proper horizontal spreading | ||
| - Fixed: Curved links with smooth connections | ||
| - Fixed: Intelligent label positioning | ||
| #### HTML Export (Default) | ||
| 2. **🌀 Force Layout** - Physics-based network | ||
| - Best for: Exploring complex relationships | ||
| - Drag nodes to reposition | ||
| - Natural clustering | ||
| - Real-time simulation | ||
| Interactive D3.js visualization with all features. | ||
| 3. **⭕ Radial Layout** - Circular/concentric view | ||
| - Best for: Understanding dependency levels | ||
| - Root at center | ||
| - Dependencies radiating outward | ||
| - Fixed: Labels outside nodes | ||
| ```bash | ||
| devcompass graph --output my-graph.html --open | ||
| ``` | ||
| 4. **⚠️ Conflict Layout** - Issues-only view | ||
| - Best for: Quick issue identification | ||
| - Card-based UI by severity | ||
| - Collapsible sections | ||
| - Beautiful "No Conflicts" state | ||
| - ✅ Full interactivity | ||
| - ✅ Zoom, pan, and explore | ||
| - ✅ Hover tooltips | ||
| - ✅ Search and filter | ||
| - ✅ Keyboard shortcuts | ||
| - ✅ All layout options | ||
| #### **Filter Controls** | ||
| Click buttons to filter packages in real-time: | ||
| #### JSON Export | ||
| - **All** - Show everything | ||
| - **Vulnerable** - Security issues only | ||
| - **Outdated** - Packages with updates available | ||
| - **Deprecated** - Officially deprecated packages | ||
| - **Unused** - Unused dependencies | ||
| Complete graph data structure for programmatic access. | ||
| #### **Depth Slider** | ||
| Drag slider to control dependency depth: | ||
| - **1-9** - Show specific depth level | ||
| - **∞** (at position 10) - Show unlimited depth | ||
| ```bash | ||
| devcompass graph --format json --output graph.json | ||
| ``` | ||
| #### **Search Box** | ||
| Type package name for instant filtering: | ||
| - Real-time updates as you type | ||
| - Highlights matching nodes | ||
| - Shows filtered count | ||
| - ✅ Full graph data (nodes + links) | ||
| - ✅ Programmatic access | ||
| - ✅ Integration with custom tools | ||
| - ✅ Lightweight and fast | ||
| #### **Zoom Controls** | ||
| Professional zoom management: | ||
| - **🔍+ Zoom In** - Magnify by 1.3x | ||
| - **🔍− Zoom Out** - Reduce by 0.7x | ||
| - **⟲ Reset** - Return to 1:1 scale | ||
| - **⛶ Fit** - Auto-scale to show entire graph | ||
| - **⊙ Center** - Smart centering within container | ||
| ### 🔍 Search & Filter | ||
| #### **Export Controls** | ||
| Save your current view: | ||
| - **📸 PNG** - Download as image (current zoom/filter) | ||
| - **💾 JSON** - Export filtered data structure | ||
| **Real-time Search:** | ||
| - 🔍 Instant package name search (Press F to focus) | ||
| - 📦 Search by version | ||
| - ⚡ Live results as you type | ||
| - 🎯 Highlight matching nodes | ||
| - 📋 Quick navigation to packages | ||
| #### **Fullscreen Mode** | ||
| - **🖵 Fullscreen** - Toggle immersive view | ||
| - Press ESC to exit | ||
| **Advanced Filters:** | ||
| - ⚠️ Show only vulnerable packages | ||
| - 📅 Show only outdated packages | ||
| - 🚫 Show only deprecated packages | ||
| - 💚 Filter by health score (Critical/Warning/Caution/Healthy) | ||
| - 📊 Filter by dependency depth level | ||
| - 📦 Filter by package type | ||
| - 🔄 Combine multiple filters | ||
| - ⚡ Real-time graph updates with live stats | ||
| ### 📊 Live Statistics Panel | ||
| **Filter UI Features:** | ||
| - 📊 Live statistics (visible/hidden nodes and links) | ||
| - 🔄 Filter reset button | ||
| - 🎨 Highlighted filtered nodes | ||
| - 🔗 Updated link connections | ||
| - 💡 Interactive dropdowns | ||
| Real-time metrics updated on every action: | ||
| ### 🎨 Interactive Features | ||
| Statistics | ||
| ─────────────── | ||
| Total: 142 | ||
| Vulnerable: 14 | ||
| Deprecated: 0 | ||
| Outdated: 4 | ||
| Unused: 5 | ||
| Healthy: 119 | ||
| **All Layouts Include:** | ||
| - 🔍 Zoom controls (in/out/reset/fit-to-screen) | ||
| - 👆 Pan and drag support | ||
| - 💡 Hover tooltips with package details | ||
| - 🎯 Node highlighting on search | ||
| - ⚡ Smooth transitions and animations | ||
| - 📱 Responsive design | ||
| - ⌨️ Keyboard shortcuts | ||
| ### 🎨 Color-Coded Health | ||
| **Force Layout Specific:** | ||
| - 🎮 Drag individual nodes | ||
| - 🌀 Real-time physics simulation | ||
| - 🔄 Reset layout button | ||
| - ⚙️ Toggle labels/links | ||
| - 🎯 Click to highlight connections | ||
| - 📊 Stats panel with selection count | ||
| - 🖥️ Fullscreen mode (cross-browser compatible) | ||
| Visual health indicators: | ||
| - 🟢 **Excellent (9-10)** - Green - Well-maintained | ||
| - 🟡 **Good (7-8)** - Light green - Healthy | ||
| - 🟠 **Fair (5-6)** - Yellow - Monitor | ||
| - 🔴 **Poor (3-4)** - Orange - Needs attention | ||
| - ⛔ **Critical (0-2)** - Red - Immediate action | ||
| **Keyboard Shortcuts:** | ||
| - **F** - Focus search box | ||
| - **R** - Reset simulation (force layout) | ||
| - **C** - Center view | ||
| - **+** or **=** - Zoom in | ||
| - **-** or **_** - Zoom out | ||
| - **ESC** - Clear selection | ||
| ### 🚀 Example Workflow | ||
| --- | ||
| ```bash | ||
| # 1. Analyze project | ||
| devcompass analyze | ||
| ## 🎉 Recent Updates | ||
| # 2. Generate unified graph | ||
| devcompass graph --open | ||
| ### v3.1.2 (2026-04-17) - Graph Layout Fixes & Dynamic Issues | ||
| # 3. Explore interactively: | ||
| # - Click "Vulnerable" filter → See security issues | ||
| # - Click "Force" layout → Explore relationships | ||
| # - Click "Fit to Screen" → See entire graph | ||
| # - Search "axios" → Find specific package | ||
| # - Click "Export PNG" → Save screenshot | ||
| **Major layout fixes and dynamic issue detection!** This release fixes critical graph layout issues and adds real-time vulnerability detection. | ||
| # 4. Fix issues | ||
| devcompass fix --batch-mode high | ||
| **Layout Fixes:** | ||
| - ✅ **Tree layout horizontal spreading** - Proper D3.js d3.tree() implementation | ||
| - ✅ **Tree layout curved links** - Using d3.linkHorizontal() for beautiful connections | ||
| - ✅ **Tree layout sibling separation** - Nodes properly spaced vertically | ||
| - ✅ **Radial layout labels** - Positioned outside nodes, no overlap | ||
| - ✅ **Radial layout text truncation** - 15 character max with ellipsis | ||
| - ✅ **Panel separation** - Controls/Statistics in right-sidebar with flexbox | ||
| # 5. Re-visualize | ||
| devcompass graph --open | ||
| # - Click "Conflict" layout → Verify fixes | ||
| ``` | ||
| **New Features:** | ||
| - ✅ **Dynamic Issues Analyzer** - Real-time npm audit integration | ||
| - ✅ **Live deprecation detection** - Checks npm registry for deprecated packages | ||
| - ✅ **Maintenance status** - Identifies unmaintained packages (2+ years) | ||
| - ✅ **Works for ANY package** - No more hardcoded issues-db.json | ||
| - ✅ **Unified visualizer** - Single entry point for all layout types | ||
| - ✅ **Async graph generation** - Proper await handling | ||
| ### 📈 Usage Examples | ||
| **Files Updated (6):** | ||
| - `src/graph/layouts/tree.js` - Complete rewrite with proper D3.js tree | ||
| - `src/graph/layouts/radial.js` - Fixed label positioning | ||
| - `src/graph/generator.js` - Async + dynamic issues + boolean flags | ||
| - `src/alerts/index.js` - Dynamic alerts for any package | ||
| - `src/commands/graph.js` - Await fix + enrichWithIssues option | ||
| - `src/analyzers/issues.js` - NEW dynamic issues analyzer | ||
| #### **Quick Security Audit** | ||
| ```bash | ||
| devcompass graph --open | ||
| # In browser: Click "Vulnerable" filter → See all security issues | ||
| ``` | ||
| ### v3.1.1 (2026-04-14) - Production Safety & Stability | ||
| #### **Dependency Exploration** | ||
| ```bash | ||
| devcompass graph --open | ||
| # In browser: | ||
| # 1. Click "Force" layout | ||
| # 2. Drag nodes around | ||
| # 3. Click nodes to highlight connections | ||
| # 4. Use search to find packages | ||
| ``` | ||
| **Comprehensive bug fixes and production hardening!** This patch release fixes critical bugs and adds 100+ validation checks for production stability. | ||
| #### **Professional Documentation** | ||
| ```bash | ||
| devcompass graph --open | ||
| # In browser: | ||
| # 1. Click "Tree" layout | ||
| # 2. Click "Fit to Screen" | ||
| # 3. Click "Export PNG" | ||
| # → Add screenshot to docs | ||
| ``` | ||
| **Critical Fixes:** | ||
| - ✅ **Fixed JSON mode crash** - `supplyChainData.warnings` undefined error resolved | ||
| - ✅ **Fixed division by zero** - Safe calculations in trend analysis | ||
| - ✅ **Fixed unsafe array operations** - Array.isArray() checks everywhere | ||
| - ✅ **Fixed cross-browser fullscreen** - Firefox/Safari/Chrome support | ||
| - ✅ **Added input validation** - All public functions validate inputs | ||
| - ✅ **Added null-safe operations** - Optional chaining throughout | ||
| - ✅ **Enhanced error handling** - Try-catch wrappers for external calls | ||
| - ✅ **Added graceful fallbacks** - Safe degradation for all operations | ||
| #### **Deep Dependency Analysis** | ||
| ```bash | ||
| devcompass graph --open | ||
| # In browser: | ||
| # 1. Drag depth slider to "2" | ||
| # 2. Click "Radial" layout | ||
| # 3. See direct + transitive dependencies | ||
| ``` | ||
| ### v3.1.0 (2026-04-08) - Advanced Graph Visualization | ||
| --- | ||
| **Multiple layouts, search/filter, and enhanced UI!** This release dramatically enhances graph visualization capabilities. | ||
| ## 🔄 Dynamic Issue Detection (v3.1.2+) | ||
| **What's New:** | ||
| - ✅ Force-directed network layout (interactive physics) | ||
| - ✅ Radial/circular layout (dependency levels) | ||
| - ✅ Conflict-only view (issues at a glance) | ||
| - ✅ Real-time search functionality | ||
| - ✅ Advanced filtering options | ||
| - ✅ Zoom controls (buttons + keyboard + mouse) | ||
| - ✅ Fullscreen mode | ||
| - ✅ Click to highlight connections | ||
| - ✅ Modern dark theme UI | ||
| - ✅ Improved performance | ||
| DevCompass detects issues in **real-time** for ANY package! | ||
| ### v3.0.2 (2026-04-08) - Modern Dependency Analysis | ||
| ```bash | ||
| devcompass analyze | ||
| devcompass graph --open | ||
| # Click "Vulnerable" filter to see live security issues | ||
| ``` | ||
| **Modern tooling for better accuracy!** This release replaces the stale `depcheck` package with `knip`, providing faster and more accurate unused dependency detection. | ||
| **Sources:** | ||
| - 🔐 **npm audit** - Real security vulnerabilities | ||
| - 📦 **npm registry** - Deprecation status | ||
| - 📅 **npm registry** - Maintenance status (2+ years = unmaintained) | ||
| **Coverage:** Works for ALL packages, not just a hardcoded list! | ||
| --- | ||
| ## ✨ Features | ||
| ## ✨ All Features at a Glance | ||
| - 🎨 **Unified Interactive Graph** (v3.1.4) - 40+ files → 1 file with dynamic controls | ||
| - 🧹 **Cleanup & Code Improvements** (v3.1.3) - Removed unused dependencies | ||
| - 🎯 **Graph Layout Fixes** (v3.1.2) - Tree/Radial layouts properly fixed | ||
| - 🔄 **Dynamic Issue Detection** (v3.1.2) - Real-time npm audit integration | ||
| - 🛡️ **Production Safety & Stability** (v3.1.1) - Comprehensive bug fixes and hardening | ||
| - 🎨 **Advanced Graph Visualization** (v3.1.0) - Multiple layouts, search & filter, interactive UI | ||
| - ✨ **Modern Dependency Analysis** (v3.0.2) - Replaced depcheck with knip for better accuracy | ||
| - 📊 **Dependency Graph Visualization** (v3.0.0) - Interactive D3.js graphs with health-based color coding | ||
| - 📦 **Batch Fix Modes** (v2.8.5) - Granular control over which categories to fix | ||
| - 💾 **Backup & Rollback** (v2.8.4) - Complete backup management for safe dependency fixes | ||
| - 📦 **Package Quality Auto-Fix** (v2.8.3) - Automatic replacement of abandoned/deprecated packages | ||
| - ⚖️ **License Conflict Auto-Fix** (v2.8.2) - Automatic GPL/AGPL replacement with MIT alternatives | ||
| - 🛡️ **Supply Chain Auto-Fix** (v2.8.1) - Automatic malicious package removal & typosquatting fixes | ||
| - 🔧 **Enhanced Fix Command** (v2.8.0) - Dry-run, progress tracking, backups & reports | ||
| - 🛡️ **Supply Chain Security** (v2.7) - Malicious package & typosquatting detection | ||
| - ⚖️ **License Risk Analysis** (v2.7) - Enhanced license compliance checking | ||
| - 📊 **Package Quality Metrics** (v2.7) - Health scoring for dependencies | ||
| - 💡 **Security Recommendations** (v2.7) - Prioritized, actionable fixes | ||
| - ⚡ **Parallel Processing** (v2.6) - 80% faster GitHub issue tracking | ||
| - 🎯 **500+ Package Coverage** (v2.5) - Comprehensive ecosystem monitoring | ||
| - 🔮 **GitHub Issue Tracking** (v2.4) - Real-time monitoring of package health | ||
| - 📈 **Predictive Warnings** (v2.4) - Detect issues before they're announced | ||
| - 🔐 **Security Scanning** (v2.3) - npm audit integration with severity breakdown | ||
| - 📦 **Bundle Size Analysis** (v2.3) - Identify heavy packages (> 1MB) | ||
| - ⚖️ **License Checker** (v2.3) - Detect restrictive licenses (GPL, AGPL) | ||
| - 🚀 **CI/CD Integration** (v2.2) - JSON output, exit codes, and silent mode | ||
| - ⚡ **Smart Caching** (v2.2) - 70% faster on repeated runs | ||
| - 🛡️ **Production Safety** (v3.1.1) - Comprehensive bug fixes and hardening | ||
| - 🎨 **Advanced Graph Visualization** (v3.1.0) - Multiple layouts, search & filter | ||
| - ✨ **Modern Dependency Analysis** (v3.0.2) - Replaced depcheck with knip | ||
| - 📊 **Interactive Graphs** (v3.0.0) - D3.js visualizations | ||
| - 📦 **Batch Fix Modes** (v2.8.5) - Granular control | ||
| - 💾 **Backup & Rollback** (v2.8.4) - Safe dependency management | ||
| - 📦 **Package Quality Auto-Fix** (v2.8.3) - Replace abandoned packages | ||
| - ⚖️ **License Conflict Auto-Fix** (v2.8.2) - GPL/AGPL replacement | ||
| - 🛡️ **Supply Chain Auto-Fix** (v2.8.1) - Malicious package removal | ||
| - 🔧 **Enhanced Fix Command** (v2.8.0) - Dry-run, progress tracking, backups | ||
| - 🛡️ **Supply Chain Security** (v2.7) - Malicious package detection | ||
| - ⚖️ **License Risk Analysis** (v2.7) - License compliance | ||
| - 📊 **Package Quality Metrics** (v2.7) - Health scoring | ||
| - 💡 **Security Recommendations** (v2.7) - Prioritized fixes | ||
| - ⚡ **Parallel Processing** (v2.6) - 80% faster GitHub tracking | ||
| - 🎯 **500+ Package Coverage** (v2.5) - Comprehensive monitoring | ||
| - 🔮 **GitHub Issue Tracking** (v2.4) - Real-time package health | ||
| - 🔐 **Security Scanning** (v2.3) - npm audit integration | ||
| - 📦 **Bundle Size Analysis** (v2.3) - Identify heavy packages | ||
| - ⚖️ **License Checker** (v2.3) - Detect restrictive licenses | ||
| - 🚀 **CI/CD Integration** (v2.2) - JSON output, exit codes | ||
| - ⚡ **Smart Caching** (v2.2) - 70% faster repeated runs | ||
@@ -341,3 +352,3 @@ ## 🚀 Installation | ||
| ```bash | ||
| npm install -g devcompass@3.1.2 | ||
| npm install -g devcompass@3.1.4 | ||
| ``` | ||
@@ -348,3 +359,3 @@ | ||
| ```bash | ||
| npm install --save-dev devcompass@3.1.2 | ||
| npm install --save-dev devcompass@3.1.4 | ||
| ``` | ||
@@ -355,3 +366,3 @@ | ||
| ```bash | ||
| npx devcompass@3.1.2 analyze | ||
| npx devcompass@3.1.4 analyze | ||
| ``` | ||
@@ -362,3 +373,3 @@ | ||
| ```bash | ||
| npm install -g devcompass@3.1.2 | ||
| npm install -g devcompass@3.1.4 | ||
| ``` | ||
@@ -376,40 +387,23 @@ | ||
| # Generate dependency graph with different layouts (v3.1.0+) | ||
| devcompass graph # Tree layout (default) - FIXED in v3.1.2! | ||
| devcompass graph --layout force --open # Interactive force-directed | ||
| devcompass graph --layout radial # Circular/radial view - FIXED in v3.1.2! | ||
| devcompass graph --layout conflict # Show only issues - IMPROVED in v3.1.2! | ||
| # Generate unified interactive graph (v3.1.4 - NEW!) | ||
| devcompass graph # Single file with ALL features | ||
| devcompass graph --open # Open in browser | ||
| # Export to different formats | ||
| devcompass graph --format json # JSON data | ||
| devcompass graph --format html --open # Interactive HTML | ||
| # Export formats | ||
| devcompass graph --format json # JSON data export | ||
| # Search and filter | ||
| devcompass graph --filter vulnerable # Vulnerable packages only | ||
| devcompass graph --filter outdated # Outdated packages only | ||
| devcompass graph --filter conflict # Packages with issues | ||
| # Auto-fix issues | ||
| devcompass fix # All fixes | ||
| devcompass fix --batch # Interactive selection | ||
| devcompass fix --dry-run # Preview only | ||
| # Combined options | ||
| devcompass graph --layout force --filter conflict --open | ||
| devcompass graph --layout radial --depth 2 --output radial-depth2.html | ||
| # Auto-fix issues (includes supply chain + license + quality fixes!) | ||
| devcompass fix | ||
| # Batch fix modes | ||
| devcompass fix --batch # Interactive selection | ||
| devcompass fix --batch-mode critical # Preset: critical only | ||
| devcompass fix --batch-mode high # Preset: high priority | ||
| devcompass fix --batch-mode all # Preset: all safe fixes | ||
| devcompass fix --only quality # Fix only quality issues | ||
| devcompass fix --skip updates # Skip updates category | ||
| devcompass fix --batch-mode critical # Critical only | ||
| devcompass fix --batch-mode high # High priority | ||
| devcompass fix --batch-mode all # All safe fixes | ||
| # Preview fixes without making changes | ||
| devcompass fix --dry-run | ||
| devcompass fix --batch --dry-run | ||
| # Category-specific fixes | ||
| devcompass fix --only quality # Quality only | ||
| devcompass fix --skip updates # Skip updates | ||
| # Auto-fix without confirmation (CI/CD) | ||
| devcompass fix --yes | ||
| devcompass fix --batch-mode high --yes | ||
| # Manage backups | ||
@@ -421,134 +415,145 @@ devcompass backup list | ||
| # JSON output (for CI/CD) | ||
| devcompass analyze --json | ||
| # CI mode (exit code 1 if score < threshold) | ||
| devcompass analyze --ci | ||
| # Silent mode (no output) | ||
| devcompass analyze --silent | ||
| # CI/CD integration | ||
| devcompass analyze --json # JSON output | ||
| devcompass analyze --ci # Exit code based on score | ||
| devcompass analyze --silent # No output | ||
| ``` | ||
| ## 📊 Graph Visualization Commands (v3.1.2) | ||
| ## 🎨 Unified Graph Commands (v3.1.4) | ||
| ### Layout Options | ||
| ### Generate Unified Interactive Graph | ||
| #### Tree Layout (Default) - FIXED! 🆕 | ||
| ```bash | ||
| # Basic tree layout - now with proper horizontal spreading! | ||
| # Basic usage - generates single interactive HTML | ||
| devcompass graph | ||
| # With depth limit | ||
| devcompass graph --depth 2 | ||
| # Open in browser | ||
| # Open in browser automatically | ||
| devcompass graph --open | ||
| ``` | ||
| #### Force-Directed Layout | ||
| # Custom output filename | ||
| devcompass graph --output my-dependencies.html --open | ||
| ```bash | ||
| # Interactive physics simulation with ultimate UI | ||
| devcompass graph --layout force --open | ||
| # With custom dimensions | ||
| devcompass graph --layout force --width 1920 --height 1080 | ||
| # With filters | ||
| devcompass graph --layout force --filter conflict --open | ||
| devcompass graph --width 1920 --height 1080 --open | ||
| ``` | ||
| #### Radial Layout - FIXED! 🆕 | ||
| ### What You Get (All in ONE File!) | ||
| **Single Command:** | ||
| ```bash | ||
| # Circular visualization - labels now outside nodes! | ||
| devcompass graph --layout radial --open | ||
| # With depth limit | ||
| devcompass graph --layout radial --depth 3 | ||
| devcompass graph --open | ||
| ``` | ||
| #### Conflict View - IMPROVED! 🆕 | ||
| **Includes ALL of these features:** | ||
| ```bash | ||
| # Card-based UI organized by severity | ||
| devcompass graph --layout conflict --open | ||
| #### **4 Layout Types** (Switchable via Buttons) | ||
| - 🌳 Tree - Hierarchical view | ||
| - 🌀 Force - Physics simulation | ||
| - ⭕ Radial - Circular view | ||
| - ⚠️ Conflict - Issues only | ||
| # Combined with filter | ||
| devcompass graph --layout conflict --filter vulnerable | ||
| ``` | ||
| #### **5 Filter Options** (Instant Filtering) | ||
| - All packages | ||
| - Vulnerable packages | ||
| - Outdated packages | ||
| - Deprecated packages | ||
| - Unused packages | ||
| #### **Interactive Controls** | ||
| - Depth slider (1-10, ∞) | ||
| - Live search box | ||
| - Zoom controls (In/Out/Reset/Fit/Center) | ||
| - Export (PNG/JSON) | ||
| - Fullscreen mode | ||
| - Live statistics | ||
| ### Export Options | ||
| #### HTML Export (Interactive) | ||
| #### HTML Export (Default - Interactive) | ||
| ```bash | ||
| # Default - interactive HTML | ||
| # Default format - single interactive HTML | ||
| devcompass graph --output my-graph.html | ||
| # Force layout | ||
| devcompass graph --layout force --output force.html --open | ||
| # With filters | ||
| devcompass graph --filter conflict --output conflicts.html | ||
| # Open immediately | ||
| devcompass graph --open | ||
| ``` | ||
| #### JSON Export | ||
| **File contains:** | ||
| - All 4 layouts (switchable) | ||
| - All 5 filters (instant) | ||
| - All interactive controls | ||
| - Export capabilities | ||
| - **Size: ~107 KB** (97% smaller than v3.1.3!) | ||
| #### JSON Export (Data Only) | ||
| ```bash | ||
| # Complete graph data | ||
| # Export as JSON data | ||
| devcompass graph --format json --output graph.json | ||
| # With filters | ||
| devcompass graph --filter vulnerable --format json --output vulnerable.json | ||
| # JSON includes complete graph structure | ||
| # nodes, links, metadata, analysis results | ||
| ``` | ||
| ### Filter Options | ||
| ### Advanced Usage Examples | ||
| #### **Quick Security Check** | ||
| ```bash | ||
| # Show only vulnerable packages | ||
| devcompass graph --filter vulnerable | ||
| # 1. Generate graph | ||
| devcompass graph --open | ||
| # Show only outdated packages | ||
| devcompass graph --filter outdated | ||
| # 2. In browser: | ||
| # - Click "Vulnerable" filter | ||
| # - See all security issues | ||
| # - Click "Export PNG" to save screenshot | ||
| ``` | ||
| # Show only unused packages | ||
| devcompass graph --filter unused | ||
| #### **Dependency Exploration** | ||
| ```bash | ||
| # 1. Generate graph | ||
| devcompass graph --open | ||
| # Show only packages with conflicts | ||
| devcompass graph --filter conflict | ||
| # All packages (default) | ||
| devcompass graph --filter all | ||
| # 2. In browser: | ||
| # - Click "Force" layout | ||
| # - Drag nodes to explore relationships | ||
| # - Use search to find specific packages | ||
| # - Click nodes to highlight connections | ||
| # - Click "Fit to Screen" to see overview | ||
| ``` | ||
| ### Advanced Examples | ||
| #### **Professional Documentation** | ||
| ```bash | ||
| # Force layout with vulnerable packages only | ||
| devcompass graph --layout force --filter vulnerable --open | ||
| # 1. Generate graph | ||
| devcompass graph --open | ||
| # Radial layout, depth 2 - with fixed label positioning! | ||
| devcompass graph --layout radial --depth 2 --open | ||
| # 2. In browser: | ||
| # - Click "Tree" layout | ||
| # - Set depth to 2 (slider) | ||
| # - Click "Fit to Screen" | ||
| # - Click "Export PNG" | ||
| # → Add to project documentation | ||
| ``` | ||
| # Quick conflict check - card-based UI! | ||
| devcompass graph --layout conflict --open | ||
| #### **Complete Workflow** | ||
| ```bash | ||
| # 1. Analyze project health | ||
| devcompass analyze | ||
| # Complete workflow | ||
| devcompass analyze # Analyze health (with dynamic issues!) | ||
| devcompass graph --layout conflict --open # Visualize issues | ||
| devcompass fix --batch-mode high # Fix critical issues | ||
| devcompass graph --layout force --open # Verify improvements | ||
| # 2. Visualize dependencies | ||
| devcompass graph --open | ||
| # - Explore with Force layout | ||
| # - Filter to Vulnerable packages | ||
| # - Export PNG for review | ||
| # Search and filter interactively | ||
| devcompass graph --layout force --open | ||
| # Then: Press F to search, use filter dropdowns, click nodes | ||
| # 3. Fix critical issues | ||
| devcompass fix --batch-mode high | ||
| # Fullscreen immersive experience | ||
| devcompass graph --layout force --open | ||
| # Then: Click Fullscreen button or press F11 | ||
| # 4. Verify improvements | ||
| devcompass graph --open | ||
| # - Click "Conflict" layout | ||
| # - Should see fewer issues! | ||
| ``` | ||
| ### Command Options (v3.1.2) | ||
| ### Command Options (v3.1.4) | ||
@@ -558,11 +563,10 @@ ```bash | ||
| -o, --output <file> # Output file (default: dependency-graph.html) | ||
| -f, --format <format> # Output format: html, json | ||
| -l, --layout <type> # Layout: tree, force, radial, conflict (default: tree) | ||
| -d, --depth <number> # Maximum depth to traverse (default: unlimited) | ||
| --filter <filter> # Filter: all, vulnerable, outdated, unused, conflict | ||
| -f, --format <format> # Output format: html, json (default: html) | ||
| -w, --width <number> # Graph width in pixels (default: 1200) | ||
| -h, --height <number> # Graph height in pixels (default: 800) | ||
| --open # Open in browser (HTML only) | ||
| --open # Open in browser after generation | ||
| ``` | ||
| **Note:** Layout and filter options are now **interactive buttons** in the HTML file, not CLI flags! | ||
| --- | ||
@@ -703,35 +707,2 @@ | ||
| ### Graph Layout Issues (Fixed in v3.1.2!) | ||
| All graph layout issues have been resolved: | ||
| ```bash | ||
| # Verify you're on v3.1.2+ | ||
| devcompass --version | ||
| # Should show: 3.1.2 or higher | ||
| # Test tree layout - should spread horizontally! | ||
| devcompass graph --layout tree --open | ||
| # Test radial layout - labels should be outside nodes! | ||
| devcompass graph --layout radial --open | ||
| # Test conflict layout - card-based UI! | ||
| devcompass graph --layout conflict --open | ||
| ``` | ||
| ### JSON Mode (Fixed in v3.1.1!) | ||
| All JSON mode issues have been resolved: | ||
| ```bash | ||
| # Verify you're on v3.1.1+ | ||
| devcompass --version | ||
| # Should show: 3.1.2 or higher | ||
| # Test JSON mode | ||
| devcompass analyze --json | ||
| # Should work perfectly with no errors! | ||
| ``` | ||
| ### Common Issues: | ||
@@ -742,3 +713,3 @@ | ||
| # Solution: Install globally | ||
| npm install -g devcompass@3.1.2 | ||
| npm install -g devcompass@3.1.4 | ||
| ``` | ||
@@ -752,27 +723,23 @@ | ||
| **Issue:** Tree layout shows vertical line (OLD BUG - FIXED!) | ||
| **Issue:** Graph controls not working | ||
| ```bash | ||
| # Solution: Upgrade to v3.1.2 | ||
| npm install -g devcompass@3.1.2 | ||
| # Tree layout now spreads horizontally with proper D3.js tree | ||
| ``` | ||
| # Solution: Ensure you're on v3.1.4 | ||
| devcompass --version # Should show 3.1.4 | ||
| **Issue:** Radial labels overlapping nodes (OLD BUG - FIXED!) | ||
| ```bash | ||
| # Solution: Upgrade to v3.1.2 | ||
| npm install -g devcompass@3.1.2 | ||
| # Radial labels now positioned outside nodes with smart anchoring | ||
| # Clear browser cache | ||
| # Hard refresh (Ctrl+F5 or Cmd+Shift+R) | ||
| ``` | ||
| **Issue:** Controls/Statistics panels overlapping (OLD BUG - FIXED!) | ||
| **Issue:** Center view centers entire page (OLD BUG - FIXED in v3.1.4!) | ||
| ```bash | ||
| # Solution: Upgrade to v3.1.2 | ||
| npm install -g devcompass@3.1.2 | ||
| # Panels now in right-sidebar with flexbox layout | ||
| # Solution: Upgrade to v3.1.4 | ||
| npm install -g devcompass@3.1.4 | ||
| # Center now uses smart bounding box calculation | ||
| ``` | ||
| **Issue:** Force layout nodes clustered | ||
| **Issue:** Labels overlapping in tree layout (OLD BUG - FIXED in v3.1.4!) | ||
| ```bash | ||
| # Solution: Use fit-to-screen button or wait for simulation to settle | ||
| # The layout spreads nodes automatically after a few seconds | ||
| # Solution: Upgrade to v3.1.4 | ||
| npm install -g devcompass@3.1.4 | ||
| # Labels now positioned intelligently | ||
| ``` | ||
@@ -788,2 +755,9 @@ | ||
| **Issue:** Graph not loading in browser | ||
| ```bash | ||
| # Check browser console for errors | ||
| # Ensure D3.js CDN is accessible | ||
| # Try hard refresh (Ctrl+F5) | ||
| ``` | ||
| --- | ||
@@ -846,3 +820,10 @@ | ||
| - [x] ~~Panel layout separation~~ ✅ **Fixed in v3.1.2!** | ||
| - [ ] Graph filters with analysis results (v3.1.3) | ||
| - [x] ~~Code cleanup and dependency optimization~~ ✅ **Added in v3.1.3!** | ||
| - [x] ~~Unified interactive graph system~~ ✅ **Added in v3.1.4!** | ||
| - [x] ~~Dynamic layout switching~~ ✅ **Added in v3.1.4!** | ||
| - [x] ~~Real-time filtering~~ ✅ **Added in v3.1.4!** | ||
| - [x] ~~Advanced zoom controls~~ ✅ **Added in v3.1.4!** | ||
| - [x] ~~Single file graph~~ ✅ **Added in v3.1.4!** | ||
| - [ ] Minimap for large graphs (v3.2.0) | ||
| - [ ] Node grouping/clustering (v3.2.0) | ||
| - [ ] Web dashboard for team health monitoring (v3.2.0) | ||
@@ -852,2 +833,3 @@ - [ ] Monorepo support with knip (v3.2.0) | ||
| - [ ] Compare graphs before/after fixes (v3.2.0) | ||
| - [ ] Timeline view (dependency changes over time) (v3.3.0) | ||
@@ -854,0 +836,0 @@ Want to contribute? Pick an item and open an issue! 🚀 |
+152
-265
| // src/analyzers/license-risk.js | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| // v3.1.4 - Dynamic license risk analysis using npm registry | ||
| /** | ||
| * License risk levels and compatibility | ||
| */ | ||
| const LICENSE_RISKS = { | ||
| // High Risk - Restrictive/Copyleft | ||
| 'GPL-1.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' }, | ||
| 'GPL-2.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' }, | ||
| 'GPL-3.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' }, | ||
| 'AGPL-1.0': { risk: 'critical', type: 'copyleft', business: 'Network copyleft - very restrictive' }, | ||
| 'AGPL-3.0': { risk: 'critical', type: 'copyleft', business: 'Network copyleft - very restrictive' }, | ||
| 'LGPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' }, | ||
| 'LGPL-2.1': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' }, | ||
| 'LGPL-3.0': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' }, | ||
| // Medium Risk | ||
| 'MPL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' }, | ||
| 'MPL-1.1': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' }, | ||
| 'MPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' }, | ||
| 'EPL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'Module-level copyleft' }, | ||
| 'EPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'Module-level copyleft' }, | ||
| 'CDDL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' }, | ||
| // Low Risk - Permissive | ||
| 'MIT': { risk: 'low', type: 'permissive', business: 'Very permissive' }, | ||
| 'Apache-2.0': { risk: 'low', type: 'permissive', business: 'Permissive with patent grant' }, | ||
| 'BSD-2-Clause': { risk: 'low', type: 'permissive', business: 'Very permissive' }, | ||
| 'BSD-3-Clause': { risk: 'low', type: 'permissive', business: 'Very permissive' }, | ||
| 'ISC': { risk: 'low', type: 'permissive', business: 'Very permissive' }, | ||
| 'CC0-1.0': { risk: 'low', type: 'public-domain', business: 'Public domain' }, | ||
| 'Unlicense': { risk: 'low', type: 'public-domain', business: 'Public domain' }, | ||
| '0BSD': { risk: 'low', type: 'permissive', business: 'Very permissive' }, | ||
| // Unknown/Special | ||
| 'UNLICENSED': { risk: 'critical', type: 'unknown', business: 'No license - all rights reserved' }, | ||
| 'SEE LICENSE IN': { risk: 'high', type: 'unknown', business: 'Custom license - review required' }, | ||
| 'CUSTOM': { risk: 'high', type: 'unknown', business: 'Custom license - review required' } | ||
| }; | ||
| const { analyzer } = require('../services'); | ||
| /** | ||
| * License compatibility matrix | ||
| * Can license A be combined with license B? | ||
| */ | ||
| const LICENSE_COMPATIBILITY = { | ||
| 'MIT': ['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-2.0', 'GPL-3.0', 'LGPL-2.1', 'LGPL-3.0'], | ||
| 'Apache-2.0': ['Apache-2.0', 'GPL-3.0', 'LGPL-3.0'], | ||
| 'GPL-2.0': ['GPL-2.0', 'MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC'], | ||
| 'GPL-3.0': ['GPL-3.0', 'MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC'], | ||
| 'LGPL-2.1': ['LGPL-2.1', 'MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-2.0'], | ||
| 'LGPL-3.0': ['LGPL-3.0', 'MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-3.0'] | ||
| }; | ||
| /** | ||
| * Normalize license name | ||
| */ | ||
| function normalizeLicense(license) { | ||
| if (!license) return 'UNLICENSED'; | ||
| const normalized = license | ||
| .replace(/\s+/g, '-') | ||
| .replace(/[()]/g, '') | ||
| .toUpperCase(); | ||
| // Handle common variations | ||
| if (normalized.includes('MIT')) return 'MIT'; | ||
| if (normalized.includes('APACHE-2')) return 'Apache-2.0'; | ||
| if (normalized.includes('BSD-2')) return 'BSD-2-Clause'; | ||
| if (normalized.includes('BSD-3')) return 'BSD-3-Clause'; | ||
| if (normalized.includes('ISC')) return 'ISC'; | ||
| if (normalized.includes('GPL-2')) return 'GPL-2.0'; | ||
| if (normalized.includes('GPL-3')) return 'GPL-3.0'; | ||
| if (normalized.includes('LGPL-2')) return 'LGPL-2.1'; | ||
| if (normalized.includes('LGPL-3')) return 'LGPL-3.0'; | ||
| if (normalized.includes('AGPL')) return 'AGPL-3.0'; | ||
| if (normalized.includes('MPL')) return 'MPL-2.0'; | ||
| if (normalized.includes('SEE LICENSE')) return 'SEE LICENSE IN'; | ||
| if (normalized === 'UNLICENSED') return 'UNLICENSED'; | ||
| return license; | ||
| } | ||
| /** | ||
| * Get license risk information | ||
| */ | ||
| function getLicenseRisk(license) { | ||
| const normalized = normalizeLicense(license); | ||
| return LICENSE_RISKS[normalized] || { | ||
| risk: 'high', | ||
| type: 'unknown', | ||
| business: 'Unknown license - review required' | ||
| }; | ||
| } | ||
| /** | ||
| * Load package alternatives database | ||
| */ | ||
| function loadAlternatives() { | ||
| async function analyzeLicenseRisks(projectPath, licenses = []) { | ||
| try { | ||
| const alternativesPath = path.join(__dirname, '../../data/package-alternatives.json'); | ||
| if (fs.existsSync(alternativesPath)) { | ||
| return JSON.parse(fs.readFileSync(alternativesPath, 'utf8')); | ||
| // Extract package names from licenses | ||
| const packageNames = licenses.map(l => l.package); | ||
| if (packageNames.length === 0) { | ||
| return { | ||
| warnings: [], | ||
| stats: { | ||
| total: 0, | ||
| critical: 0, | ||
| high: 0, | ||
| medium: 0, | ||
| low: 0, | ||
| clean: 0 | ||
| }, | ||
| projectLicense: getProjectLicense(projectPath) | ||
| }; | ||
| } | ||
| return null; | ||
| } catch (error) { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * Find alternative package for license conflict | ||
| */ | ||
| function findAlternative(packageName, license) { | ||
| const alternatives = loadAlternatives(); | ||
| if (!alternatives) return null; | ||
| const pkgName = packageName.split('@')[0]; | ||
| // Check GPL alternatives | ||
| if (license.includes('GPL') && !license.includes('LGPL')) { | ||
| const gplAlts = alternatives.gpl_alternatives[pkgName]; | ||
| if (gplAlts && gplAlts.alternatives.length > 0) { | ||
| return gplAlts.alternatives[0]; | ||
| // Get project license | ||
| const projectLicense = getProjectLicense(projectPath); | ||
| // Analyze license conflicts dynamically | ||
| const conflicts = await analyzer.license.getLicenseConflicts( | ||
| packageNames, | ||
| projectLicense | ||
| ); | ||
| const warnings = []; | ||
| // Process critical conflicts (AGPL, etc.) | ||
| for (const conflict of conflicts.critical) { | ||
| warnings.push({ | ||
| package: conflict.package, | ||
| license: conflict.license, | ||
| severity: 'critical', | ||
| message: conflict.message, | ||
| risk: 'Viral copyleft - requires entire codebase to be open source', | ||
| recommendation: conflict.alternative | ||
| ? `Replace with ${conflict.alternative}` | ||
| : 'Find permissive alternative', | ||
| suggestedAlternative: conflict.alternative ? { | ||
| name: conflict.alternative, | ||
| license: 'MIT' // Most alternatives are MIT | ||
| } : null, | ||
| autoFixable: conflict.alternative ? true : false | ||
| }); | ||
| } | ||
| } | ||
| // Check AGPL alternatives | ||
| if (license.includes('AGPL')) { | ||
| const agplAlts = alternatives.agpl_alternatives[pkgName]; | ||
| if (agplAlts && agplAlts.alternatives.length > 0) { | ||
| return agplAlts.alternatives[0]; | ||
| // Process high-risk conflicts (GPL, etc.) | ||
| for (const conflict of conflicts.high) { | ||
| warnings.push({ | ||
| package: conflict.package, | ||
| license: conflict.license, | ||
| severity: 'high', | ||
| message: conflict.message, | ||
| risk: 'Strong copyleft - derivative works must be GPL licensed', | ||
| recommendation: conflict.alternative | ||
| ? `Replace with ${conflict.alternative}` | ||
| : 'Consider permissive alternative', | ||
| suggestedAlternative: conflict.alternative ? { | ||
| name: conflict.alternative, | ||
| license: 'MIT' | ||
| } : null, | ||
| autoFixable: conflict.alternative ? true : false | ||
| }); | ||
| } | ||
| } | ||
| // Check LGPL alternatives | ||
| if (license.includes('LGPL')) { | ||
| const lgplAlts = alternatives.lgpl_alternatives[pkgName]; | ||
| if (lgplAlts && lgplAlts.alternatives.length > 0) { | ||
| return lgplAlts.alternatives[0]; | ||
| // Process medium-risk conflicts (LGPL, MPL, etc.) | ||
| for (const conflict of conflicts.medium) { | ||
| warnings.push({ | ||
| package: conflict.package, | ||
| license: conflict.license, | ||
| severity: 'medium', | ||
| message: conflict.message, | ||
| risk: 'Weak copyleft - modifications must be shared', | ||
| recommendation: conflict.alternative | ||
| ? `Consider replacing with ${conflict.alternative}` | ||
| : 'Review license compatibility', | ||
| suggestedAlternative: conflict.alternative ? { | ||
| name: conflict.alternative, | ||
| license: 'MIT' | ||
| } : null, | ||
| autoFixable: false // Medium risk - manual review needed | ||
| }); | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Check license compatibility | ||
| */ | ||
| function checkLicenseCompatibility(projectLicense, dependencyLicenses) { | ||
| const conflicts = []; | ||
| const normalized = normalizeLicense(projectLicense); | ||
| const compatible = LICENSE_COMPATIBILITY[normalized] || []; | ||
| for (const [pkg, license] of Object.entries(dependencyLicenses)) { | ||
| const depNormalized = normalizeLicense(license); | ||
| const depRisk = getLicenseRisk(license); | ||
| // Check if copyleft license conflicts with permissive project | ||
| if (depRisk.type === 'copyleft' && !compatible.includes(depNormalized)) { | ||
| const alternative = findAlternative(pkg, license); | ||
| conflicts.push({ | ||
| package: pkg, | ||
| license: license, | ||
| projectLicense: projectLicense, | ||
| severity: 'high', | ||
| issue: 'License incompatibility', | ||
| message: `${license} dependency may conflict with ${projectLicense} project license`, | ||
| recommendation: alternative | ||
| ? `Replace with ${alternative.name} (${alternative.license})` | ||
| : 'Review license compatibility with legal team', | ||
| autoFixable: alternative ? true : false, | ||
| autoFixAction: alternative ? 'replace' : 'review', | ||
| suggestedAlternative: alternative, | ||
| requiresConfirmation: true | ||
| // Process unknown licenses | ||
| for (const conflict of conflicts.unknown) { | ||
| warnings.push({ | ||
| package: conflict.package, | ||
| license: conflict.license, | ||
| severity: 'medium', | ||
| message: conflict.message, | ||
| risk: 'Unknown license - cannot determine compatibility', | ||
| recommendation: 'Review license manually', | ||
| suggestedAlternative: null, | ||
| autoFixable: false | ||
| }); | ||
| } | ||
| return { | ||
| warnings, | ||
| stats: { | ||
| total: packageNames.length, | ||
| critical: conflicts.critical.length, | ||
| high: conflicts.high.length, | ||
| medium: conflicts.medium.length, | ||
| low: 0, | ||
| clean: conflicts.clean | ||
| }, | ||
| projectLicense, | ||
| conflicts: { | ||
| critical: conflicts.critical, | ||
| high: conflicts.high, | ||
| medium: conflicts.medium, | ||
| unknown: conflicts.unknown | ||
| } | ||
| }; | ||
| } catch (error) { | ||
| console.error('[license-risk] Analysis failed:', error.message); | ||
| return { | ||
| warnings: [], | ||
| stats: { | ||
| total: 0, | ||
| critical: 0, | ||
| high: 0, | ||
| medium: 0, | ||
| low: 0, | ||
| clean: 0 | ||
| }, | ||
| projectLicense: 'MIT' | ||
| }; | ||
| } | ||
| return conflicts; | ||
| } | ||
| /** | ||
| * Analyze license risks for all dependencies | ||
| */ | ||
| async function analyzeLicenseRisks(projectPath, licenses) { | ||
| const warnings = []; | ||
| const stats = { | ||
| total: 0, | ||
| critical: 0, | ||
| high: 0, | ||
| medium: 0, | ||
| low: 0, | ||
| copyleft: 0, | ||
| permissive: 0, | ||
| unknown: 0 | ||
| }; | ||
| // Get project license | ||
| let projectLicense = 'MIT'; // Default | ||
| function getProjectLicense(projectPath) { | ||
| try { | ||
| const projectPkgPath = path.join(projectPath, 'package.json'); | ||
| if (fs.existsSync(projectPkgPath)) { | ||
| const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8')); | ||
| projectLicense = projectPkg.license || 'MIT'; | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const packageJsonPath = path.join(projectPath, 'package.json'); | ||
| if (fs.existsSync(packageJsonPath)) { | ||
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); | ||
| return packageJson.license || 'MIT'; | ||
| } | ||
| } catch (error) { | ||
| // Use default | ||
| // Ignore errors | ||
| } | ||
| const dependencyLicenses = {}; | ||
| // Analyze each license | ||
| for (const pkg of licenses) { | ||
| stats.total++; | ||
| const risk = getLicenseRisk(pkg.license); | ||
| dependencyLicenses[pkg.package] = pkg.license; | ||
| // Count by type | ||
| if (risk.type === 'copyleft' || risk.type === 'weak-copyleft') { | ||
| stats.copyleft++; | ||
| } else if (risk.type === 'permissive' || risk.type === 'public-domain') { | ||
| stats.permissive++; | ||
| } else { | ||
| stats.unknown++; | ||
| } | ||
| // Add warnings for high-risk licenses | ||
| if (risk.risk === 'critical' || risk.risk === 'high') { | ||
| stats[risk.risk]++; | ||
| const alternative = findAlternative(pkg.package, pkg.license); | ||
| warnings.push({ | ||
| package: pkg.package, | ||
| license: pkg.license, | ||
| severity: risk.risk, | ||
| type: risk.type, | ||
| issue: 'High-risk license', | ||
| message: `${pkg.license}: ${risk.business}`, | ||
| recommendation: alternative | ||
| ? `Replace with ${alternative.name} (${alternative.license})` | ||
| : risk.risk === 'critical' | ||
| ? 'Replace with permissive alternative immediately' | ||
| : 'Consider replacing with MIT/Apache alternative', | ||
| autoFixable: alternative ? true : false, | ||
| autoFixAction: alternative ? 'replace' : 'review', | ||
| suggestedAlternative: alternative, | ||
| requiresConfirmation: true | ||
| }); | ||
| } else if (risk.risk === 'medium') { | ||
| stats.medium++; | ||
| } else { | ||
| stats.low++; | ||
| } | ||
| return 'MIT'; // Default assumption | ||
| } | ||
| function getLicenseRiskScore(licenseRiskData) { | ||
| if (!licenseRiskData || !licenseRiskData.stats) { | ||
| return 0; | ||
| } | ||
| // Check license compatibility | ||
| const conflicts = checkLicenseCompatibility(projectLicense, dependencyLicenses); | ||
| warnings.push(...conflicts); | ||
| const { critical, high, medium } = licenseRiskData.stats; | ||
| return { | ||
| warnings, | ||
| stats, | ||
| projectLicense | ||
| }; | ||
| } | ||
| /** | ||
| * Get license risk score (0-10) | ||
| */ | ||
| function getLicenseRiskScore(stats) { | ||
| let score = 10; | ||
| // Penalty calculation | ||
| let penalty = 0; | ||
| penalty += critical * 3; // Critical = -3 points each | ||
| penalty += high * 2; // High = -2 points each | ||
| penalty += medium * 1; // Medium = -1 point each | ||
| score -= stats.critical * 3; | ||
| score -= stats.high * 2; | ||
| score -= stats.medium * 0.5; | ||
| return Math.max(0, score); | ||
| return Math.min(10, penalty); // Cap at 10 points penalty | ||
| } | ||
@@ -287,8 +179,3 @@ | ||
| analyzeLicenseRisks, | ||
| getLicenseRisk, | ||
| checkLicenseCompatibility, | ||
| normalizeLicense, | ||
| getLicenseRiskScore, | ||
| findAlternative, | ||
| LICENSE_RISKS | ||
| getLicenseRiskScore | ||
| }; |
+107
-477
| // src/analyzers/package-quality.js | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const https = require('https'); | ||
| // v3.1.4 - Dynamic quality analysis using npm registry | ||
| /** | ||
| * Fetch package metadata from npm registry | ||
| */ | ||
| function fetchNpmPackageInfo(packageName) { | ||
| return new Promise((resolve, reject) => { | ||
| const options = { | ||
| hostname: 'registry.npmjs.org', | ||
| path: `/${packageName}`, | ||
| method: 'GET', | ||
| headers: { | ||
| 'User-Agent': 'DevCompass', | ||
| 'Accept': 'application/json' | ||
| } | ||
| }; | ||
| const req = https.request(options, (res) => { | ||
| let data = ''; | ||
| res.on('data', (chunk) => { | ||
| data += chunk; | ||
| }); | ||
| res.on('end', () => { | ||
| if (res.statusCode === 200) { | ||
| try { | ||
| resolve(JSON.parse(data)); | ||
| } catch (error) { | ||
| reject(new Error('Failed to parse npm response')); | ||
| } | ||
| } else if (res.statusCode === 404) { | ||
| reject(new Error('Package not found')); | ||
| } else if (res.statusCode === 429) { | ||
| reject(new Error('Rate limit exceeded')); | ||
| } else { | ||
| reject(new Error(`npm registry returned ${res.statusCode}`)); | ||
| } | ||
| }); | ||
| }); | ||
| req.on('error', reject); | ||
| req.setTimeout(5000, () => { | ||
| req.destroy(); | ||
| reject(new Error('Request timeout')); | ||
| }); | ||
| req.end(); | ||
| }); | ||
| } | ||
| const { analyzer } = require('../services'); | ||
| /** | ||
| * Calculate days since last publish | ||
| */ | ||
| function daysSincePublish(dateString) { | ||
| if (!dateString) return 0; | ||
| async function analyzePackageQuality(dependencies, githubData = []) { | ||
| const packages = Object.keys(dependencies); | ||
| try { | ||
| const publishDate = new Date(dateString); | ||
| const now = new Date(); | ||
| const diffTime = Math.abs(now - publishDate); | ||
| const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); | ||
| return diffDays; | ||
| } catch (error) { | ||
| return 0; | ||
| } | ||
| } | ||
| /** | ||
| * Calculate package health score (0-10) | ||
| */ | ||
| function calculateHealthScore(packageData, githubData = null) { | ||
| let score = 10; | ||
| // ✅ FIXED: Validate packageData structure | ||
| if (!packageData || typeof packageData !== 'object') { | ||
| return 5; // Default score for invalid data | ||
| } | ||
| // Get latest version info with safety checks | ||
| const latestVersion = packageData['dist-tags']?.latest; | ||
| const versionData = latestVersion ? packageData.versions?.[latestVersion] : null; | ||
| const time = latestVersion ? packageData.time?.[latestVersion] : null; | ||
| if (!versionData || !time) { | ||
| return 5; // Default score if data missing | ||
| } | ||
| // 1. Age factor (max -2 points) | ||
| const daysSince = daysSincePublish(time); | ||
| if (daysSince > 365 * 3) { | ||
| score -= 2; // 3+ years old | ||
| } else if (daysSince > 365 * 2) { | ||
| score -= 1.5; // 2-3 years old | ||
| } else if (daysSince > 365) { | ||
| score -= 1; // 1-2 years old | ||
| } else if (daysSince > 180) { | ||
| score -= 0.5; // 6-12 months old | ||
| } | ||
| // 2. Maintenance frequency (max -2 points) | ||
| const versions = Object.keys(packageData.versions || {}); | ||
| const recentVersions = versions.filter(v => { | ||
| const vTime = packageData.time?.[v]; | ||
| if (!vTime) return false; | ||
| return daysSincePublish(vTime) <= 365; | ||
| }); | ||
| if (recentVersions.length === 0) { | ||
| score -= 2; // No updates in a year | ||
| } else if (recentVersions.length < 3) { | ||
| score -= 1; // Less than 3 updates in a year | ||
| } | ||
| // 3. GitHub activity (if available) (max -2 points) | ||
| if (githubData && typeof githubData === 'object') { | ||
| const { totalIssues = 0, last7Days = 0, last30Days = 0 } = githubData; | ||
| // High issue count with low activity is bad | ||
| if (totalIssues > 100 && last30Days < 5) { | ||
| score -= 1.5; // Many issues, low maintenance | ||
| } else if (totalIssues > 50 && last30Days < 3) { | ||
| score -= 1; // Medium issues, low maintenance | ||
| } | ||
| // Very high recent activity might indicate problems | ||
| if (last7Days > 30) { | ||
| score -= 0.5; // Unusually high activity | ||
| } | ||
| } | ||
| // 4. Dependencies count (max -1 point) | ||
| const deps = versionData.dependencies || {}; | ||
| const depCount = Object.keys(deps).length; | ||
| if (depCount > 50) { | ||
| score -= 1; // Too many dependencies | ||
| } else if (depCount > 30) { | ||
| score -= 0.5; | ||
| } | ||
| // 5. Has description and repository (max -1 point) | ||
| if (!packageData.description) { | ||
| score -= 0.5; | ||
| } | ||
| if (!packageData.repository) { | ||
| score -= 0.5; | ||
| } | ||
| // 6. Deprecated packages (automatic 0) | ||
| if (versionData.deprecated) { | ||
| return 0; | ||
| } | ||
| return Math.max(0, Math.min(10, score)); | ||
| } | ||
| /** | ||
| * Determine package status based on health score | ||
| */ | ||
| function getPackageStatus(score, daysSince) { | ||
| // ✅ FIXED: Add input validation | ||
| const validScore = typeof score === 'number' && !isNaN(score) ? score : 5; | ||
| const validDaysSince = typeof daysSince === 'number' && !isNaN(daysSince) ? daysSince : 0; | ||
| if (validScore === 0) { | ||
| if (packages.length === 0) { | ||
| return { | ||
| status: 'DEPRECATED', | ||
| color: 'red', | ||
| severity: 'critical', | ||
| label: 'DEPRECATED' | ||
| results: [], | ||
| stats: { | ||
| total: 0, | ||
| healthy: 0, | ||
| needsAttention: 0, | ||
| stale: 0, | ||
| abandoned: 0, | ||
| deprecated: 0 | ||
| } | ||
| }; | ||
| } else if (validScore < 3 || validDaysSince > 365 * 3) { | ||
| return { | ||
| status: 'ABANDONED', | ||
| color: 'red', | ||
| severity: 'critical', | ||
| label: 'ABANDONED' | ||
| }; | ||
| } else if (validScore < 5 || validDaysSince > 365 * 2) { | ||
| return { | ||
| status: 'STALE', | ||
| color: 'yellow', | ||
| severity: 'high', | ||
| label: 'STALE' | ||
| }; | ||
| } else if (validScore < 7) { | ||
| return { | ||
| status: 'NEEDS_ATTENTION', | ||
| color: 'yellow', | ||
| severity: 'medium', | ||
| label: 'NEEDS ATTENTION' | ||
| }; | ||
| } else { | ||
| return { | ||
| status: 'HEALTHY', | ||
| color: 'green', | ||
| severity: 'low', | ||
| label: 'HEALTHY' | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * Get maintainer activity status | ||
| */ | ||
| function getMaintainerStatus(packageData) { | ||
| // ✅ FIXED: Add null checks | ||
| if (!packageData || typeof packageData !== 'object') { | ||
| return 'unknown'; | ||
| } | ||
| const maintainers = packageData.maintainers || []; | ||
| const latestVersion = packageData['dist-tags']?.latest; | ||
| const time = latestVersion ? packageData.time?.[latestVersion] : null; | ||
| if (!time) { | ||
| return 'unknown'; | ||
| } | ||
| const daysSince = daysSincePublish(time); | ||
| if (daysSince > 365 * 2) { | ||
| return 'inactive'; | ||
| } else if (daysSince > 365) { | ||
| return 'low_activity'; | ||
| } else if (daysSince > 180) { | ||
| return 'moderate_activity'; | ||
| } else { | ||
| return 'active'; | ||
| } | ||
| } | ||
| /** | ||
| * Format last update time in human-readable format | ||
| */ | ||
| function formatLastUpdate(daysSince) { | ||
| // ✅ FIXED: Add input validation | ||
| const validDays = typeof daysSince === 'number' && !isNaN(daysSince) ? daysSince : 0; | ||
| if (validDays < 30) { | ||
| return `${validDays} days ago`; | ||
| } else if (validDays < 365) { | ||
| const months = Math.floor(validDays / 30); | ||
| return `${months} month${months > 1 ? 's' : ''} ago`; | ||
| } else { | ||
| const years = Math.floor(validDays / 365); | ||
| return `${years} year${years > 1 ? 's' : ''} ago`; | ||
| } | ||
| } | ||
| /** | ||
| * Analyze package quality for all dependencies | ||
| */ | ||
| async function analyzePackageQuality(dependencies, githubData = []) { | ||
| // ✅ FIXED: Validate inputs | ||
| if (!dependencies || typeof dependencies !== 'object') { | ||
| return { | ||
| total: 0, | ||
| try { | ||
| // Fetch quality data for all packages dynamically | ||
| const qualityResults = await analyzer.quality.analyzeBatch(packages); | ||
| const results = []; | ||
| const stats = { | ||
| total: packages.length, | ||
| healthy: 0, | ||
@@ -266,240 +34,106 @@ needsAttention: 0, | ||
| abandoned: 0, | ||
| deprecated: 0, | ||
| packages: [] | ||
| deprecated: 0 | ||
| }; | ||
| } | ||
| // ✅ FIXED: Ensure githubData is always an array | ||
| const safeGithubData = Array.isArray(githubData) ? githubData : []; | ||
| // Load quality fixer for alternative suggestions | ||
| let qualityFixer = null; | ||
| try { | ||
| const QualityFixer = require('../utils/quality-fixer'); | ||
| qualityFixer = new QualityFixer(); | ||
| } catch (error) { | ||
| // Quality fixer not available, continue without alternatives | ||
| } | ||
| const results = []; | ||
| const stats = { | ||
| total: 0, | ||
| healthy: 0, | ||
| needsAttention: 0, | ||
| stale: 0, | ||
| abandoned: 0, | ||
| deprecated: 0 | ||
| }; | ||
| // Create GitHub data lookup | ||
| const githubLookup = {}; | ||
| for (const data of safeGithubData) { | ||
| if (data && data.package) { | ||
| githubLookup[data.package] = data; | ||
| } | ||
| } | ||
| // Analyze each package (limit to prevent rate limiting) | ||
| const packages = Object.keys(dependencies).slice(0, 20); // Analyze first 20 | ||
| for (const packageName of packages) { | ||
| // ✅ FIXED: Skip invalid package names | ||
| if (!packageName || typeof packageName !== 'string') { | ||
| continue; | ||
| } | ||
| try { | ||
| stats.total++; | ||
| for (const [name, data] of qualityResults) { | ||
| // Find matching GitHub data if available | ||
| const githubMetrics = githubData.find(g => g.package === name); | ||
| // Fetch package info from npm | ||
| const packageData = await fetchNpmPackageInfo(packageName); | ||
| // ✅ FIXED: Validate packageData before proceeding | ||
| if (!packageData || typeof packageData !== 'object') { | ||
| console.error(`Invalid package data for ${packageName}`); | ||
| continue; | ||
| } | ||
| // Calculate health score | ||
| const github = githubLookup[packageName]; | ||
| const healthScore = calculateHealthScore(packageData, github); | ||
| const healthScore = calculateHealthScore(data, githubMetrics); | ||
| // Get latest version info | ||
| const latestVersion = packageData['dist-tags']?.latest; | ||
| const time = latestVersion ? packageData.time?.[latestVersion] : null; | ||
| const daysSince = time ? daysSincePublish(time) : 0; | ||
| // Determine status | ||
| const statusInfo = getPackageStatus(healthScore, daysSince); | ||
| const maintainerStatus = getMaintainerStatus(packageData); | ||
| // Count by status | ||
| if (statusInfo.status === 'HEALTHY') { | ||
| let status = 'healthy'; | ||
| if (data.deprecated) { | ||
| status = 'deprecated'; | ||
| stats.deprecated++; | ||
| } else if (data.abandoned) { | ||
| status = 'abandoned'; | ||
| stats.abandoned++; | ||
| } else if (data.stale) { | ||
| status = 'stale'; | ||
| stats.stale++; | ||
| } else if (healthScore < 7) { | ||
| status = 'needs_attention'; | ||
| stats.needsAttention++; | ||
| } else { | ||
| stats.healthy++; | ||
| } else if (statusInfo.status === 'NEEDS_ATTENTION') { | ||
| stats.needsAttention++; | ||
| } else if (statusInfo.status === 'STALE') { | ||
| stats.stale++; | ||
| } else if (statusInfo.status === 'ABANDONED') { | ||
| stats.abandoned++; | ||
| } else if (statusInfo.status === 'DEPRECATED') { | ||
| stats.deprecated++; | ||
| } | ||
| // Get repository info | ||
| const repository = packageData.repository?.url || ''; | ||
| const hasGithub = repository.includes('github.com'); | ||
| // Check for alternative packages (only if qualityFixer is available) | ||
| let alternative = null; | ||
| if (qualityFixer) { | ||
| try { | ||
| alternative = qualityFixer.findAlternative(packageName); | ||
| } catch (error) { | ||
| // Alternative lookup failed, continue without it | ||
| } | ||
| } | ||
| // Determine if package is auto-fixable | ||
| const isAutoFixable = ( | ||
| (statusInfo.status === 'ABANDONED' || statusInfo.status === 'DEPRECATED' || statusInfo.status === 'STALE') && | ||
| alternative !== null | ||
| ); | ||
| // Build result | ||
| const result = { | ||
| name: packageName, | ||
| package: packageName, | ||
| version: dependencies[packageName], | ||
| healthScore: Number(healthScore.toFixed(1)), | ||
| status: statusInfo.status, | ||
| severity: statusInfo.severity, | ||
| label: statusInfo.label, | ||
| lastPublish: time ? new Date(time).toISOString().split('T')[0] : 'unknown', | ||
| lastUpdate: formatLastUpdate(daysSince), | ||
| daysSincePublish: daysSince, | ||
| maintainerStatus: maintainerStatus, | ||
| hasRepository: !!packageData.repository, | ||
| hasGithub: hasGithub, | ||
| totalVersions: Object.keys(packageData.versions || {}).length, | ||
| description: packageData.description || '', | ||
| deprecated: latestVersion && packageData.versions?.[latestVersion]?.deprecated ? true : false, | ||
| // Auto-fix metadata | ||
| autoFixable: isAutoFixable, | ||
| autoFixAction: isAutoFixable ? 'replace' : null, | ||
| suggestedAlternative: alternative ? alternative.recommended : null, | ||
| allAlternatives: alternative ? alternative.alternatives : null, | ||
| migrationGuide: alternative ? alternative.migration_guide : null, | ||
| requiresConfirmation: true, | ||
| reason: alternative | ||
| ? alternative.reason | ||
| : getQualityRecommendation({ | ||
| status: statusInfo.status, | ||
| healthScore, | ||
| daysSincePublish: daysSince | ||
| }).recommendation | ||
| }; | ||
| // Add GitHub metrics if available | ||
| if (github && typeof github === 'object') { | ||
| result.githubMetrics = { | ||
| totalIssues: github.totalIssues || 0, | ||
| recentIssues: github.last30Days || 0, | ||
| trend: github.trend || 'stable' | ||
| }; | ||
| } | ||
| results.push(result); | ||
| // Small delay to respect npm registry rate limits | ||
| await new Promise(resolve => setTimeout(resolve, 100)); | ||
| } catch (error) { | ||
| // ✅ FIXED: Better error handling with specific error types | ||
| if (error.message === 'Rate limit exceeded') { | ||
| console.error(`Rate limit hit while analyzing ${packageName}, skipping remaining packages`); | ||
| break; // Stop analyzing more packages | ||
| } else if (error.message === 'Package not found') { | ||
| // Skip packages that don't exist | ||
| continue; | ||
| } else { | ||
| console.error(`Error analyzing ${packageName}:`, error.message); | ||
| // Continue with next package | ||
| } | ||
| // Add to results | ||
| results.push({ | ||
| package: name, | ||
| name: name, | ||
| status: status, | ||
| deprecated: data.deprecated, | ||
| abandoned: data.abandoned, | ||
| stale: data.stale, | ||
| lastPublish: data.lastPublish, | ||
| daysSincePublish: data.monthsSinceUpdate ? data.monthsSinceUpdate * 30 : null, | ||
| monthsSinceUpdate: data.monthsSinceUpdate, | ||
| deprecationMessage: data.deprecationMessage, | ||
| alternative: data.alternative, | ||
| healthScore: healthScore, | ||
| githubMetrics: githubMetrics || null, | ||
| autoFixable: data.alternative ? true : false, | ||
| source: data.source | ||
| }); | ||
| } | ||
| } | ||
| // ✅ FIXED: Return consistent structure with results instead of packages | ||
| return { | ||
| total: results.length, | ||
| healthy: stats.healthy, | ||
| needsAttention: stats.needsAttention, | ||
| stale: stats.stale, | ||
| abandoned: stats.abandoned, | ||
| deprecated: stats.deprecated, | ||
| results: results, // Return as 'results' | ||
| packages: results, // Also keep as 'packages' for backward compatibility | ||
| stats: stats // Also include stats object | ||
| }; | ||
| } | ||
| /** | ||
| * Get quality recommendations for a package | ||
| */ | ||
| function getQualityRecommendation(packageResult) { | ||
| // ✅ FIXED: Add input validation | ||
| if (!packageResult || typeof packageResult !== 'object') { | ||
| return { | ||
| action: 'none', | ||
| message: 'No data available', | ||
| recommendation: 'Unable to provide recommendation' | ||
| results, | ||
| stats, | ||
| packages: results.filter(r => r.autoFixable) // For fix command | ||
| }; | ||
| } | ||
| const { | ||
| status = 'UNKNOWN', | ||
| healthScore = 5, | ||
| daysSincePublish = 0, | ||
| maintainerStatus = 'unknown' | ||
| } = packageResult; | ||
| if (status === 'DEPRECATED') { | ||
| } catch (error) { | ||
| console.error('[package-quality] Analysis failed:', error.message); | ||
| return { | ||
| action: 'critical', | ||
| message: 'Package is deprecated', | ||
| recommendation: 'Find an actively maintained alternative immediately' | ||
| results: [], | ||
| stats: { | ||
| total: packages.length, | ||
| healthy: 0, | ||
| needsAttention: 0, | ||
| stale: 0, | ||
| abandoned: 0, | ||
| deprecated: 0 | ||
| } | ||
| }; | ||
| } | ||
| } | ||
| function calculateHealthScore(qualityData, githubMetrics) { | ||
| let score = 10; | ||
| if (status === 'ABANDONED') { | ||
| const years = Math.floor(daysSincePublish / 365); | ||
| return { | ||
| action: 'high', | ||
| message: `Last updated ${years} year${years > 1 ? 's' : ''} ago`, | ||
| recommendation: 'Migrate to an actively maintained alternative' | ||
| }; | ||
| // Penalize based on maintenance status | ||
| if (qualityData.deprecated) { | ||
| score = 0; // Deprecated = 0 | ||
| } else if (qualityData.abandoned) { | ||
| score = 2; // Abandoned = 2 | ||
| } else if (qualityData.stale) { | ||
| score = 5; // Stale = 5 | ||
| } else if (qualityData.monthsSinceUpdate) { | ||
| // Gradual penalty for age | ||
| const months = qualityData.monthsSinceUpdate; | ||
| if (months > 12) score -= 2; | ||
| else if (months > 6) score -= 1; | ||
| } | ||
| if (status === 'STALE') { | ||
| const months = Math.floor(daysSincePublish / 30); | ||
| return { | ||
| action: 'medium', | ||
| message: `Not updated in ${months} month${months > 1 ? 's' : ''} ago`, | ||
| recommendation: 'Consider finding a more actively maintained package' | ||
| }; | ||
| // Adjust based on GitHub metrics if available | ||
| if (githubMetrics) { | ||
| const { openIssues, totalIssues, starsCount } = githubMetrics; | ||
| // Penalize high issue ratio | ||
| if (totalIssues > 0) { | ||
| const issueRatio = openIssues / totalIssues; | ||
| if (issueRatio > 0.8) score -= 1; | ||
| if (issueRatio > 0.9) score -= 1; | ||
| } | ||
| // Bonus for popular packages | ||
| if (starsCount > 10000) score += 0.5; | ||
| if (starsCount > 50000) score += 0.5; | ||
| } | ||
| if (status === 'NEEDS_ATTENTION') { | ||
| return { | ||
| action: 'low', | ||
| message: `Health score: ${healthScore}/10`, | ||
| recommendation: 'Monitor for updates and potential alternatives' | ||
| }; | ||
| } | ||
| return { | ||
| action: 'none', | ||
| message: 'Package is healthy', | ||
| recommendation: 'No action needed' | ||
| }; | ||
| return Math.max(0, Math.min(10, Math.round(score * 10) / 10)); | ||
| } | ||
@@ -509,7 +143,3 @@ | ||
| analyzePackageQuality, | ||
| calculateHealthScore, | ||
| getPackageStatus, | ||
| getMaintainerStatus, | ||
| getQualityRecommendation, | ||
| fetchNpmPackageInfo | ||
| calculateHealthScore | ||
| }; |
| // src/analyzers/supply-chain.js | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| // v3.1.4 - Dynamic supply chain security analysis | ||
| // Known malicious packages database | ||
| const MALICIOUS_PACKAGES = [ | ||
| 'epress', 'expres', 'expresss', 'requst', 'requist', 'lodas', 'loadash', | ||
| 'axois', 'axioss', 'webpak', 'webback', 'reacts', 'mongose', 'mongosse' | ||
| ]; | ||
| const { analyzer } = require('../services'); | ||
| // Popular packages to check for typosquatting | ||
| const POPULAR_PACKAGES = [ | ||
| 'express', 'request', 'lodash', 'axios', 'webpack', 'react', 'vue', 'angular', | ||
| 'next', 'typescript', 'eslint', 'prettier', 'jest', 'mocha', 'chai' | ||
| ]; | ||
| // Legitimate packages that should NOT be flagged (expanded whitelist) | ||
| const WHITELIST = [ | ||
| 'chalk', 'ora', 'yargs', 'commander', 'semver', 'dotenv', 'debug', 'uuid', | ||
| 'mime', 'qs', 'joi', 'bcrypt', 'passport', 'multer', 'cors', 'helmet', | ||
| 'morgan', 'winston', 'pino', 'bunyan', 'nodemon', 'pm2', 'async', 'bluebird', | ||
| 'ramda', 'underscore', 'moment', 'dayjs', 'date-fns', 'luxon', 'validator', | ||
| 'sanitize-html', 'dompurify', 'cheerio', 'jsdom', 'puppeteer', 'playwright' | ||
| ]; | ||
| // Suspicious install script patterns | ||
| const SUSPICIOUS_PATTERNS = [ | ||
| 'curl', 'wget', 'http://', 'https://', 'eval', 'exec', 'child_process', | ||
| '/bin/sh', '/bin/bash', 'powershell', 'bitcoin', 'mining', 'keylogger', 'backdoor' | ||
| ]; | ||
| async function analyzeSupplyChain(projectPath, dependencies) { | ||
| const warnings = []; | ||
| /** | ||
| * Analyze supply chain security for project dependencies | ||
| */ | ||
| async function analyzeSupplyChain(projectPath, dependencies = {}) { | ||
| const packages = Object.keys(dependencies); | ||
| if (packages.length === 0) { | ||
| return { | ||
| warnings: [], | ||
| total: 0, | ||
| summary: { | ||
| typosquatting: 0, | ||
| suspiciousScripts: 0, | ||
| vulnerabilities: 0 | ||
| } | ||
| }; | ||
| } | ||
| try { | ||
| const packageJsonPath = path.join(projectPath, 'package.json'); | ||
| if (!fs.existsSync(packageJsonPath)) { | ||
| return { warnings: [], total: 0, summary: { malicious: 0, typosquatting: 0, suspiciousScripts: 0 } }; | ||
| } | ||
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); | ||
| const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; | ||
| for (const [pkgName, version] of Object.entries(allDeps)) { | ||
| // Check for malicious packages | ||
| if (MALICIOUS_PACKAGES.includes(pkgName)) { | ||
| const warnings = []; | ||
| // 1. Check for typosquatting (dynamic) | ||
| for (const pkg of packages) { | ||
| const typosquat = analyzer.security.checkTyposquatting(pkg); | ||
| if (typosquat) { | ||
| warnings.push({ | ||
| package: pkgName, | ||
| type: 'malicious', | ||
| severity: 'critical', | ||
| description: 'Known malicious package detected', | ||
| reason: 'This package is known to be malicious', | ||
| action: 'Remove immediately', | ||
| autoFixable: true, | ||
| autoFixAction: 'remove', | ||
| requiresConfirmation: false | ||
| }); | ||
| continue; | ||
| } | ||
| // Check for typosquatting | ||
| const typosquatCheck = checkTyposquatting(pkgName); | ||
| if (typosquatCheck) { | ||
| warnings.push({ | ||
| package: pkgName, | ||
| package: pkg, | ||
| type: 'typosquatting', | ||
| severity: 'high', | ||
| description: `Similar to: ${typosquatCheck.original} (official package)`, | ||
| reason: `Potential malicious package - typo of ${typosquatCheck.original}`, | ||
| action: `Remove ${pkgName} and install ${typosquatCheck.original}`, | ||
| replacement: typosquatCheck.original, | ||
| autoFixable: true, | ||
| autoFixAction: 'replace', | ||
| requiresConfirmation: false | ||
| description: typosquat.warning, | ||
| correctPackage: typosquat.similarTo, | ||
| distance: typosquat.distance, | ||
| reason: `Package name is ${typosquat.distance} character(s) different from popular package "${typosquat.similarTo}"`, | ||
| risk: 'Possible typosquatting attack - malicious package mimicking popular library', | ||
| action: 'remove', | ||
| autoFixable: false // Too risky to auto-remove | ||
| }); | ||
| continue; | ||
| } | ||
| // Check for suspicious install scripts | ||
| const scriptCheck = await checkInstallScripts(projectPath, pkgName); | ||
| if (scriptCheck) { | ||
| } | ||
| // 2. Run npm audit for vulnerabilities (dynamic) | ||
| let auditResults = { vulnerabilities: [], summary: { total: 0 } }; | ||
| try { | ||
| auditResults = analyzer.security.runNpmAudit(projectPath); | ||
| } catch (error) { | ||
| // npm audit may fail in some environments, continue anyway | ||
| } | ||
| // Add high/critical vulnerabilities to warnings | ||
| for (const vuln of auditResults.vulnerabilities) { | ||
| const severity = (vuln.severity || 'moderate').toLowerCase(); | ||
| if (severity === 'critical' || severity === 'high') { | ||
| warnings.push({ | ||
| package: pkgName, | ||
| type: 'suspicious_script', | ||
| severity: 'medium', | ||
| description: `Install script contains suspicious patterns`, | ||
| reason: `Suspicious install script detected`, | ||
| action: 'Review the install script before deployment', | ||
| script: scriptCheck.script, | ||
| patterns: scriptCheck.patterns, | ||
| autoFixable: true, | ||
| autoFixAction: 'review', | ||
| requiresConfirmation: true | ||
| package: vuln.package, | ||
| type: 'vulnerability', | ||
| severity: severity, | ||
| description: vuln.title, | ||
| reason: vuln.title, | ||
| risk: `${severity.toUpperCase()} security vulnerability`, | ||
| action: 'update', | ||
| url: vuln.url, | ||
| range: vuln.range, | ||
| autoFixable: true // npm audit fix can handle this | ||
| }); | ||
| } | ||
| } | ||
| // Calculate summary | ||
| // Summary statistics | ||
| const summary = { | ||
| malicious: warnings.filter(w => w.type === 'malicious').length, | ||
| typosquatting: warnings.filter(w => w.type === 'typosquatting').length, | ||
| suspiciousScripts: warnings.filter(w => w.type === 'suspicious_script').length | ||
| suspiciousScripts: warnings.filter(w => w.type === 'install_script').length, | ||
| vulnerabilities: auditResults.summary.total, | ||
| critical: auditResults.summary.critical || 0, | ||
| high: auditResults.summary.high || 0 | ||
| }; | ||
| return { | ||
| warnings, | ||
| total: warnings.length, | ||
| summary | ||
| summary, | ||
| audit: auditResults | ||
| }; | ||
| } catch (error) { | ||
| console.error('Supply chain analysis error:', error.message); | ||
| return { warnings: [], total: 0, summary: { malicious: 0, typosquatting: 0, suspiciousScripts: 0 } }; | ||
| console.error('[supply-chain] Analysis failed:', error.message); | ||
| return { | ||
| warnings: [], | ||
| total: 0, | ||
| summary: { | ||
| typosquatting: 0, | ||
| suspiciousScripts: 0, | ||
| vulnerabilities: 0 | ||
| } | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * Check for typosquatting using Levenshtein distance | ||
| */ | ||
| function checkTyposquatting(packageName) { | ||
| // Skip if package is in whitelist | ||
| if (WHITELIST.includes(packageName)) { | ||
| return null; | ||
| } | ||
| for (const popular of POPULAR_PACKAGES) { | ||
| // Skip if both are in whitelist | ||
| if (WHITELIST.includes(popular)) { | ||
| continue; | ||
| } | ||
| const distance = levenshteinDistance(packageName, popular); | ||
| // Flag if 1-2 character difference | ||
| if (distance > 0 && distance <= 2 && packageName !== popular) { | ||
| return { original: popular, distance }; | ||
| } | ||
| } | ||
| return null; | ||
| function addToWhitelist(packageName) { | ||
| analyzer.security.addToWhitelist(packageName); | ||
| } | ||
| /** | ||
| * Calculate Levenshtein distance between two strings | ||
| */ | ||
| function levenshteinDistance(str1, str2) { | ||
| const matrix = []; | ||
| for (let i = 0; i <= str2.length; i++) { | ||
| matrix[i] = [i]; | ||
| } | ||
| for (let j = 0; j <= str1.length; j++) { | ||
| matrix[0][j] = j; | ||
| } | ||
| for (let i = 1; i <= str2.length; i++) { | ||
| for (let j = 1; j <= str1.length; j++) { | ||
| if (str2.charAt(i - 1) === str1.charAt(j - 1)) { | ||
| matrix[i][j] = matrix[i - 1][j - 1]; | ||
| } else { | ||
| matrix[i][j] = Math.min( | ||
| matrix[i - 1][j - 1] + 1, | ||
| matrix[i][j - 1] + 1, | ||
| matrix[i - 1][j] + 1 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| return matrix[str2.length][str1.length]; | ||
| function removeFromWhitelist(packageName) { | ||
| analyzer.security.removeFromWhitelist(packageName); | ||
| } | ||
| /** | ||
| * Check package for suspicious install scripts | ||
| */ | ||
| async function checkInstallScripts(projectPath, packageName) { | ||
| try { | ||
| const pkgPath = path.join(projectPath, 'node_modules', packageName, 'package.json'); | ||
| if (!fs.existsSync(pkgPath)) { | ||
| return null; | ||
| } | ||
| const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); | ||
| const scripts = pkgJson.scripts || {}; | ||
| const suspiciousScripts = ['preinstall', 'install', 'postinstall']; | ||
| for (const scriptName of suspiciousScripts) { | ||
| if (scripts[scriptName]) { | ||
| const scriptContent = scripts[scriptName]; | ||
| const foundPatterns = SUSPICIOUS_PATTERNS.filter(pattern => | ||
| scriptContent.toLowerCase().includes(pattern.toLowerCase()) | ||
| ); | ||
| if (foundPatterns.length > 0) { | ||
| return { | ||
| script: scriptName, | ||
| content: scriptContent, | ||
| patterns: foundPatterns | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } catch (error) { | ||
| return null; | ||
| } | ||
| function isWhitelisted(packageName) { | ||
| return analyzer.security.isWhitelisted(packageName); | ||
| } | ||
| module.exports = { analyzeSupplyChain }; | ||
| module.exports = { | ||
| analyzeSupplyChain, | ||
| addToWhitelist, | ||
| removeFromWhitelist, | ||
| isWhitelisted | ||
| }; |
+125
-157
| // src/commands/graph.js | ||
| // v3.1.3 - Fixed analyzeProject import and graph filter enrichment | ||
| // v3.1.4 - Unified graph with dynamic layout/filter controls | ||
| const fs = require('fs'); | ||
@@ -11,3 +12,3 @@ const path = require('path'); | ||
| /** | ||
| * Graph command | ||
| * Graph command - Generate unified interactive dependency visualization | ||
| */ | ||
@@ -29,18 +30,12 @@ async function graphCommand(options) { | ||
| // Validate layout option | ||
| const validLayouts = ['tree', 'force', 'radial', 'conflict']; | ||
| if (!validLayouts.includes(layout)) { | ||
| console.error(chalk.red(`✗ Invalid layout: ${layout}`)); | ||
| console.log(chalk.gray(` Valid options: ${validLayouts.join(', ')}`)); | ||
| return; | ||
| // For JSON export, use traditional single-layout approach | ||
| const isJSONExport = format === 'json' || output.endsWith('.json'); | ||
| if (!isJSONExport) { | ||
| console.log(chalk.cyan('💡 Generating unified interactive graph with:')); | ||
| console.log(chalk.gray(' • All layouts (Tree, Force, Radial, Conflict)')); | ||
| console.log(chalk.gray(' • All filters (Vulnerable, Outdated, Unused, Deprecated)')); | ||
| console.log(chalk.gray(' • Dynamic controls (no page reload needed)\n')); | ||
| } | ||
| // Validate filter option | ||
| const validFilters = ['all', 'vulnerable', 'outdated', 'unused', 'conflict', 'deprecated']; | ||
| if (!validFilters.includes(filter)) { | ||
| console.error(chalk.red(`✗ Invalid filter: ${filter}`)); | ||
| console.log(chalk.gray(` Valid options: ${validFilters.join(', ')}`)); | ||
| return; | ||
| } | ||
| const spinner = ora('Generating dependency graph...').start(); | ||
@@ -56,7 +51,5 @@ | ||
| // v3.1.3 - FIXED: Import analyzeProject correctly | ||
| try { | ||
| const analyzeModule = require('./analyze'); | ||
| // Check if analyzeProject exists (v3.1.3+) | ||
| if (typeof analyzeModule.analyzeProject === 'function') { | ||
@@ -69,23 +62,6 @@ spinner.text = 'Running analysis for graph enrichment...'; | ||
| analysisLoaded = true; | ||
| // Debug: log what we got | ||
| if (process.env.DEBUG) { | ||
| console.log('\n[graph] Analysis results loaded:'); | ||
| console.log(' - Security vulnerabilities:', analysisResults.security?.vulnerabilities?.length || 0); | ||
| console.log(' - Outdated packages:', analysisResults.outdatedPackages?.length || 0); | ||
| console.log(' - Unused dependencies:', analysisResults.unusedDependencies?.length || 0); | ||
| console.log(' - Ecosystem alerts:', analysisResults.ecosystemAlerts?.length || 0); | ||
| } | ||
| } | ||
| } else { | ||
| // Fallback for older versions without analyzeProject | ||
| if (process.env.DEBUG) { | ||
| console.log('[graph] analyzeProject not available, skipping enrichment'); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| // Analysis not available, continue without enrichment | ||
| if (process.env.DEBUG) { | ||
| console.log('[graph] Analysis failed:', error.message); | ||
| } | ||
| } | ||
@@ -95,7 +71,6 @@ | ||
| // v3.1.3 - generator.generate() is async, must use await | ||
| const graphData = await generator.generate({ | ||
| maxDepth: depth !== Infinity ? parseInt(depth) : Infinity, | ||
| filter, | ||
| enrichWithIssues: false // Dynamic npm fetching disabled for speed | ||
| enrichWithIssues: false | ||
| }); | ||
@@ -108,11 +83,16 @@ | ||
| // v3.1.3 - Show enrichment status in spinner | ||
| if (analysisLoaded) { | ||
| const issueCount = graphData.nodes.filter(n => n.issues && n.issues.length > 0).length; | ||
| spinner.succeed(`Generated graph with ${graphData.nodes.length} nodes (${issueCount} with issues)`); | ||
| } else { | ||
| spinner.succeed(`Generated graph with ${graphData.nodes.length} nodes`); | ||
| } | ||
| // Add metadata for unified HTML | ||
| graphData.metadata = graphData.metadata || {}; | ||
| graphData.metadata.availableLayouts = ['tree', 'force', 'radial', 'conflict']; | ||
| graphData.metadata.availableFilters = ['all', 'vulnerable', 'outdated', 'unused', 'deprecated', 'conflict']; | ||
| graphData.metadata.defaultLayout = layout; | ||
| graphData.metadata.defaultFilter = filter; | ||
| graphData.metadata.defaultDepth = depth !== Infinity ? depth : 10; | ||
| graphData.metadata.width = width; | ||
| graphData.metadata.height = height; | ||
| // Detect format from output filename if not specified | ||
| const issueCount = graphData.nodes.filter(n => n.issues && n.issues.length > 0).length; | ||
| spinner.succeed(`Generated graph with ${chalk.cyan(graphData.nodes.length)} nodes${issueCount > 0 ? ` (${chalk.yellow(issueCount)} with issues)` : ''}`); | ||
| // Detect format | ||
| let detectedFormat = format; | ||
@@ -130,3 +110,4 @@ if (!detectedFormat) { | ||
| height: parseInt(height), | ||
| filter | ||
| filter, | ||
| unified: detectedFormat === 'html' // Enable unified mode for HTML | ||
| }); | ||
@@ -147,65 +128,6 @@ | ||
| // Display summary | ||
| console.log('\n' + chalk.gray('─'.repeat(70))); | ||
| console.log(chalk.bold('\n📈 GRAPH SUMMARY\n')); | ||
| console.log(` ${chalk.gray('Format:')} ${result.format.toUpperCase()}`); | ||
| console.log(` ${chalk.gray('Layout:')} ${layout}`); | ||
| console.log(` ${chalk.gray('Total Nodes:')} ${graphData.nodes.length}`); | ||
| console.log(` ${chalk.gray('Total Links:')} ${graphData.links.length}`); | ||
| console.log(` ${chalk.gray('Max Depth:')} ${graphData.metadata.maxDepth}`); | ||
| console.log(` ${chalk.gray('File Size:')} ${result.fileSize || getFileSize(result.path)}`); | ||
| if (filter !== 'all') { | ||
| console.log(` ${chalk.gray('Filter:')} ${filter}`); | ||
| console.log(` ${chalk.gray('Filtered:')} ${graphData.metadata.visibleDependencies} / ${graphData.metadata.totalDependencies}`); | ||
| } | ||
| // v3.1.3 - Show enrichment status with detailed breakdown | ||
| if (analysisLoaded) { | ||
| const issueNodes = graphData.nodes.filter(n => n.issues && n.issues.length > 0).length; | ||
| const vulnNodes = graphData.nodes.filter(n => n.isVulnerable).length; | ||
| const outdatedNodes = graphData.nodes.filter(n => n.isOutdated).length; | ||
| const unusedNodes = graphData.nodes.filter(n => n.isUnused).length; | ||
| const deprecatedNodes = graphData.nodes.filter(n => n.isDeprecated).length; | ||
| console.log(` ${chalk.gray('Enriched:')} ${chalk.green('✓')} Analysis data applied`); | ||
| if (issueNodes > 0) { | ||
| console.log(` ${chalk.gray('With Issues:')} ${issueNodes} packages`); | ||
| if (vulnNodes > 0) console.log(` ${chalk.gray('Vulnerable:')} ${chalk.red(vulnNodes)}`); | ||
| if (outdatedNodes > 0) console.log(` ${chalk.gray('Outdated:')} ${chalk.yellow(outdatedNodes)}`); | ||
| if (unusedNodes > 0) console.log(` ${chalk.gray('Unused:')} ${chalk.blue(unusedNodes)}`); | ||
| if (deprecatedNodes > 0) console.log(` ${chalk.gray('Deprecated:')} ${chalk.magenta(deprecatedNodes)}`); | ||
| } else { | ||
| console.log(` ${chalk.gray('With Issues:')} ${chalk.green('0 (healthy project)')}`); | ||
| } | ||
| } else { | ||
| console.log(` ${chalk.gray('Enriched:')} ${chalk.yellow('✗')} Run 'devcompass analyze' first for full data`); | ||
| } | ||
| if (result.method) { | ||
| console.log(` ${chalk.gray('Export Method:')} ${result.method}`); | ||
| } | ||
| console.log('\n' + chalk.gray('─'.repeat(70))); | ||
| displaySummary(graphData, result, analysisLoaded, options); | ||
| // Show format-specific tips | ||
| if (result.format === 'html') { | ||
| console.log(chalk.cyan('\n💡 TIPS:')); | ||
| console.log(` • ${chalk.gray('Zoom:')} Mouse wheel or pinch`); | ||
| console.log(` • ${chalk.gray('Pan:')} Click and drag`); | ||
| console.log(` • ${chalk.gray('Details:')} Hover over nodes`); | ||
| if (layout === 'force') { | ||
| console.log(` • ${chalk.gray('Move nodes:')} Drag individual nodes`); | ||
| console.log(` • ${chalk.gray('Reset:')} Use "Reset Layout" button`); | ||
| } | ||
| if (options.includeSearch !== false) { | ||
| console.log(` • ${chalk.gray('Search:')} Use search panel on left`); | ||
| console.log(` • ${chalk.gray('Filter:')} Apply filters to focus on issues`); | ||
| } | ||
| } | ||
| // Open in browser if requested | ||
| if (result.format === 'html' && shouldOpen) { | ||
| if (result.format === 'HTML' && shouldOpen) { | ||
| try { | ||
@@ -221,38 +143,2 @@ console.log(chalk.cyan('\n🌐 Opening in browser...')); | ||
| // Show next steps | ||
| console.log(chalk.bold('\n📋 NEXT STEPS:\n')); | ||
| if (result.format === 'html') { | ||
| console.log(` 1. Open in browser: ${chalk.cyan(`file://${path.resolve(result.path)}`)}`); | ||
| console.log(` 2. Explore dependencies interactively`); | ||
| console.log(` 3. Use filters to identify issues`); | ||
| } | ||
| // v3.1.3 - Show filter suggestions based on analysis | ||
| if (filter === 'all' && analysisLoaded) { | ||
| const vulnCount = graphData.nodes.filter(n => n.isVulnerable).length; | ||
| const outdatedCount = graphData.nodes.filter(n => n.isOutdated).length; | ||
| const unusedCount = graphData.nodes.filter(n => n.isUnused).length; | ||
| if (vulnCount > 0) { | ||
| console.log(` • Try: ${chalk.cyan(`devcompass graph --filter vulnerable`)} to see ${chalk.red(vulnCount)} vulnerable packages`); | ||
| } | ||
| if (outdatedCount > 0) { | ||
| console.log(` • Try: ${chalk.cyan(`devcompass graph --filter outdated`)} to see ${chalk.yellow(outdatedCount)} outdated packages`); | ||
| } | ||
| if (unusedCount > 0) { | ||
| console.log(` • Try: ${chalk.cyan(`devcompass graph --filter unused`)} to see ${chalk.blue(unusedCount)} unused packages`); | ||
| } | ||
| if (vulnCount === 0 && outdatedCount === 0 && unusedCount === 0) { | ||
| console.log(` • ${chalk.green('✓')} Your project looks healthy! No issues detected.`); | ||
| } | ||
| } else if (filter === 'all') { | ||
| console.log(` • Try: ${chalk.cyan(`devcompass graph --filter conflict`)} to see only problematic packages`); | ||
| } | ||
| if (layout === 'tree') { | ||
| console.log(` • Try: ${chalk.cyan(`devcompass graph --layout force`)} for interactive physics layout`); | ||
| console.log(` • Try: ${chalk.cyan(`devcompass graph --layout radial`)} for circular visualization`); | ||
| } | ||
| console.log(chalk.green('\n✓ Graph generation complete!\n')); | ||
@@ -262,10 +148,2 @@ | ||
| exportSpinner.fail(`Export failed: ${result.error}`); | ||
| // Show helpful error messages | ||
| if (result.error.includes('puppeteer') || result.error.includes('canvas')) { | ||
| console.log(chalk.yellow('\n💡 TIP: For PNG export, install one of:')); | ||
| console.log(chalk.gray(' npm install -g puppeteer (recommended, ~300MB)')); | ||
| console.log(chalk.gray(' npm install -g canvas (lighter, ~50MB)')); | ||
| console.log(chalk.gray('\nOr use HTML/SVG formats which require no additional dependencies.')); | ||
| } | ||
| } | ||
@@ -280,12 +158,102 @@ | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Display comprehensive summary | ||
| */ | ||
| function displaySummary(graphData, result, analysisLoaded, options) { | ||
| const stats = { | ||
| totalNodes: graphData.nodes.length, | ||
| totalLinks: graphData.links.length, | ||
| maxDepth: graphData.metadata.maxDepth, | ||
| withIssues: graphData.nodes.filter(n => n.issues && n.issues.length > 0).length, | ||
| vulnerable: graphData.nodes.filter(n => n.isVulnerable).length, | ||
| deprecated: graphData.nodes.filter(n => n.isDeprecated).length, | ||
| outdated: graphData.nodes.filter(n => n.isOutdated).length, | ||
| unused: graphData.nodes.filter(n => n.isUnused).length | ||
| }; | ||
| console.log('\n' + chalk.gray('─'.repeat(70))); | ||
| console.log(chalk.bold('\n📈 GRAPH SUMMARY\n')); | ||
| console.log(` ${chalk.gray('Format:')} ${result.format}`); | ||
| if (result.format === 'HTML') { | ||
| console.log(` ${chalk.gray('Mode:')} ${chalk.green('✓ Unified Interactive')}`); | ||
| console.log(` ${chalk.gray('Layouts:')} Tree, Force, Radial, Conflict ${chalk.gray('(switchable)')}`); | ||
| console.log(` ${chalk.gray('Filters:')} All, Vulnerable, Outdated, Unused, Deprecated ${chalk.gray('(switchable)')}`); | ||
| } else { | ||
| console.log(` ${chalk.gray('Layout:')} ${options.layout || 'tree'}`); | ||
| } | ||
| console.log(` ${chalk.gray('Total Nodes:')} ${stats.totalNodes}`); | ||
| console.log(` ${chalk.gray('Total Links:')} ${stats.totalLinks}`); | ||
| console.log(` ${chalk.gray('Max Depth:')} ${stats.maxDepth}`); | ||
| console.log(` ${chalk.gray('File Size:')} ${result.fileSize || getFileSize(result.path)}`); | ||
| if (analysisLoaded) { | ||
| console.log(` ${chalk.gray('Enriched:')} ${chalk.green('✓ Analysis data applied')}`); | ||
| // Show troubleshooting tips | ||
| console.log(chalk.yellow('\n💡 TROUBLESHOOTING:')); | ||
| console.log(chalk.gray(' • Ensure package.json exists in the project directory')); | ||
| console.log(chalk.gray(' • Run npm install to generate package-lock.json')); | ||
| console.log(chalk.gray(' • Check file permissions for output directory')); | ||
| console.log(chalk.gray(` • Try: ${chalk.cyan('devcompass graph --format json')} for simpler output`)); | ||
| if (stats.withIssues > 0) { | ||
| console.log(` ${chalk.gray('With Issues:')} ${stats.withIssues} packages`); | ||
| if (stats.vulnerable > 0) console.log(` ${chalk.gray('Vulnerable:')} ${chalk.red(stats.vulnerable)}`); | ||
| if (stats.deprecated > 0) console.log(` ${chalk.gray('Deprecated:')} ${chalk.magenta(stats.deprecated)}`); | ||
| if (stats.outdated > 0) console.log(` ${chalk.gray('Outdated:')} ${chalk.yellow(stats.outdated)}`); | ||
| if (stats.unused > 0) console.log(` ${chalk.gray('Unused:')} ${chalk.blue(stats.unused)}`); | ||
| } | ||
| } | ||
| console.log('\n' + chalk.gray('─'.repeat(70))); | ||
| if (result.format === 'HTML') { | ||
| console.log(chalk.bold('\n📋 INTERACTIVE CONTROLS\n')); | ||
| console.log(' Open the HTML file to access:'); | ||
| console.log(` ${chalk.cyan('•')} Layout switcher (Tree/Force/Radial/Conflict)`); | ||
| console.log(` ${chalk.cyan('•')} Filter controls (Vulnerable/Outdated/Unused/Deprecated)`); | ||
| console.log(` ${chalk.cyan('•')} Depth slider (1-10)`); | ||
| console.log(` ${chalk.cyan('•')} Search functionality`); | ||
| console.log(` ${chalk.cyan('•')} Zoom & pan controls`); | ||
| console.log(` ${chalk.cyan('•')} Real-time updates (no page reload)`); | ||
| console.log(chalk.bold('\n💡 USAGE TIPS\n')); | ||
| console.log(` ${chalk.gray('Zoom:')} Mouse wheel or pinch`); | ||
| console.log(` ${chalk.gray('Pan:')} Click and drag background`); | ||
| console.log(` ${chalk.gray('Move nodes:')} Drag nodes (Force layout)`); | ||
| console.log(` ${chalk.gray('Node details:')} Hover over nodes`); | ||
| console.log(` ${chalk.gray('Search:')} Type package name in search box`); | ||
| console.log(chalk.cyan('\n──────────────────────────────────────────────────────────────────────\n')); | ||
| } | ||
| // Suggestions | ||
| if (stats.vulnerable > 0 || stats.deprecated > 0 || stats.outdated > 0) { | ||
| console.log(chalk.bold('📋 SUGGESTIONS\n')); | ||
| if (stats.vulnerable > 0) { | ||
| console.log(chalk.yellow(` ⚠️ ${stats.vulnerable} vulnerable package(s) detected`)); | ||
| console.log(` ${chalk.gray('→')} Use ${chalk.cyan('Vulnerable filter')} in the graph UI`); | ||
| console.log(` ${chalk.gray('→')} Run: ${chalk.cyan('devcompass fix')} to resolve\n`); | ||
| } | ||
| if (stats.deprecated > 0) { | ||
| console.log(chalk.yellow(` ⚠️ ${stats.deprecated} deprecated package(s) found`)); | ||
| console.log(` ${chalk.gray('→')} Use ${chalk.cyan('Deprecated filter')} in the graph UI`); | ||
| console.log(` ${chalk.gray('→')} Run: ${chalk.cyan('devcompass fix --only quality')}\n`); | ||
| } | ||
| if (stats.outdated > 0) { | ||
| console.log(chalk.yellow(` ⚠️ ${stats.outdated} outdated package(s) found`)); | ||
| console.log(` ${chalk.gray('→')} Use ${chalk.cyan('Outdated filter')} in the graph UI`); | ||
| console.log(` ${chalk.gray('→')} Run: ${chalk.cyan('npm update')}\n`); | ||
| } | ||
| } else if (analysisLoaded) { | ||
| console.log(chalk.bold('📋 STATUS\n')); | ||
| console.log(` ${chalk.green('✓')} Your project looks healthy! No critical issues detected.\n`); | ||
| } | ||
| } | ||
| /** | ||
| * Get file size helper | ||
| */ | ||
| function getFileSize(filePath) { | ||
@@ -292,0 +260,0 @@ try { |
+98
-246
| // src/graph/exporter.js | ||
| // Graph exporter - generates HTML/JSON output from graph data | ||
| // v3.1.2 - Fixed: module.exports = GraphExporter (not object) | ||
| // v3.1.4 - Unified graph exporter with dynamic controls | ||
@@ -8,3 +7,3 @@ const fs = require('fs'); | ||
| // Import layout generators with safe fallbacks | ||
| // Import layout generators | ||
| let generateTreeLayoutHTML, generateRadialLayoutHTML, generateForceLayoutHTML, generateConflictLayoutHTML; | ||
@@ -41,236 +40,3 @@ | ||
| /** | ||
| * Fallback HTML generator if layout file fails to load | ||
| */ | ||
| function generateFallbackHTML(graphData, options, layoutType) { | ||
| const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : []; | ||
| const links = Array.isArray(graphData.links) ? graphData.links : []; | ||
| const projectName = options.projectName || 'Project'; | ||
| return `<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>DevCompass - Dependency Graph</title> | ||
| <script src="https://d3js.org/d3.v7.min.js"></script> | ||
| <style> | ||
| :root { | ||
| --bg-primary: #0f172a; | ||
| --bg-secondary: #1e293b; | ||
| --text-primary: #f1f5f9; | ||
| --text-secondary: #94a3b8; | ||
| --accent-blue: #3b82f6; | ||
| --accent-cyan: #06b6d4; | ||
| --border-color: #475569; | ||
| } | ||
| * { margin: 0; padding: 0; box-sizing: border-box; } | ||
| body { | ||
| font-family: 'Segoe UI', system-ui, sans-serif; | ||
| background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); | ||
| color: var(--text-primary); | ||
| min-height: 100vh; | ||
| overflow: hidden; | ||
| } | ||
| .header { | ||
| position: fixed; | ||
| top: 20px; | ||
| left: 50%; | ||
| transform: translateX(-50%); | ||
| background: rgba(30, 41, 59, 0.95); | ||
| padding: 16px 32px; | ||
| border-radius: 16px; | ||
| border: 1px solid var(--border-color); | ||
| backdrop-filter: blur(12px); | ||
| z-index: 100; | ||
| text-align: center; | ||
| } | ||
| .header h1 { font-size: 20px; margin-bottom: 4px; } | ||
| .header p { font-size: 12px; color: var(--text-secondary); } | ||
| svg { width: 100vw; height: 100vh; cursor: grab; } | ||
| svg:active { cursor: grabbing; } | ||
| .node circle { | ||
| fill: var(--accent-blue); | ||
| stroke: var(--bg-primary); | ||
| stroke-width: 2px; | ||
| cursor: pointer; | ||
| transition: all 0.2s; | ||
| } | ||
| .node circle:hover { | ||
| stroke: var(--text-primary); | ||
| filter: drop-shadow(0 0 8px var(--accent-cyan)); | ||
| } | ||
| .node.root circle { fill: #60a5fa; r: 18; } | ||
| .node text { | ||
| fill: var(--text-secondary); | ||
| font-size: 10px; | ||
| pointer-events: none; | ||
| } | ||
| .link { | ||
| stroke: var(--border-color); | ||
| stroke-width: 1.5px; | ||
| stroke-opacity: 0.5; | ||
| } | ||
| .controls { | ||
| position: fixed; | ||
| bottom: 20px; | ||
| right: 20px; | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 8px; | ||
| z-index: 100; | ||
| } | ||
| .controls button { | ||
| width: 44px; | ||
| height: 44px; | ||
| background: rgba(30, 41, 59, 0.95); | ||
| border: 1px solid var(--border-color); | ||
| border-radius: 12px; | ||
| color: var(--text-primary); | ||
| font-size: 20px; | ||
| cursor: pointer; | ||
| backdrop-filter: blur(12px); | ||
| } | ||
| .controls button:hover { | ||
| background: var(--accent-blue); | ||
| border-color: var(--accent-blue); | ||
| } | ||
| .legend { | ||
| position: fixed; | ||
| bottom: 20px; | ||
| left: 20px; | ||
| background: rgba(30, 41, 59, 0.95); | ||
| padding: 16px; | ||
| border-radius: 16px; | ||
| border: 1px solid var(--border-color); | ||
| backdrop-filter: blur(12px); | ||
| z-index: 100; | ||
| } | ||
| .legend-title { font-size: 13px; font-weight: 700; margin-bottom: 10px; } | ||
| .legend-item { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 11px; color: var(--text-secondary); } | ||
| .legend-dot { width: 12px; height: 12px; border-radius: 50%; } | ||
| .tooltip { | ||
| position: absolute; | ||
| padding: 12px 16px; | ||
| background: rgba(15, 23, 42, 0.98); | ||
| border: 1px solid var(--border-color); | ||
| border-radius: 10px; | ||
| font-size: 12px; | ||
| pointer-events: none; | ||
| opacity: 0; | ||
| transition: opacity 0.2s; | ||
| z-index: 1000; | ||
| backdrop-filter: blur(12px); | ||
| } | ||
| .tooltip.visible { opacity: 1; } | ||
| .tooltip-title { font-weight: 700; color: var(--accent-cyan); margin-bottom: 6px; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="header"> | ||
| <h1>🧭 DevCompass - ${layoutType} Layout</h1> | ||
| <p>${nodes.length} packages • ${links.length} dependencies</p> | ||
| </div> | ||
| <svg id="graph"></svg> | ||
| <div class="controls"> | ||
| <button onclick="zoomIn()" title="Zoom In">+</button> | ||
| <button onclick="zoomOut()" title="Zoom Out">−</button> | ||
| <button onclick="resetZoom()" title="Reset">⟲</button> | ||
| </div> | ||
| <div class="legend"> | ||
| <div class="legend-title">🎨 Health Status</div> | ||
| <div class="legend-item"><div class="legend-dot" style="background: #10b981;"></div>Healthy (7-10)</div> | ||
| <div class="legend-item"><div class="legend-dot" style="background: #eab308;"></div>Caution (5-7)</div> | ||
| <div class="legend-item"><div class="legend-dot" style="background: #f97316;"></div>Warning (3-5)</div> | ||
| <div class="legend-item"><div class="legend-dot" style="background: #ef4444;"></div>Critical (<3)</div> | ||
| <div class="legend-item"><div class="legend-dot" style="background: #60a5fa;"></div>Root Package</div> | ||
| </div> | ||
| <div class="tooltip" id="tooltip"></div> | ||
| <script> | ||
| const graphData = ${JSON.stringify({ nodes, links })}; | ||
| const width = window.innerWidth; | ||
| const height = window.innerHeight; | ||
| function getColor(node) { | ||
| if (node.type === 'root') return '#60a5fa'; | ||
| const score = node.healthScore || 8; | ||
| if (score >= 7) return '#10b981'; | ||
| if (score >= 5) return '#eab308'; | ||
| if (score >= 3) return '#f97316'; | ||
| return '#ef4444'; | ||
| } | ||
| function getRadius(node) { | ||
| if (node.type === 'root') return 18; | ||
| if (node.depth === 1) return 10; | ||
| return 6; | ||
| } | ||
| const svg = d3.select("#graph").attr("width", width).attr("height", height); | ||
| const g = svg.append("g"); | ||
| const zoom = d3.zoom() | ||
| .scaleExtent([0.1, 4]) | ||
| .on("zoom", (e) => g.attr("transform", e.transform)); | ||
| svg.call(zoom); | ||
| const simulation = d3.forceSimulation(graphData.nodes) | ||
| .force("link", d3.forceLink(graphData.links).id(d => d.id).distance(60)) | ||
| .force("charge", d3.forceManyBody().strength(-150)) | ||
| .force("center", d3.forceCenter(width / 2, height / 2)) | ||
| .force("collision", d3.forceCollide().radius(d => getRadius(d) + 5)); | ||
| const link = g.append("g").selectAll("line") | ||
| .data(graphData.links).join("line").attr("class", "link"); | ||
| const node = g.append("g").selectAll("g") | ||
| .data(graphData.nodes).join("g") | ||
| .attr("class", d => "node" + (d.type === "root" ? " root" : "")) | ||
| .call(d3.drag() | ||
| .on("start", (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }) | ||
| .on("drag", (e, d) => { d.fx = e.x; d.fy = e.y; }) | ||
| .on("end", (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })) | ||
| .on("mouseover", showTooltip) | ||
| .on("mouseout", hideTooltip); | ||
| node.append("circle").attr("r", d => getRadius(d)).attr("fill", d => getColor(d)); | ||
| node.append("text").attr("dy", d => getRadius(d) + 12).attr("text-anchor", "middle") | ||
| .text(d => (d.name || d.id).substring(0, 20)); | ||
| simulation.on("tick", () => { | ||
| link.attr("x1", d => d.source.x).attr("y1", d => d.source.y) | ||
| .attr("x2", d => d.target.x).attr("y2", d => d.target.y); | ||
| node.attr("transform", d => "translate(" + d.x + "," + d.y + ")"); | ||
| }); | ||
| function showTooltip(event, d) { | ||
| const tooltip = document.getElementById('tooltip'); | ||
| tooltip.innerHTML = '<div class="tooltip-title">' + (d.name || d.id) + '</div>' + | ||
| 'Version: ' + (d.version || 'N/A') + '<br>' + | ||
| 'Health: ' + (d.healthScore || 8) + '/10<br>' + | ||
| 'Depth: ' + (d.depth || 0); | ||
| tooltip.style.left = (event.pageX + 15) + 'px'; | ||
| tooltip.style.top = (event.pageY - 10) + 'px'; | ||
| tooltip.classList.add('visible'); | ||
| } | ||
| function hideTooltip() { | ||
| document.getElementById('tooltip').classList.remove('visible'); | ||
| } | ||
| window.zoomIn = () => svg.transition().call(zoom.scaleBy, 1.3); | ||
| window.zoomOut = () => svg.transition().call(zoom.scaleBy, 0.7); | ||
| window.resetZoom = () => svg.transition().call(zoom.transform, d3.zoomIdentity); | ||
| </script> | ||
| </body> | ||
| </html>`; | ||
| } | ||
| /** | ||
| * GraphExporter - Exports graph data to various formats | ||
| * This class IS the module.exports (not wrapped in object) | ||
| */ | ||
@@ -287,2 +53,3 @@ class GraphExporter { | ||
| filter: options.filter || 'all', | ||
| unified: options.unified !== false, // v3.1.4 - Enable unified mode by default | ||
| ...options | ||
@@ -324,2 +91,3 @@ }; | ||
| n.type === 'root' || | ||
| n.isVulnerable === true || | ||
| (Array.isArray(n.issues) && n.issues.some(i => | ||
@@ -347,2 +115,10 @@ i.type === 'security' || i.type === 'vulnerability' | ||
| case 'deprecated': | ||
| filteredNodes = nodes.filter(n => | ||
| n.type === 'root' || | ||
| n.isDeprecated === true || | ||
| (Array.isArray(n.issues) && n.issues.some(i => i.type === 'deprecated')) | ||
| ); | ||
| break; | ||
| case 'conflict': | ||
@@ -375,5 +151,26 @@ filteredNodes = nodes.filter(n => | ||
| /** | ||
| * Generate HTML content based on layout type | ||
| * Generate unified HTML with dynamic controls (v3.1.4) | ||
| */ | ||
| generateHTML() { | ||
| generateUnifiedHTML() { | ||
| const templatePath = path.join(__dirname, 'template.html'); | ||
| try { | ||
| let template = fs.readFileSync(templatePath, 'utf8'); | ||
| // Inject graph data - replace the placeholder | ||
| template = template.replace('{{GRAPH_DATA}}', JSON.stringify(this.graphData, null, 2)); | ||
| return template; | ||
| } catch (error) { | ||
| console.error('Failed to load unified template:', error.message); | ||
| console.error('Template path:', templatePath); | ||
| // Fallback to traditional layout | ||
| return this.generateTraditionalHTML(); | ||
| } | ||
| } | ||
| /** | ||
| * Generate traditional HTML (single layout) | ||
| */ | ||
| generateTraditionalHTML() { | ||
| const filteredData = this.applyFilter(); | ||
@@ -413,7 +210,66 @@ const layout = (this.options.layout || 'tree').toLowerCase(); | ||
| // Fallback to built-in generator | ||
| return generateFallbackHTML(filteredData, this.options, layout); | ||
| // Final fallback | ||
| return this.generateFallbackHTML(filteredData, layout); | ||
| } | ||
| /** | ||
| * Generate HTML content | ||
| */ | ||
| generateHTML() { | ||
| // v3.1.4 - Use unified template by default | ||
| if (this.options.unified) { | ||
| return this.generateUnifiedHTML(); | ||
| } | ||
| // Fallback to traditional single-layout mode | ||
| return this.generateTraditionalHTML(); | ||
| } | ||
| /** | ||
| * Fallback HTML generator | ||
| */ | ||
| generateFallbackHTML(graphData, layoutType) { | ||
| const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : []; | ||
| const links = Array.isArray(graphData.links) ? graphData.links : []; | ||
| return `<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>DevCompass - Dependency Graph</title> | ||
| <script src="https://d3js.org/d3.v7.min.js"></script> | ||
| <style> | ||
| body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; margin: 0; } | ||
| svg { width: 100vw; height: 100vh; } | ||
| .node circle { fill: #3b82f6; stroke: #fff; stroke-width: 2px; } | ||
| .node text { fill: #94a3b8; font-size: 10px; } | ||
| .link { stroke: #475569; stroke-width: 1.5px; stroke-opacity: 0.6; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <svg id="graph"></svg> | ||
| <script> | ||
| const data = ${JSON.stringify({ nodes, links })}; | ||
| const svg = d3.select("#graph"); | ||
| const width = window.innerWidth; | ||
| const height = window.innerHeight; | ||
| const simulation = d3.forceSimulation(data.nodes) | ||
| .force("link", d3.forceLink(data.links).id(d => d.id)) | ||
| .force("charge", d3.forceManyBody().strength(-200)) | ||
| .force("center", d3.forceCenter(width/2, height/2)); | ||
| const link = svg.append("g").selectAll("line").data(data.links).join("line").attr("class", "link"); | ||
| const node = svg.append("g").selectAll("g").data(data.nodes).join("g").attr("class", "node"); | ||
| node.append("circle").attr("r", 8); | ||
| node.append("text").attr("dy", -12).text(d => d.name); | ||
| simulation.on("tick", () => { | ||
| link.attr("x1", d => d.source.x).attr("y1", d => d.source.y).attr("x2", d => d.target.x).attr("y2", d => d.target.y); | ||
| node.attr("transform", d => \`translate(\${d.x},\${d.y})\`); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html>`; | ||
| } | ||
| /** | ||
| * Generate JSON output | ||
@@ -442,3 +298,3 @@ */ | ||
| /** | ||
| * Export to file (main method called by graph.js) | ||
| * Export to file (main method) | ||
| */ | ||
@@ -450,3 +306,3 @@ export(outputPath) { | ||
| /** | ||
| * Export to file | ||
| * Export to file implementation | ||
| */ | ||
@@ -510,6 +366,2 @@ exportToFile(outputPath) { | ||
| // ============================================================================ | ||
| // CRITICAL: Export the CLASS DIRECTLY, not wrapped in an object | ||
| // This matches: const GraphExporter = require('../graph/exporter'); | ||
| // ============================================================================ | ||
| module.exports = GraphExporter; |
+967
-357
@@ -6,3 +6,3 @@ <!DOCTYPE html> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>{{TITLE}} - DevCompass</title> | ||
| <title>DevCompass - Dependency Graph</title> | ||
| <script src="https://d3js.org/d3.v7.min.js"></script> | ||
@@ -15,262 +15,349 @@ <style> | ||
| } | ||
| body { | ||
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | ||
| background: #0f172a; | ||
| color: #e2e8f0; | ||
| padding: 20px; | ||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | ||
| background: #0a0e1a; | ||
| color: #e0e6ed; | ||
| overflow: hidden; | ||
| } | ||
| /* ========== HEADER ========== */ | ||
| .header { | ||
| background: linear-gradient(135deg, #1e293b 0%, #334155 100%); | ||
| padding: 24px; | ||
| border-radius: 12px; | ||
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | ||
| margin-bottom: 20px; | ||
| border: 1px solid #334155; | ||
| background: linear-gradient(135deg, #1a1f35 0%, #0f1219 100%); | ||
| border-bottom: 1px solid #2a3142; | ||
| padding: 1rem 1.5rem; | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| flex-wrap: wrap; | ||
| gap: 1rem; | ||
| position: relative; | ||
| z-index: 1000; | ||
| } | ||
| h1 { | ||
| font-size: 28px; | ||
| color: #f1f5f9; | ||
| margin-bottom: 12px; | ||
| font-weight: 700; | ||
| .header h1 { | ||
| font-size: 1.25rem; | ||
| font-weight: 600; | ||
| color: #fff; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 12px; | ||
| gap: 0.5rem; | ||
| } | ||
| h1::before { | ||
| content: "📊"; | ||
| font-size: 32px; | ||
| .header h1 .icon { | ||
| font-size: 1.5rem; | ||
| } | ||
| .metadata { | ||
| color: #94a3b8; | ||
| font-size: 14px; | ||
| /* ========== CONTROLS PANEL ========== */ | ||
| .controls { | ||
| display: flex; | ||
| gap: 1.5rem; | ||
| align-items: center; | ||
| flex-wrap: wrap; | ||
| gap: 24px; | ||
| } | ||
| .metadata-item { | ||
| .control-group { | ||
| display: flex; | ||
| gap: 0.5rem; | ||
| align-items: center; | ||
| gap: 6px; | ||
| } | ||
| .metadata-label { | ||
| font-weight: 600; | ||
| color: #cbd5e1; | ||
| .control-label { | ||
| font-size: 0.85rem; | ||
| color: #8b92a7; | ||
| font-weight: 500; | ||
| } | ||
| .controls { | ||
| background: #1e293b; | ||
| padding: 16px; | ||
| border-radius: 12px; | ||
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | ||
| margin-bottom: 20px; | ||
| .btn-group { | ||
| display: flex; | ||
| gap: 12px; | ||
| flex-wrap: wrap; | ||
| border: 1px solid #334155; | ||
| gap: 0.25rem; | ||
| background: #1a1f35; | ||
| border-radius: 6px; | ||
| padding: 3px; | ||
| } | ||
| .controls button { | ||
| padding: 10px 18px; | ||
| border: 1px solid #475569; | ||
| border-radius: 8px; | ||
| background: linear-gradient(135deg, #334155 0%, #475569 100%); | ||
| color: #f1f5f9; | ||
| font-size: 14px; | ||
| .btn { | ||
| padding: 0.4rem 0.9rem; | ||
| border: none; | ||
| background: transparent; | ||
| color: #8b92a7; | ||
| font-size: 0.85rem; | ||
| cursor: pointer; | ||
| border-radius: 4px; | ||
| transition: all 0.2s; | ||
| font-weight: 500; | ||
| cursor: pointer; | ||
| transition: all 0.2s ease; | ||
| } | ||
| .btn:hover { | ||
| background: #252b42; | ||
| color: #fff; | ||
| } | ||
| .btn.active { | ||
| background: #3b82f6; | ||
| color: #fff; | ||
| } | ||
| .slider-container { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 8px; | ||
| gap: 0.5rem; | ||
| } | ||
| .controls button:hover { | ||
| background: linear-gradient(135deg, #475569 0%, #64748b 100%); | ||
| border-color: #64748b; | ||
| transform: translateY(-1px); | ||
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); | ||
| input[type="range"] { | ||
| width: 100px; | ||
| height: 4px; | ||
| background: #1a1f35; | ||
| border-radius: 2px; | ||
| outline: none; | ||
| -webkit-appearance: none; | ||
| } | ||
| .controls button:active { | ||
| transform: translateY(0); | ||
| input[type="range"]::-webkit-slider-thumb { | ||
| -webkit-appearance: none; | ||
| width: 14px; | ||
| height: 14px; | ||
| background: #3b82f6; | ||
| border-radius: 50%; | ||
| cursor: pointer; | ||
| } | ||
| .controls button::before { | ||
| font-size: 16px; | ||
| input[type="range"]::-moz-range-thumb { | ||
| width: 14px; | ||
| height: 14px; | ||
| background: #3b82f6; | ||
| border-radius: 50%; | ||
| cursor: pointer; | ||
| border: none; | ||
| } | ||
| .controls button.zoom-in::before { content: "🔍"; } | ||
| .controls button.zoom-out::before { content: "🔎"; } | ||
| .controls button.reset::before { content: "↻"; } | ||
| .controls button.export::before { content: "💾"; } | ||
| #graph-container { | ||
| background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); | ||
| border-radius: 12px; | ||
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | ||
| overflow: hidden; | ||
| border: 1px solid #334155; | ||
| .depth-value { | ||
| min-width: 30px; | ||
| text-align: center; | ||
| font-size: 0.85rem; | ||
| color: #3b82f6; | ||
| font-weight: 600; | ||
| } | ||
| /* ========== SEARCH ========== */ | ||
| .search-container { | ||
| position: relative; | ||
| } | ||
| .search-input { | ||
| padding: 0.5rem 1rem; | ||
| background: #1a1f35; | ||
| border: 1px solid #2a3142; | ||
| border-radius: 6px; | ||
| color: #e0e6ed; | ||
| font-size: 0.85rem; | ||
| width: 200px; | ||
| outline: none; | ||
| transition: all 0.2s; | ||
| } | ||
| .search-input:focus { | ||
| border-color: #3b82f6; | ||
| background: #252b42; | ||
| } | ||
| /* ========== MAIN CONTAINER ========== */ | ||
| .container { | ||
| display: flex; | ||
| height: calc(100vh - 80px); | ||
| } | ||
| /* ========== GRAPH AREA ========== */ | ||
| .graph-container { | ||
| flex: 1; | ||
| position: relative; | ||
| overflow: hidden; | ||
| } | ||
| #graph { | ||
| width: 100%; | ||
| height: 800px; | ||
| height: 100%; | ||
| } | ||
| .legend { | ||
| background: #1e293b; | ||
| padding: 20px; | ||
| border-radius: 12px; | ||
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | ||
| margin-top: 20px; | ||
| border: 1px solid #334155; | ||
| /* ========== SIDEBAR ========== */ | ||
| .sidebar { | ||
| width: 300px; | ||
| background: #0f1219; | ||
| border-left: 1px solid #2a3142; | ||
| overflow-y: auto; | ||
| padding: 1.5rem; | ||
| } | ||
| .legend h3 { | ||
| font-size: 16px; | ||
| font-weight: 600; | ||
| margin-bottom: 16px; | ||
| color: #f1f5f9; | ||
| .sidebar h2 { | ||
| font-size: 1rem; | ||
| color: #fff; | ||
| margin-bottom: 1rem; | ||
| padding-bottom: 0.5rem; | ||
| border-bottom: 1px solid #2a3142; | ||
| } | ||
| .stat-grid { | ||
| display: grid; | ||
| gap: 0.75rem; | ||
| margin-bottom: 1.5rem; | ||
| } | ||
| .stat-item { | ||
| background: #1a1f35; | ||
| padding: 0.75rem; | ||
| border-radius: 6px; | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| gap: 8px; | ||
| } | ||
| .legend h3::before { | ||
| content: "📌"; | ||
| font-size: 18px; | ||
| .stat-label { | ||
| font-size: 0.85rem; | ||
| color: #8b92a7; | ||
| } | ||
| .legend-items { | ||
| display: grid; | ||
| grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | ||
| gap: 16px; | ||
| .stat-value { | ||
| font-size: 1.25rem; | ||
| font-weight: 600; | ||
| color: #fff; | ||
| } | ||
| .stat-value.danger { color: #ef4444; } | ||
| .stat-value.warning { color: #f59e0b; } | ||
| .stat-value.success { color: #10b981; } | ||
| /* ========== LEGEND ========== */ | ||
| .legend { | ||
| margin-top: 1rem; | ||
| } | ||
| .legend-item { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 10px; | ||
| font-size: 13px; | ||
| color: #cbd5e1; | ||
| padding: 8px; | ||
| gap: 0.5rem; | ||
| padding: 0.5rem 0; | ||
| font-size: 0.85rem; | ||
| } | ||
| .legend-color { | ||
| width: 16px; | ||
| height: 16px; | ||
| border-radius: 3px; | ||
| } | ||
| /* ========== CONTROL BUTTONS ========== */ | ||
| .control-btn { | ||
| width: 100%; | ||
| padding: 0.6rem 1rem; | ||
| background: #1a1f35; | ||
| border: 1px solid #2a3142; | ||
| border-radius: 6px; | ||
| transition: background 0.2s; | ||
| color: #e0e6ed; | ||
| font-size: 0.85rem; | ||
| cursor: pointer; | ||
| transition: all 0.2s; | ||
| text-align: left; | ||
| font-weight: 500; | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 0.5rem; | ||
| } | ||
| .legend-item:hover { | ||
| background: rgba(51, 65, 85, 0.5); | ||
| .control-btn:hover { | ||
| background: #252b42; | ||
| border-color: #3b82f6; | ||
| color: #fff; | ||
| } | ||
| .legend-color { | ||
| width: 20px; | ||
| height: 20px; | ||
| border-radius: 50%; | ||
| border: 2px solid #0f172a; | ||
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); | ||
| flex-shrink: 0; | ||
| .control-btn:active { | ||
| transform: scale(0.98); | ||
| background: #3b82f6; | ||
| } | ||
| .footer { | ||
| margin-top: 20px; | ||
| padding: 16px; | ||
| text-align: center; | ||
| color: #64748b; | ||
| font-size: 13px; | ||
| background: #1e293b; | ||
| border-radius: 12px; | ||
| border: 1px solid #334155; | ||
| /* ========== TOOLTIP ========== */ | ||
| .tooltip { | ||
| position: absolute; | ||
| background: rgba(15, 18, 25, 0.95); | ||
| border: 1px solid #3b82f6; | ||
| border-radius: 8px; | ||
| padding: 1rem; | ||
| pointer-events: none; | ||
| opacity: 0; | ||
| transition: opacity 0.2s; | ||
| max-width: 350px; | ||
| z-index: 10000; | ||
| backdrop-filter: blur(10px); | ||
| } | ||
| .footer a { | ||
| color: #60a5fa; | ||
| text-decoration: none; | ||
| .tooltip.visible { | ||
| opacity: 1; | ||
| } | ||
| .tooltip-title { | ||
| font-size: 1rem; | ||
| font-weight: 600; | ||
| color: #fff; | ||
| margin-bottom: 0.5rem; | ||
| } | ||
| .tooltip-content { | ||
| font-size: 0.85rem; | ||
| color: #8b92a7; | ||
| line-height: 1.5; | ||
| } | ||
| .tooltip-badge { | ||
| display: inline-block; | ||
| padding: 0.15rem 0.5rem; | ||
| background: #ef4444; | ||
| color: #fff; | ||
| font-size: 0.75rem; | ||
| border-radius: 4px; | ||
| margin-right: 0.25rem; | ||
| font-weight: 500; | ||
| } | ||
| .footer a:hover { | ||
| color: #93c5fd; | ||
| text-decoration: underline; | ||
| } | ||
| /* Node and link styles */ | ||
| .tooltip-badge.warning { background: #f59e0b; } | ||
| .tooltip-badge.info { background: #3b82f6; } | ||
| /* ========== GRAPH STYLES ========== */ | ||
| .node { | ||
| cursor: pointer; | ||
| stroke: #1e293b; | ||
| stroke-width: 2px; | ||
| transition: all 0.3s ease; | ||
| transition: all 0.2s; | ||
| } | ||
| .node:hover { | ||
| stroke: #f1f5f9; | ||
| stroke-width: 3px; | ||
| filter: brightness(1.2); | ||
| filter: brightness(1.3); | ||
| } | ||
| .link { | ||
| stroke: #475569; | ||
| stroke-opacity: 0.4; | ||
| stroke-width: 1.5px; | ||
| fill: none; | ||
| .node-circle { | ||
| stroke-width: 2px; | ||
| transition: all 0.2s; | ||
| } | ||
| .node-label { | ||
| font-size: 11px; | ||
| fill: #cbd5e1; | ||
| fill: #e0e6ed; | ||
| pointer-events: none; | ||
| text-anchor: middle; | ||
| pointer-events: none; | ||
| user-select: none; | ||
| font-weight: 500; | ||
| } | ||
| /* Tooltip */ | ||
| .tooltip { | ||
| position: absolute; | ||
| padding: 12px 16px; | ||
| background: rgba(15, 23, 42, 0.95); | ||
| border: 1px solid #475569; | ||
| border-radius: 8px; | ||
| pointer-events: none; | ||
| font-size: 13px; | ||
| max-width: 320px; | ||
| box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4); | ||
| z-index: 1000; | ||
| backdrop-filter: blur(8px); | ||
| .link { | ||
| stroke: #2a3142; | ||
| stroke-opacity: 0.6; | ||
| fill: none; | ||
| stroke-width: 1.5px; | ||
| } | ||
| .tooltip-title { | ||
| font-weight: 600; | ||
| margin-bottom: 10px; | ||
| color: #60a5fa; | ||
| font-size: 15px; | ||
| border-bottom: 1px solid #334155; | ||
| padding-bottom: 8px; | ||
| .link.circular { | ||
| stroke: #ef4444; | ||
| stroke-dasharray: 5, 5; | ||
| stroke-opacity: 0.8; | ||
| } | ||
| .tooltip-row { | ||
| margin: 6px 0; | ||
| display: flex; | ||
| justify-content: space-between; | ||
| gap: 16px; | ||
| } | ||
| .tooltip-label { | ||
| color: #94a3b8; | ||
| font-weight: 500; | ||
| } | ||
| .tooltip-value { | ||
| color: #e2e8f0; | ||
| font-weight: 600; | ||
| } | ||
| /* Loading spinner */ | ||
| /* ========== LOADING ========== */ | ||
| .loading { | ||
@@ -281,194 +368,717 @@ position: absolute; | ||
| transform: translate(-50%, -50%); | ||
| color: #60a5fa; | ||
| font-size: 18px; | ||
| font-weight: 600; | ||
| text-align: center; | ||
| display: none; | ||
| } | ||
| .loading::after { | ||
| content: "..."; | ||
| animation: dots 1.5s steps(4, end) infinite; | ||
| .loading.visible { | ||
| display: block; | ||
| } | ||
| @keyframes dots { | ||
| 0%, 20% { content: "."; } | ||
| 40% { content: ".."; } | ||
| 60%, 100% { content: "..."; } | ||
| .spinner { | ||
| width: 50px; | ||
| height: 50px; | ||
| border: 3px solid #2a3142; | ||
| border-top-color: #3b82f6; | ||
| border-radius: 50%; | ||
| animation: spin 1s linear infinite; | ||
| margin: 0 auto 1rem; | ||
| } | ||
| /* Responsive */ | ||
| @media (max-width: 768px) { | ||
| body { | ||
| padding: 12px; | ||
| @keyframes spin { | ||
| to { transform: rotate(360deg); } | ||
| } | ||
| /* ========== RESPONSIVE ========== */ | ||
| @media (max-width: 1024px) { | ||
| .sidebar { | ||
| width: 250px; | ||
| } | ||
| h1 { | ||
| font-size: 22px; | ||
| .controls { | ||
| gap: 1rem; | ||
| } | ||
| .metadata { | ||
| } | ||
| @media (max-width: 768px) { | ||
| .header { | ||
| flex-direction: column; | ||
| gap: 8px; | ||
| height: auto; | ||
| gap: 1rem; | ||
| } | ||
| .controls { | ||
| justify-content: center; | ||
| width: 100%; | ||
| justify-content: flex-start; | ||
| } | ||
| .legend-items { | ||
| grid-template-columns: 1fr; | ||
| .sidebar { | ||
| display: none; | ||
| } | ||
| #graph { | ||
| height: 600px; | ||
| } | ||
| } | ||
| /* Scrollbar */ | ||
| ::-webkit-scrollbar { | ||
| width: 8px; | ||
| height: 8px; | ||
| } | ||
| ::-webkit-scrollbar-track { | ||
| background: #0f172a; | ||
| border-radius: 4px; | ||
| } | ||
| ::-webkit-scrollbar-thumb { | ||
| background: #475569; | ||
| border-radius: 4px; | ||
| } | ||
| ::-webkit-scrollbar-thumb:hover { | ||
| background: #64748b; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <!-- Header --> | ||
| <div class="header"> | ||
| <h1>{{TITLE}}</h1> | ||
| <div class="metadata"> | ||
| <div class="metadata-item"> | ||
| <span class="metadata-label">Project:</span> | ||
| <span>{{PROJECT_NAME}}</span> | ||
| <h1> | ||
| <span class="icon">📊</span> | ||
| DevCompass - Dependency Graph | ||
| </h1> | ||
| <!-- Controls --> | ||
| <div class="controls"> | ||
| <!-- Layout Switcher --> | ||
| <div class="control-group"> | ||
| <span class="control-label">Layout:</span> | ||
| <div class="btn-group"> | ||
| <button class="btn active" data-layout="tree">Tree</button> | ||
| <button class="btn" data-layout="force">Force</button> | ||
| <button class="btn" data-layout="radial">Radial</button> | ||
| <button class="btn" data-layout="conflict">Conflict</button> | ||
| </div> | ||
| </div> | ||
| <div class="metadata-item"> | ||
| <span class="metadata-label">Version:</span> | ||
| <span>{{PROJECT_VERSION}}</span> | ||
| <!-- Filter Switcher --> | ||
| <div class="control-group"> | ||
| <span class="control-label">Filter:</span> | ||
| <div class="btn-group"> | ||
| <button class="btn active" data-filter="all">All</button> | ||
| <button class="btn" data-filter="vulnerable">Vulnerable</button> | ||
| <button class="btn" data-filter="outdated">Outdated</button> | ||
| <button class="btn" data-filter="deprecated">Deprecated</button> | ||
| <button class="btn" data-filter="unused">Unused</button> | ||
| </div> | ||
| </div> | ||
| <div class="metadata-item"> | ||
| <span class="metadata-label">Dependencies:</span> | ||
| <span>{{TOTAL_DEPS}}</span> | ||
| <!-- Depth Slider --> | ||
| <div class="control-group slider-container"> | ||
| <span class="control-label">Depth:</span> | ||
| <input type="range" id="depthSlider" min="1" max="10" value="10"> | ||
| <span class="depth-value" id="depthValue">∞</span> | ||
| </div> | ||
| <div class="metadata-item"> | ||
| <span class="metadata-label">Max Depth:</span> | ||
| <span>{{MAX_DEPTH}}</span> | ||
| <!-- Search --> | ||
| <div class="search-container"> | ||
| <input type="text" class="search-input" placeholder="Search packages..." id="searchInput"> | ||
| </div> | ||
| <div class="metadata-item"> | ||
| <span class="metadata-label">Generated:</span> | ||
| <span>{{GENERATED_AT}}</span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div class="controls"> | ||
| <button id="zoom-in" class="zoom-in">Zoom In</button> | ||
| <button id="zoom-out" class="zoom-out">Zoom Out</button> | ||
| <button id="reset-zoom" class="reset">Reset View</button> | ||
| <button id="export-svg" class="export">Export SVG</button> | ||
| </div> | ||
| {{SEARCH_FILTER_HTML}} | ||
| <div id="graph-container"> | ||
| <div id="graph"> | ||
| <div class="loading">Loading graph</div> | ||
| <!-- Main Container --> | ||
| <div class="container"> | ||
| <!-- Graph --> | ||
| <div class="graph-container"> | ||
| <svg id="graph"></svg> | ||
| <div class="loading" id="loading"> | ||
| <div class="spinner"></div> | ||
| <div>Rendering graph...</div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div class="legend"> | ||
| <h3>Health Score Legend</h3> | ||
| <div class="legend-items"> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #10b981;"></div> | ||
| <span>Excellent (9-10)</span> | ||
| <!-- Sidebar --> | ||
| <div class="sidebar"> | ||
| <h2>Statistics</h2> | ||
| <div class="stat-grid" id="stats"> | ||
| <!-- Populated by JS --> | ||
| </div> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #84cc16;"></div> | ||
| <span>Good (7-8)</span> | ||
| <h2>Legend</h2> | ||
| <div class="legend"> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #10b981;"></div> | ||
| <span>Healthy</span> | ||
| </div> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #f59e0b;"></div> | ||
| <span>Outdated</span> | ||
| </div> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #ef4444;"></div> | ||
| <span>Vulnerable</span> | ||
| </div> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #8b5cf6;"></div> | ||
| <span>Deprecated</span> | ||
| </div> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #6b7280;"></div> | ||
| <span>Unused</span> | ||
| </div> | ||
| </div> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #eab308;"></div> | ||
| <span>Fair (5-6)</span> | ||
| <h2>Controls</h2> | ||
| <div style="display: flex; flex-direction: column; gap: 0.5rem;"> | ||
| <button class="control-btn" onclick="fitToScreen()">⛶ Fit to Screen</button> | ||
| <button class="control-btn" onclick="zoomIn()">🔍+ Zoom In</button> | ||
| <button class="control-btn" onclick="zoomOut()">🔍− Zoom Out</button> | ||
| <button class="control-btn" onclick="resetZoom()">⟲ Reset Zoom</button> | ||
| <button class="control-btn" onclick="centerGraph()">⊙ Center View</button> | ||
| <hr style="border: none; border-top: 1px solid #2a3142; margin: 0.5rem 0;"> | ||
| <button class="control-btn" onclick="exportPNG()">📸 Save as PNG</button> | ||
| <button class="control-btn" onclick="exportJSON()">💾 Save as JSON</button> | ||
| <button class="control-btn" onclick="toggleFullscreen()">🖵 Fullscreen</button> | ||
| </div> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #f97316;"></div> | ||
| <span>Poor (3-4)</span> | ||
| </div> | ||
| <div class="legend-item"> | ||
| <div class="legend-color" style="background: #ef4444;"></div> | ||
| <span>Critical (0-2)</span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div class="footer"> | ||
| Generated by <a href="https://github.com/AjayBThorat-20/devcompass" target="_blank">DevCompass</a> v3.1.0 | ||
| · <a href="https://www.npmjs.com/package/devcompass" target="_blank">npm</a> | ||
| </div> | ||
| <!-- Tooltip --> | ||
| <div class="tooltip" id="tooltip"></div> | ||
| <script> | ||
| // Remove loading indicator | ||
| setTimeout(() => { | ||
| const loading = document.querySelector('.loading'); | ||
| if (loading) loading.remove(); | ||
| }, 100); | ||
| {{GRAPH_SCRIPT}} | ||
| // Zoom controls | ||
| document.getElementById('zoom-in').addEventListener('click', function() { | ||
| if (typeof zoom !== 'undefined') { | ||
| d3.select('#graph svg').transition().duration(300).call(zoom.scaleBy, 1.3); | ||
| } | ||
| // Graph data will be injected here | ||
| // @ts-nocheck | ||
| const graphData = {{GRAPH_DATA}}; | ||
| const metadata = graphData.metadata || {}; | ||
| // State | ||
| let currentLayout = metadata.defaultLayout || 'tree'; | ||
| let currentFilter = metadata.defaultFilter || 'all'; | ||
| let currentDepth = metadata.defaultDepth || 10; | ||
| let searchTerm = ''; | ||
| let currentZoom = null; // Store zoom behavior | ||
| let currentSvg = null; // Store current SVG | ||
| let currentG = null; // Store current group | ||
| // Initialize | ||
| document.addEventListener('DOMContentLoaded', () => { | ||
| initializeControls(); | ||
| renderGraph(); | ||
| updateStats(); | ||
| }); | ||
| document.getElementById('zoom-out').addEventListener('click', function() { | ||
| if (typeof zoom !== 'undefined') { | ||
| d3.select('#graph svg').transition().duration(300).call(zoom.scaleBy, 0.7); | ||
| // Control handlers | ||
| function initializeControls() { | ||
| // Layout buttons | ||
| document.querySelectorAll('[data-layout]').forEach(btn => { | ||
| btn.addEventListener('click', () => { | ||
| document.querySelectorAll('[data-layout]').forEach(b => b.classList.remove('active')); | ||
| btn.classList.add('active'); | ||
| currentLayout = btn.dataset.layout; | ||
| renderGraph(); | ||
| }); | ||
| }); | ||
| // Filter buttons | ||
| document.querySelectorAll('[data-filter]').forEach(btn => { | ||
| btn.addEventListener('click', () => { | ||
| document.querySelectorAll('[data-filter]').forEach(b => b.classList.remove('active')); | ||
| btn.classList.add('active'); | ||
| currentFilter = btn.dataset.filter; | ||
| renderGraph(); | ||
| }); | ||
| }); | ||
| // Depth slider | ||
| const slider = document.getElementById('depthSlider'); | ||
| const value = document.getElementById('depthValue'); | ||
| slider.addEventListener('input', (e) => { | ||
| currentDepth = parseInt(e.target.value); | ||
| value.textContent = currentDepth === 10 ? '∞' : currentDepth; | ||
| renderGraph(); | ||
| }); | ||
| // Search | ||
| document.getElementById('searchInput').addEventListener('input', (e) => { | ||
| searchTerm = e.target.value.toLowerCase(); | ||
| renderGraph(); | ||
| }); | ||
| } | ||
| // Filter nodes | ||
| function filterNodes(nodes) { | ||
| let filtered = nodes; | ||
| // Apply depth filter | ||
| const maxDepth = currentDepth === 10 ? Infinity : currentDepth; | ||
| filtered = filtered.filter(n => (n.depth || 0) <= maxDepth); | ||
| // Apply category filter | ||
| if (currentFilter !== 'all') { | ||
| filtered = filtered.filter(n => { | ||
| if (n.type === 'root') return true; | ||
| switch(currentFilter) { | ||
| case 'vulnerable': return n.isVulnerable; | ||
| case 'outdated': return n.isOutdated; | ||
| case 'deprecated': return n.isDeprecated; | ||
| case 'unused': return n.isUnused; | ||
| case 'conflict': return n.isVulnerable || n.isDeprecated || n.isOutdated; | ||
| default: return true; | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| // Apply search filter | ||
| if (searchTerm) { | ||
| filtered = filtered.filter(n => | ||
| n.name.toLowerCase().includes(searchTerm) | ||
| ); | ||
| } | ||
| return filtered; | ||
| } | ||
| // Render graph | ||
| function renderGraph() { | ||
| const loading = document.getElementById('loading'); | ||
| loading.classList.add('visible'); | ||
| setTimeout(() => { | ||
| const svg = d3.select('#graph'); | ||
| svg.selectAll('*').remove(); | ||
| const filteredNodes = filterNodes(graphData.nodes); | ||
| const nodeIds = new Set(filteredNodes.map(n => n.id)); | ||
| const filteredLinks = graphData.links.filter(l => { | ||
| const sourceId = typeof l.source === 'object' ? l.source.id : l.source; | ||
| const targetId = typeof l.target === 'object' ? l.target.id : l.target; | ||
| return nodeIds.has(sourceId) && nodeIds.has(targetId); | ||
| }); | ||
| // Render based on layout | ||
| switch(currentLayout) { | ||
| case 'tree': renderTreeLayout(svg, filteredNodes, filteredLinks); break; | ||
| case 'force': renderForceLayout(svg, filteredNodes, filteredLinks); break; | ||
| case 'radial': renderRadialLayout(svg, filteredNodes, filteredLinks); break; | ||
| case 'conflict': renderConflictLayout(svg, filteredNodes, filteredLinks); break; | ||
| } | ||
| loading.classList.remove('visible'); | ||
| updateStats(); | ||
| }, 100); | ||
| } | ||
| // Layout implementations | ||
| function renderTreeLayout(svg, nodes, links) { | ||
| const width = window.innerWidth - 300; | ||
| const height = window.innerHeight - 80; | ||
| svg.attr('width', width).attr('height', height); | ||
| const g = svg.append('g'); | ||
| const root = buildHierarchy(nodes, links); | ||
| if (!root) return; | ||
| // Increase spacing between nodes | ||
| const tree = d3.tree() | ||
| .size([width - 200, height - 200]) | ||
| .separation((a, b) => (a.parent === b.parent ? 1.5 : 2)); | ||
| tree(root); | ||
| // Links | ||
| g.selectAll('.link') | ||
| .data(root.links()) | ||
| .enter().append('path') | ||
| .attr('class', 'link') | ||
| .attr('d', d3.linkVertical() | ||
| .x(d => d.x + 100) | ||
| .y(d => d.y + 100)); | ||
| // Nodes | ||
| const node = g.selectAll('.node') | ||
| .data(root.descendants()) | ||
| .enter().append('g') | ||
| .attr('class', 'node') | ||
| .attr('transform', d => `translate(${d.x + 100},${d.y + 100})`); | ||
| node.append('circle') | ||
| .attr('class', 'node-circle') | ||
| .attr('r', 6) | ||
| .attr('fill', d => getNodeColor(d.data)) | ||
| .attr('stroke', d => getNodeStroke(d.data)) | ||
| .on('mouseover', showTooltip) | ||
| .on('mouseout', hideTooltip); | ||
| // Improved label positioning to prevent overlap | ||
| node.append('text') | ||
| .attr('class', 'node-label') | ||
| .attr('dy', d => d.children ? -12 : 15) // Above if has children, below if leaf | ||
| .attr('text-anchor', 'middle') | ||
| .text(d => { | ||
| // Truncate long names | ||
| const name = d.data.name; | ||
| return name.length > 15 ? name.substring(0, 15) + '...' : name; | ||
| }) | ||
| .style('font-size', '10px') | ||
| .style('pointer-events', 'none'); | ||
| addZoom(svg, g); | ||
| // Auto-fit after rendering | ||
| setTimeout(() => centerGraph(), 100); | ||
| } | ||
| function renderForceLayout(svg, nodes, links) { | ||
| const width = window.innerWidth - 300; | ||
| const height = window.innerHeight - 80; | ||
| svg.attr('width', width).attr('height', height); | ||
| const g = svg.append('g'); | ||
| const simulation = d3.forceSimulation(nodes) | ||
| .force('link', d3.forceLink(links).id(d => d.id).distance(100)) | ||
| .force('charge', d3.forceManyBody().strength(-300)) | ||
| .force('center', d3.forceCenter(width / 2, height / 2)); | ||
| const link = g.selectAll('.link') | ||
| .data(links) | ||
| .enter().append('line') | ||
| .attr('class', 'link'); | ||
| const node = g.selectAll('.node') | ||
| .data(nodes) | ||
| .enter().append('g') | ||
| .attr('class', 'node') | ||
| .call(d3.drag() | ||
| .on('start', dragStart) | ||
| .on('drag', dragging) | ||
| .on('end', dragEnd)); | ||
| node.append('circle') | ||
| .attr('class', 'node-circle') | ||
| .attr('r', 8) | ||
| .attr('fill', getNodeColor) | ||
| .attr('stroke', getNodeStroke) | ||
| .on('mouseover', showTooltip) | ||
| .on('mouseout', hideTooltip); | ||
| node.append('text') | ||
| .attr('class', 'node-label') | ||
| .attr('dy', -12) | ||
| .text(d => d.name); | ||
| simulation.on('tick', () => { | ||
| link | ||
| .attr('x1', d => d.source.x) | ||
| .attr('y1', d => d.source.y) | ||
| .attr('x2', d => d.target.x) | ||
| .attr('y2', d => d.target.y); | ||
| node.attr('transform', d => `translate(${d.x},${d.y})`); | ||
| }); | ||
| addZoom(svg, g); | ||
| function dragStart(event) { | ||
| if (!event.active) simulation.alphaTarget(0.3).restart(); | ||
| event.subject.fx = event.subject.x; | ||
| event.subject.fy = event.subject.y; | ||
| } | ||
| function dragging(event) { | ||
| event.subject.fx = event.x; | ||
| event.subject.fy = event.y; | ||
| } | ||
| function dragEnd(event) { | ||
| if (!event.active) simulation.alphaTarget(0); | ||
| event.subject.fx = null; | ||
| event.subject.fy = null; | ||
| } | ||
| } | ||
| function renderRadialLayout(svg, nodes, links) { | ||
| const width = window.innerWidth - 300; | ||
| const height = window.innerHeight - 80; | ||
| const radius = Math.min(width, height) / 2 - 100; | ||
| svg.attr('width', width).attr('height', height); | ||
| const g = svg.append('g').attr('transform', `translate(${width/2},${height/2})`); | ||
| const root = buildHierarchy(nodes, links); | ||
| if (!root) return; | ||
| const tree = d3.cluster().size([2 * Math.PI, radius]); | ||
| tree(root); | ||
| // Links | ||
| g.selectAll('.link') | ||
| .data(root.links()) | ||
| .enter().append('path') | ||
| .attr('class', 'link') | ||
| .attr('d', d3.linkRadial() | ||
| .angle(d => d.x) | ||
| .radius(d => d.y)); | ||
| // Nodes | ||
| const node = g.selectAll('.node') | ||
| .data(root.descendants()) | ||
| .enter().append('g') | ||
| .attr('class', 'node') | ||
| .attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`); | ||
| node.append('circle') | ||
| .attr('class', 'node-circle') | ||
| .attr('r', 6) | ||
| .attr('fill', d => getNodeColor(d.data)) | ||
| .attr('stroke', d => getNodeStroke(d.data)) | ||
| .on('mouseover', showTooltip) | ||
| .on('mouseout', hideTooltip); | ||
| node.append('text') | ||
| .attr('class', 'node-label') | ||
| .attr('dy', -10) | ||
| .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null) | ||
| .text(d => d.data.name); | ||
| addZoom(svg, g); | ||
| } | ||
| function renderConflictLayout(svg, nodes, links) { | ||
| const conflictNodes = nodes.filter(n => | ||
| n.type === 'root' || n.isVulnerable || n.isDeprecated || n.isOutdated | ||
| ); | ||
| const nodeIds = new Set(conflictNodes.map(n => n.id)); | ||
| const conflictLinks = links.filter(l => | ||
| nodeIds.has(typeof l.source === 'object' ? l.source.id : l.source) && | ||
| nodeIds.has(typeof l.target === 'object' ? l.target.id : l.target) | ||
| ); | ||
| renderForceLayout(svg, conflictNodes, conflictLinks); | ||
| } | ||
| // Utilities | ||
| function buildHierarchy(nodes, links) { | ||
| const root = nodes.find(n => n.type === 'root'); | ||
| if (!root) return null; | ||
| try { | ||
| return d3.stratify() | ||
| .id(d => d.id) | ||
| .parentId(d => { | ||
| const link = links.find(l => { | ||
| const targetId = typeof l.target === 'object' ? l.target.id : l.target; | ||
| return targetId === d.id; | ||
| }); | ||
| return link ? (typeof link.source === 'object' ? link.source.id : link.source) : null; | ||
| })(nodes); | ||
| } catch (e) { | ||
| console.error('Hierarchy build failed:', e); | ||
| return null; | ||
| } | ||
| } | ||
| function getNodeColor(node) { | ||
| if (node.type === 'root') return '#3b82f6'; | ||
| if (node.isVulnerable) return '#ef4444'; | ||
| if (node.isDeprecated) return '#8b5cf6'; | ||
| if (node.isOutdated) return '#f59e0b'; | ||
| if (node.isUnused) return '#6b7280'; | ||
| return '#10b981'; | ||
| } | ||
| function getNodeStroke(node) { | ||
| if (node.type === 'root') return '#2563eb'; | ||
| return '#1a1f35'; | ||
| } | ||
| function showTooltip(event, d) { | ||
| const data = d.data || d; | ||
| const tooltip = document.getElementById('tooltip'); | ||
| let content = `<div class="tooltip-title">${data.name}@${data.version || 'unknown'}</div>`; | ||
| content += `<div class="tooltip-content">`; | ||
| if (data.isVulnerable) content += `<span class="tooltip-badge">Vulnerable</span>`; | ||
| if (data.isDeprecated) content += `<span class="tooltip-badge">Deprecated</span>`; | ||
| if (data.isOutdated) content += `<span class="tooltip-badge warning">Outdated</span>`; | ||
| if (data.isUnused) content += `<span class="tooltip-badge info">Unused</span>`; | ||
| content += `<br>Depth: ${data.depth || 0}`; | ||
| if (data.healthScore) content += `<br>Health: ${data.healthScore}/10`; | ||
| content += `</div>`; | ||
| tooltip.innerHTML = content; | ||
| tooltip.style.left = (event.pageX + 10) + 'px'; | ||
| tooltip.style.top = (event.pageY + 10) + 'px'; | ||
| tooltip.classList.add('visible'); | ||
| } | ||
| function hideTooltip() { | ||
| document.getElementById('tooltip').classList.remove('visible'); | ||
| } | ||
| function addZoom(svg, g) { | ||
| const zoom = d3.zoom() | ||
| .scaleExtent([0.1, 4]) | ||
| .on('zoom', (event) => g.attr('transform', event.transform)); | ||
| svg.call(zoom); | ||
| // Store references for external controls | ||
| currentZoom = zoom; | ||
| currentSvg = svg; | ||
| currentG = g; | ||
| } | ||
| // ========== CONTROL FUNCTIONS ========== | ||
| document.getElementById('reset-zoom').addEventListener('click', function() { | ||
| if (typeof zoom !== 'undefined') { | ||
| d3.select('#graph svg').transition().duration(500).call(zoom.transform, d3.zoomIdentity); | ||
| function zoomIn() { | ||
| if (currentSvg && currentZoom) { | ||
| currentSvg.transition().duration(300).call(currentZoom.scaleBy, 1.3); | ||
| } | ||
| }); | ||
| // SVG export | ||
| document.getElementById('export-svg').addEventListener('click', function() { | ||
| } | ||
| function zoomOut() { | ||
| if (currentSvg && currentZoom) { | ||
| currentSvg.transition().duration(300).call(currentZoom.scaleBy, 0.7); | ||
| } | ||
| } | ||
| function resetZoom() { | ||
| if (currentSvg && currentZoom) { | ||
| currentSvg.transition().duration(500).call(currentZoom.transform, d3.zoomIdentity); | ||
| } | ||
| } | ||
| function centerGraph() { | ||
| if (!currentSvg || !currentZoom || !currentG) return; | ||
| try { | ||
| const svgElement = document.querySelector('#graph svg'); | ||
| if (!svgElement) { | ||
| alert('No graph to export. Please wait for the graph to load.'); | ||
| return; | ||
| } | ||
| // Get the bounding box of all graph elements | ||
| const bbox = currentG.node().getBBox(); | ||
| const svgData = svgElement.outerHTML; | ||
| const blob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'}); | ||
| const url = URL.createObjectURL(blob); | ||
| const link = document.createElement('a'); | ||
| link.href = url; | ||
| link.download = 'dependency-graph-' + new Date().getTime() + '.svg'; | ||
| document.body.appendChild(link); | ||
| link.click(); | ||
| document.body.removeChild(link); | ||
| URL.revokeObjectURL(url); | ||
| // Get container dimensions | ||
| const containerWidth = currentSvg.node().clientWidth; | ||
| const containerHeight = currentSvg.node().clientHeight; | ||
| // Calculate scale to fit content with padding | ||
| const padding = 50; | ||
| const scaleX = (containerWidth - padding * 2) / bbox.width; | ||
| const scaleY = (containerHeight - padding * 2) / bbox.height; | ||
| const scale = Math.min(scaleX, scaleY, 1); // Don't zoom in beyond 1x | ||
| // Calculate translation to center the content | ||
| const tx = (containerWidth - bbox.width * scale) / 2 - bbox.x * scale; | ||
| const ty = (containerHeight - bbox.height * scale) / 2 - bbox.y * scale; | ||
| // Apply transform | ||
| currentSvg.transition().duration(750).call( | ||
| currentZoom.transform, | ||
| d3.zoomIdentity.translate(tx, ty).scale(scale) | ||
| ); | ||
| } catch (error) { | ||
| console.error('Export failed:', error); | ||
| alert('Failed to export SVG. See console for details.'); | ||
| // Fallback to simple center | ||
| const width = currentSvg.node().clientWidth; | ||
| const height = currentSvg.node().clientHeight; | ||
| currentSvg.transition().duration(500).call( | ||
| currentZoom.transform, | ||
| d3.zoomIdentity.translate(width / 2, height / 2).scale(1) | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
| function fitToScreen() { | ||
| centerGraph(); // Uses the same logic | ||
| } | ||
| function exportPNG() { | ||
| try { | ||
| const svgElement = document.getElementById('graph'); | ||
| const svgData = new XMLSerializer().serializeToString(svgElement); | ||
| const canvas = document.createElement('canvas'); | ||
| const ctx = canvas.getContext('2d'); | ||
| const img = new Image(); | ||
| canvas.width = svgElement.clientWidth; | ||
| canvas.height = svgElement.clientHeight; | ||
| img.onload = function() { | ||
| ctx.fillStyle = '#0a0e1a'; | ||
| ctx.fillRect(0, 0, canvas.width, canvas.height); | ||
| ctx.drawImage(img, 0, 0); | ||
| canvas.toBlob(function(blob) { | ||
| const url = URL.createObjectURL(blob); | ||
| const a = document.createElement('a'); | ||
| a.href = url; | ||
| a.download = 'dependency-graph-' + currentLayout + '-' + currentFilter + '.png'; | ||
| a.click(); | ||
| URL.revokeObjectURL(url); | ||
| }); | ||
| }; | ||
| img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))); | ||
| } catch (error) { | ||
| alert('PNG export requires the graph to be fully loaded. Please try again.'); | ||
| } | ||
| } | ||
| function exportJSON() { | ||
| const filtered = filterNodes(graphData.nodes); | ||
| const nodeIds = new Set(filtered.map(n => n.id)); | ||
| const filteredLinks = graphData.links.filter(l => { | ||
| const sourceId = typeof l.source === 'object' ? l.source.id : l.source; | ||
| const targetId = typeof l.target === 'object' ? l.target.id : l.target; | ||
| return nodeIds.has(sourceId) && nodeIds.has(targetId); | ||
| }); | ||
| const exportData = { | ||
| layout: currentLayout, | ||
| filter: currentFilter, | ||
| depth: currentDepth, | ||
| nodes: filtered, | ||
| links: filteredLinks, | ||
| metadata: graphData.metadata | ||
| }; | ||
| const dataStr = JSON.stringify(exportData, null, 2); | ||
| const dataBlob = new Blob([dataStr], { type: 'application/json' }); | ||
| const url = URL.createObjectURL(dataBlob); | ||
| const a = document.createElement('a'); | ||
| a.href = url; | ||
| a.download = 'dependency-graph-' + currentLayout + '-' + currentFilter + '.json'; | ||
| a.click(); | ||
| URL.revokeObjectURL(url); | ||
| } | ||
| function toggleFullscreen() { | ||
| if (!document.fullscreenElement) { | ||
| document.documentElement.requestFullscreen().catch(err => { | ||
| alert('Fullscreen not supported on this browser'); | ||
| }); | ||
| } else { | ||
| document.exitFullscreen(); | ||
| } | ||
| } | ||
| function updateStats() { | ||
| const filtered = filterNodes(graphData.nodes); | ||
| const stats = { | ||
| total: filtered.length, | ||
| vulnerable: filtered.filter(n => n.isVulnerable).length, | ||
| deprecated: filtered.filter(n => n.isDeprecated).length, | ||
| outdated: filtered.filter(n => n.isOutdated).length, | ||
| unused: filtered.filter(n => n.isUnused).length, | ||
| healthy: filtered.filter(n => !n.isVulnerable && !n.isDeprecated && !n.isOutdated && !n.isUnused).length | ||
| }; | ||
| document.getElementById('stats').innerHTML = ` | ||
| <div class="stat-item"> | ||
| <span class="stat-label">Total</span> | ||
| <span class="stat-value">${stats.total}</span> | ||
| </div> | ||
| <div class="stat-item"> | ||
| <span class="stat-label">Vulnerable</span> | ||
| <span class="stat-value danger">${stats.vulnerable}</span> | ||
| </div> | ||
| <div class="stat-item"> | ||
| <span class="stat-label">Deprecated</span> | ||
| <span class="stat-value warning">${stats.deprecated}</span> | ||
| </div> | ||
| <div class="stat-item"> | ||
| <span class="stat-label">Outdated</span> | ||
| <span class="stat-value warning">${stats.outdated}</span> | ||
| </div> | ||
| <div class="stat-item"> | ||
| <span class="stat-label">Unused</span> | ||
| <span class="stat-value">${stats.unused}</span> | ||
| </div> | ||
| <div class="stat-item"> | ||
| <span class="stat-label">Healthy</span> | ||
| <span class="stat-value success">${stats.healthy}</span> | ||
| </div> | ||
| `; | ||
| } | ||
| </script> | ||
| </body> | ||
| </html> |
| // src/utils/license-conflict-fixer.js | ||
| // v3.1.4 - License conflict fixer with dynamic alternatives | ||
| const { execSync } = require('child_process'); | ||
| const chalk = require('chalk'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const { analyzer } = require('../services'); | ||
| class LicenseConflictFixer { | ||
| constructor() { | ||
| this.fixesApplied = []; | ||
| this.fixesSkipped = []; | ||
| this.fixes = []; | ||
| this.skipped = []; | ||
| this.errors = []; | ||
| this.alternatives = this.loadAlternatives(); | ||
| } | ||
| /** | ||
| * Load package alternatives database | ||
| * Fix a license conflict warning | ||
| */ | ||
| loadAlternatives() { | ||
| async fixWarning(warning, dryRun = false) { | ||
| const packageName = warning.package; | ||
| try { | ||
| const alternativesPath = path.join(__dirname, '../../data/package-alternatives.json'); | ||
| if (fs.existsSync(alternativesPath)) { | ||
| return JSON.parse(fs.readFileSync(alternativesPath, 'utf8')); | ||
| } | ||
| return null; | ||
| } catch (error) { | ||
| console.error('Warning: Could not load package alternatives database'); | ||
| return null; | ||
| } | ||
| } | ||
| async fixWarning(warning, projectPath, report, progress, skipConfirmation = false) { | ||
| try { | ||
| switch (warning.autoFixAction) { | ||
| case 'replace': | ||
| return await this.replacePackage(warning, projectPath, report, progress, skipConfirmation); | ||
| case 'review': | ||
| // Requires manual review | ||
| this.fixesSkipped.push({ | ||
| package: warning.package, | ||
| reason: 'Requires manual legal review', | ||
| warning: warning | ||
| // Get dynamic alternative from license analyzer | ||
| const result = await analyzer.license.analyzePackage(packageName); | ||
| if (result.alternative) { | ||
| if (!dryRun) { | ||
| // Uninstall problematic package | ||
| try { | ||
| execSync(`npm uninstall ${packageName}`, { | ||
| stdio: 'pipe', | ||
| cwd: process.cwd() | ||
| }); | ||
| } catch (error) { | ||
| // Ignore uninstall errors | ||
| } | ||
| // Install alternative | ||
| execSync(`npm install ${result.alternative}`, { | ||
| stdio: 'pipe', | ||
| cwd: process.cwd() | ||
| }); | ||
| report.addSkipped( | ||
| warning.package, | ||
| 'License conflict - requires manual legal review' | ||
| ); | ||
| return false; | ||
| } | ||
| default: | ||
| this.fixesSkipped.push({ | ||
| package: warning.package, | ||
| reason: 'No auto-fix available', | ||
| warning: warning | ||
| }); | ||
| return false; | ||
| this.fixes.push({ | ||
| package: packageName, | ||
| action: 'replaced', | ||
| replacement: result.alternative, | ||
| oldLicense: warning.license, | ||
| newLicense: 'MIT', // Most alternatives are MIT | ||
| severity: warning.severity | ||
| }); | ||
| return { | ||
| success: true, | ||
| action: 'replaced', | ||
| metadata: { | ||
| from: packageName, | ||
| to: result.alternative, | ||
| oldLicense: warning.license, | ||
| newLicense: 'MIT' | ||
| } | ||
| }; | ||
| } else { | ||
| // No alternative found - requires manual review | ||
| this.skipped.push({ | ||
| package: packageName, | ||
| license: warning.license, | ||
| severity: warning.severity, | ||
| reason: 'No permissive alternative available - manual review required' | ||
| }); | ||
| return { | ||
| success: false, | ||
| action: 'review', | ||
| reason: 'No alternative found' | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| this.errors.push({ | ||
| package: warning.package, | ||
| package: packageName, | ||
| error: error.message | ||
| }); | ||
| report.addError(warning.package, error); | ||
| return false; | ||
| return { | ||
| success: false, | ||
| action: 'error', | ||
| reason: error.message | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * Replace package with license-compatible alternative | ||
| * Display summary of license fixes | ||
| */ | ||
| async replacePackage(warning, projectPath, report, progress, skipConfirmation) { | ||
| const pkgName = warning.package.split('@')[0]; | ||
| const alternative = warning.suggestedAlternative; | ||
| if (!alternative) { | ||
| this.fixesSkipped.push({ | ||
| package: warning.package, | ||
| reason: 'No alternative available', | ||
| warning: warning | ||
| displaySummary() { | ||
| console.log(chalk.bold.cyan('\n⚖️ LICENSE FIXES SUMMARY\n')); | ||
| if (this.fixes.length > 0) { | ||
| console.log(chalk.green(`✓ ${this.fixes.length} license conflict(s) resolved:\n`)); | ||
| this.fixes.forEach(fix => { | ||
| console.log(` ${chalk.cyan(fix.package)} → ${chalk.green(fix.replacement)}`); | ||
| console.log(` ${chalk.gray('License:')} ${chalk.red(fix.oldLicense)} → ${chalk.green(fix.newLicense)}`); | ||
| }); | ||
| return false; | ||
| console.log(''); | ||
| } | ||
| // Requires confirmation unless skipConfirmation is true | ||
| if (!skipConfirmation && warning.requiresConfirmation) { | ||
| this.fixesSkipped.push({ | ||
| package: warning.package, | ||
| reason: 'Requires confirmation (use --yes to auto-apply)', | ||
| warning: warning | ||
| if (this.skipped.length > 0) { | ||
| console.log(chalk.yellow(`⚠️ ${this.skipped.length} conflict(s) require manual review:\n`)); | ||
| this.skipped.forEach(skip => { | ||
| console.log(` ${chalk.yellow(skip.package)}`); | ||
| console.log(` ${chalk.gray('License:')} ${skip.license}`); | ||
| console.log(` ${chalk.gray('Severity:')} ${skip.severity}`); | ||
| console.log(` ${chalk.gray('Reason:')} ${skip.reason}`); | ||
| }); | ||
| report.addSkipped( | ||
| warning.package, | ||
| 'License conflict - requires confirmation' | ||
| ); | ||
| return false; | ||
| console.log(''); | ||
| } | ||
| progress.update(`Replacing ${pkgName} with ${alternative.name}...`); | ||
| try { | ||
| // Remove conflicting package | ||
| execSync(`npm uninstall ${pkgName}`, { | ||
| cwd: projectPath, | ||
| stdio: 'pipe' | ||
| if (this.errors.length > 0) { | ||
| console.log(chalk.red(`✗ ${this.errors.length} error(s) occurred:\n`)); | ||
| this.errors.forEach(err => { | ||
| console.log(` ${chalk.red(err.package)}`); | ||
| console.log(` ${chalk.gray('Error:')} ${err.error}`); | ||
| }); | ||
| // Install alternative | ||
| execSync(`npm install ${alternative.name}`, { | ||
| cwd: projectPath, | ||
| stdio: 'pipe' | ||
| }); | ||
| this.fixesApplied.push({ | ||
| type: 'license_replaced', | ||
| package: warning.package, | ||
| alternative: alternative.name, | ||
| action: `Replaced with license-compatible alternative: ${alternative.name}`, | ||
| oldLicense: warning.license, | ||
| newLicense: alternative.license | ||
| }); | ||
| report.addFix( | ||
| 'license-conflict', | ||
| warning.package, | ||
| `Replaced with ${alternative.name}`, | ||
| { | ||
| from: warning.package, | ||
| to: alternative.name, | ||
| oldLicense: warning.license, | ||
| newLicense: alternative.license, | ||
| reason: warning.reason | ||
| } | ||
| ); | ||
| return true; | ||
| } catch (error) { | ||
| throw new Error(`Failed to replace ${pkgName}: ${error.message}`); | ||
| console.log(''); | ||
| } | ||
| } | ||
| /** | ||
| * Find alternative for a package | ||
| * Get summary statistics | ||
| * @returns {Object} | ||
| */ | ||
| findAlternative(packageName, license) { | ||
| if (!this.alternatives) return null; | ||
| // Check GPL alternatives | ||
| if (license.includes('GPL') && !license.includes('LGPL')) { | ||
| const gplAlts = this.alternatives.gpl_alternatives[packageName]; | ||
| if (gplAlts && gplAlts.alternatives.length > 0) { | ||
| return gplAlts.alternatives[0]; | ||
| } | ||
| } | ||
| // Check AGPL alternatives | ||
| if (license.includes('AGPL')) { | ||
| const agplAlts = this.alternatives.agpl_alternatives[packageName]; | ||
| if (agplAlts && agplAlts.alternatives.length > 0) { | ||
| return agplAlts.alternatives[0]; | ||
| } | ||
| } | ||
| // Check LGPL alternatives | ||
| if (license.includes('LGPL')) { | ||
| const lgplAlts = this.alternatives.lgpl_alternatives[packageName]; | ||
| if (lgplAlts && lgplAlts.alternatives.length > 0) { | ||
| return lgplAlts.alternatives[0]; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Get summary of fixes | ||
| */ | ||
| getSummary() { | ||
| return { | ||
| applied: this.fixesApplied.length, | ||
| skipped: this.fixesSkipped.length, | ||
| errors: this.errors.length, | ||
| details: { | ||
| gplReplaced: this.fixesApplied.filter(f => f.oldLicense && f.oldLicense.includes('GPL') && !f.oldLicense.includes('LGPL')).length, | ||
| agplReplaced: this.fixesApplied.filter(f => f.oldLicense && f.oldLicense.includes('AGPL')).length, | ||
| lgplReplaced: this.fixesApplied.filter(f => f.oldLicense && f.oldLicense.includes('LGPL')).length | ||
| } | ||
| totalFixes: this.fixes.length, | ||
| totalSkipped: this.skipped.length, | ||
| totalErrors: this.errors.length, | ||
| fixes: this.fixes, | ||
| skipped: this.skipped, | ||
| errors: this.errors | ||
| }; | ||
| } | ||
| /** | ||
| * Display summary | ||
| * Reset fixer state | ||
| */ | ||
| displaySummary() { | ||
| const summary = this.getSummary(); | ||
| if (summary.applied > 0) { | ||
| console.log(chalk.green.bold(`\n✓ License Conflict Fixes Applied: ${summary.applied}`)); | ||
| if (summary.details.gplReplaced > 0) { | ||
| console.log(chalk.yellow(` • GPL packages replaced: ${summary.details.gplReplaced}`)); | ||
| } | ||
| if (summary.details.agplReplaced > 0) { | ||
| console.log(chalk.red(` • AGPL packages replaced: ${summary.details.agplReplaced}`)); | ||
| } | ||
| if (summary.details.lgplReplaced > 0) { | ||
| console.log(chalk.yellow(` • LGPL packages replaced: ${summary.details.lgplReplaced}`)); | ||
| } | ||
| } | ||
| if (summary.skipped > 0) { | ||
| console.log(chalk.yellow(`\n⊘ License Conflict Fixes Skipped: ${summary.skipped}`)); | ||
| this.fixesSkipped.forEach(skip => { | ||
| console.log(chalk.gray(` • ${skip.package}: ${skip.reason}`)); | ||
| }); | ||
| } | ||
| if (summary.errors > 0) { | ||
| console.log(chalk.red(`\n✗ License Conflict Fix Errors: ${summary.errors}`)); | ||
| this.errors.forEach(err => { | ||
| console.log(chalk.red(` • ${err.package}: ${err.error}`)); | ||
| }); | ||
| } | ||
| reset() { | ||
| this.fixes = []; | ||
| this.skipped = []; | ||
| this.errors = []; | ||
| } | ||
@@ -224,0 +158,0 @@ } |
+136
-212
| // src/utils/quality-fixer.js | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| // v3.1.4 - Quality issue fixer with dynamic alternatives | ||
| const { execSync } = require('child_process'); | ||
| const chalk = require('chalk'); | ||
| const { analyzer } = require('../services'); | ||
| class QualityFixer { | ||
| constructor() { | ||
| this.fixesApplied = []; | ||
| this.fixesSkipped = []; | ||
| this.alternatives = null; | ||
| this.fixes = []; | ||
| this.skipped = []; | ||
| this.errors = []; | ||
| } | ||
| /** | ||
| * Load quality alternatives database | ||
| * Find alternative for a package (dynamic lookup) | ||
| */ | ||
| loadAlternatives() { | ||
| if (this.alternatives) { | ||
| return this.alternatives; | ||
| } | ||
| async findAlternative(packageName) { | ||
| try { | ||
| const alternativesPath = path.join(__dirname, '../../data/quality-alternatives.json'); | ||
| this.alternatives = JSON.parse(fs.readFileSync(alternativesPath, 'utf8')); | ||
| return this.alternatives; | ||
| const result = await analyzer.quality.analyzePackage(packageName); | ||
| if (result.alternative) { | ||
| return { | ||
| recommended: result.alternative, | ||
| reason: result.deprecationMessage || 'Package has quality issues', | ||
| migration_guide: null // Could be enhanced with migration guides | ||
| }; | ||
| } | ||
| return null; | ||
| } catch (error) { | ||
| console.error(chalk.yellow('⚠️ Could not load quality alternatives database')); | ||
| this.alternatives = { | ||
| abandoned_alternatives: {}, | ||
| stale_alternatives: {}, | ||
| migration_guides: {} | ||
| }; | ||
| return this.alternatives; | ||
| return null; | ||
| } | ||
| } | ||
| async fixWarning(warning, dryRun = false) { | ||
| this.loadAlternatives(); | ||
| const { package: pkgName, status, healthScore } = warning; | ||
| // Determine fix action based on status | ||
| if (status === 'ABANDONED' || status === 'DEPRECATED') { | ||
| return this.replacePackage(pkgName, warning, dryRun); | ||
| } else if (status === 'STALE') { | ||
| return this.replacePackage(pkgName, warning, dryRun); | ||
| } else if (status === 'NEEDS_ATTENTION') { | ||
| // For packages that need attention but aren't abandoned, just review | ||
| return this.reviewPackage(pkgName, warning, dryRun); | ||
| } | ||
| return { | ||
| success: false, | ||
| action: 'skipped', | ||
| reason: 'No fix available for this status' | ||
| }; | ||
| } | ||
| /** | ||
| * Replace package with modern alternative | ||
| * Fix a quality warning | ||
| */ | ||
| async replacePackage(pkgName, warning, dryRun = false) { | ||
| const alternative = this.findAlternative(pkgName); | ||
| if (!alternative) { | ||
| this.fixesSkipped.push({ | ||
| package: pkgName, | ||
| reason: 'No alternative found in database', | ||
| status: warning.status | ||
| async fixWarning(pkg, dryRun = false) { | ||
| const packageName = pkg.name || pkg.package; | ||
| try { | ||
| // Get dynamic alternative | ||
| const alternative = await this.findAlternative(packageName); | ||
| if (alternative) { | ||
| if (!dryRun) { | ||
| // Uninstall old package | ||
| try { | ||
| execSync(`npm uninstall ${packageName}`, { | ||
| stdio: 'pipe', | ||
| cwd: process.cwd() | ||
| }); | ||
| } catch (error) { | ||
| // Ignore uninstall errors | ||
| } | ||
| // Install alternative | ||
| execSync(`npm install ${alternative.recommended}`, { | ||
| stdio: 'pipe', | ||
| cwd: process.cwd() | ||
| }); | ||
| } | ||
| this.fixes.push({ | ||
| package: packageName, | ||
| action: 'replaced', | ||
| replacement: alternative.recommended, | ||
| reason: alternative.reason, | ||
| status: pkg.status | ||
| }); | ||
| return { | ||
| success: true, | ||
| action: 'replaced', | ||
| metadata: { | ||
| from: packageName, | ||
| to: alternative.recommended, | ||
| reason: alternative.reason | ||
| } | ||
| }; | ||
| } else { | ||
| // No alternative available - requires manual review | ||
| this.skipped.push({ | ||
| package: packageName, | ||
| reason: 'No alternative available - requires manual review', | ||
| status: pkg.status | ||
| }); | ||
| return { | ||
| success: false, | ||
| action: 'review', | ||
| reason: 'No alternative available' | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| this.errors.push({ | ||
| package: packageName, | ||
| error: error.message | ||
| }); | ||
| return { | ||
| success: false, | ||
| action: 'skipped', | ||
| reason: 'No alternative available' | ||
| action: 'error', | ||
| reason: error.message | ||
| }; | ||
| } | ||
| const fix = { | ||
| package: pkgName, | ||
| action: 'Replace with modern alternative', | ||
| alternative: alternative.recommended, | ||
| allAlternatives: alternative.alternatives, | ||
| reason: alternative.reason, | ||
| migrationGuide: alternative.migration_guide, | ||
| status: warning.status | ||
| }; | ||
| if (dryRun) { | ||
| return { | ||
| success: true, | ||
| action: 'planned', | ||
| fix | ||
| }; | ||
| } | ||
| // Record the fix | ||
| this.fixesApplied.push(fix); | ||
| return { | ||
| success: true, | ||
| action: 'replaced', | ||
| fix, | ||
| command: `npm uninstall ${pkgName} && npm install ${alternative.recommended}` | ||
| }; | ||
| } | ||
| /** | ||
| * Review package (no automatic fix) | ||
| * Display summary of quality fixes | ||
| */ | ||
| async reviewPackage(pkgName, warning, dryRun = false) { | ||
| const fix = { | ||
| package: pkgName, | ||
| action: 'Review and monitor', | ||
| healthScore: warning.healthScore, | ||
| lastUpdate: warning.lastUpdate, | ||
| reason: 'Package needs attention but is not abandoned', | ||
| status: warning.status | ||
| }; | ||
| if (dryRun) { | ||
| return { | ||
| success: true, | ||
| action: 'review', | ||
| fix | ||
| }; | ||
| displaySummary() { | ||
| console.log(chalk.bold.cyan('\n📦 QUALITY FIXES SUMMARY\n')); | ||
| if (this.fixes.length > 0) { | ||
| console.log(chalk.green(`✓ ${this.fixes.length} package(s) replaced:\n`)); | ||
| this.fixes.forEach(fix => { | ||
| console.log(` ${chalk.cyan(fix.package)} → ${chalk.green(fix.replacement)}`); | ||
| console.log(` ${chalk.gray('Reason:')} ${fix.reason}`); | ||
| }); | ||
| console.log(''); | ||
| } | ||
| this.fixesSkipped.push(fix); | ||
| return { | ||
| success: false, | ||
| action: 'review', | ||
| reason: 'Manual review required', | ||
| fix | ||
| }; | ||
| } | ||
| /** | ||
| * Remove package without replacement | ||
| */ | ||
| async removePackage(pkgName, warning, dryRun = false) { | ||
| const fix = { | ||
| package: pkgName, | ||
| action: 'Remove package', | ||
| reason: warning.reason || 'Package is obsolete', | ||
| status: warning.status | ||
| }; | ||
| if (dryRun) { | ||
| return { | ||
| success: true, | ||
| action: 'planned', | ||
| fix | ||
| }; | ||
| if (this.skipped.length > 0) { | ||
| console.log(chalk.yellow(`⚠️ ${this.skipped.length} package(s) require manual review:\n`)); | ||
| this.skipped.forEach(skip => { | ||
| console.log(` ${chalk.yellow(skip.package)}`); | ||
| console.log(` ${chalk.gray('Reason:')} ${skip.reason}`); | ||
| }); | ||
| console.log(''); | ||
| } | ||
| this.fixesApplied.push(fix); | ||
| return { | ||
| success: true, | ||
| action: 'removed', | ||
| fix, | ||
| command: `npm uninstall ${pkgName}` | ||
| }; | ||
| } | ||
| /** | ||
| * Find alternative for a package | ||
| */ | ||
| findAlternative(pkgName) { | ||
| this.loadAlternatives(); | ||
| // Check abandoned packages first | ||
| if (this.alternatives.abandoned_alternatives[pkgName]) { | ||
| return this.alternatives.abandoned_alternatives[pkgName]; | ||
| if (this.errors.length > 0) { | ||
| console.log(chalk.red(`✗ ${this.errors.length} error(s) occurred:\n`)); | ||
| this.errors.forEach(err => { | ||
| console.log(` ${chalk.red(err.package)}`); | ||
| console.log(` ${chalk.gray('Error:')} ${err.error}`); | ||
| }); | ||
| console.log(''); | ||
| } | ||
| // Check stale packages | ||
| if (this.alternatives.stale_alternatives[pkgName]) { | ||
| return this.alternatives.stale_alternatives[pkgName]; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Get migration guide URL | ||
| * Get summary statistics | ||
| */ | ||
| getMigrationGuide(from, to) { | ||
| this.loadAlternatives(); | ||
| const key = `${from}_to_${to}`; | ||
| return this.alternatives.migration_guides[key] || null; | ||
| } | ||
| /** | ||
| * Get summary of fixes | ||
| */ | ||
| getSummary() { | ||
| const abandonedFixed = this.fixesApplied.filter(f => | ||
| f.status === 'ABANDONED' || f.status === 'DEPRECATED' | ||
| ).length; | ||
| const staleFixed = this.fixesApplied.filter(f => | ||
| f.status === 'STALE' | ||
| ).length; | ||
| return { | ||
| total: this.fixesApplied.length, | ||
| abandoned: abandonedFixed, | ||
| stale: staleFixed, | ||
| skipped: this.fixesSkipped.length, | ||
| fixes: this.fixesApplied, | ||
| skippedItems: this.fixesSkipped | ||
| totalFixes: this.fixes.length, | ||
| totalSkipped: this.skipped.length, | ||
| totalErrors: this.errors.length, | ||
| fixes: this.fixes, | ||
| skipped: this.skipped, | ||
| errors: this.errors | ||
| }; | ||
| } | ||
| /** | ||
| * Display summary in terminal | ||
| * Reset fixer state | ||
| */ | ||
| displaySummary() { | ||
| const summary = this.getSummary(); | ||
| if (summary.total === 0) { | ||
| return; | ||
| } | ||
| console.log('\n' + chalk.green('✓ Package Quality Fixes Applied: ') + chalk.bold(summary.total)); | ||
| if (summary.abandoned > 0) { | ||
| console.log(chalk.gray(' • Abandoned/deprecated packages replaced: ') + chalk.bold(summary.abandoned)); | ||
| } | ||
| if (summary.stale > 0) { | ||
| console.log(chalk.gray(' • Stale packages replaced: ') + chalk.bold(summary.stale)); | ||
| } | ||
| if (summary.skipped > 0) { | ||
| console.log(chalk.gray(' • Packages skipped (no alternative): ') + chalk.bold(summary.skipped)); | ||
| } | ||
| } | ||
| /** | ||
| * Reset fixes tracking | ||
| */ | ||
| reset() { | ||
| this.fixesApplied = []; | ||
| this.fixesSkipped = []; | ||
| this.fixes = []; | ||
| this.skipped = []; | ||
| this.errors = []; | ||
| } | ||
@@ -249,0 +173,0 @@ } |
+160
-176
| // src/utils/supply-chain-fixer.js | ||
| // v3.1.4 - Supply chain security fixer with dynamic detection | ||
| const { execSync } = require('child_process'); | ||
| const chalk = require('chalk'); | ||
| const { analyzer } = require('../services'); | ||
| class SupplyChainFixer { | ||
| constructor() { | ||
| this.fixesApplied = []; | ||
| this.fixesSkipped = []; | ||
| this.fixes = []; | ||
| this.skipped = []; | ||
| this.errors = []; | ||
| } | ||
| async fixWarning(warning, projectPath, report, progress, skipConfirmation = false) { | ||
| /** | ||
| * Fix a supply chain warning | ||
| */ | ||
| async fixWarning(warning, dryRun = false) { | ||
| const packageName = warning.package; | ||
| try { | ||
| switch (warning.autoFixAction) { | ||
| case 'remove': | ||
| return await this.removeMaliciousPackage(warning, projectPath, report, progress); | ||
| if (warning.type === 'typosquatting') { | ||
| // Verify it's still suspicious with live check | ||
| const check = analyzer.security.checkTyposquatting(packageName); | ||
| case 'replace': | ||
| return await this.replaceTyposquatPackage(warning, projectPath, report, progress); | ||
| case 'review': | ||
| // Requires manual confirmation due to suspicious scripts | ||
| if (skipConfirmation || warning.requiresConfirmation === false) { | ||
| return await this.removeSuspiciousPackage(warning, projectPath, report, progress); | ||
| } else { | ||
| this.fixesSkipped.push({ | ||
| package: warning.package, | ||
| reason: 'Requires manual review (suspicious install script)', | ||
| warning: warning | ||
| if (check) { | ||
| if (!dryRun) { | ||
| // Remove suspicious package | ||
| execSync(`npm uninstall ${packageName}`, { | ||
| stdio: 'pipe', | ||
| cwd: process.cwd() | ||
| }); | ||
| report.addSkipped( | ||
| warning.package, | ||
| 'Suspicious install script - requires manual review' | ||
| ); | ||
| return false; | ||
| // Install correct package if suggested | ||
| if (warning.correctPackage) { | ||
| try { | ||
| execSync(`npm install ${warning.correctPackage}`, { | ||
| stdio: 'pipe', | ||
| cwd: process.cwd() | ||
| }); | ||
| } catch (error) { | ||
| // If correct package install fails, just remove suspicious one | ||
| } | ||
| } | ||
| } | ||
| this.fixes.push({ | ||
| package: packageName, | ||
| action: 'removed', | ||
| correctPackage: warning.correctPackage || null, | ||
| reason: 'Typosquatting detected', | ||
| distance: warning.distance | ||
| }); | ||
| return { | ||
| success: true, | ||
| action: 'removed', | ||
| metadata: { | ||
| package: packageName, | ||
| correctPackage: warning.correctPackage, | ||
| reason: 'Typosquatting' | ||
| } | ||
| }; | ||
| } else { | ||
| // No longer flagged as suspicious | ||
| this.skipped.push({ | ||
| package: packageName, | ||
| reason: 'No longer flagged as suspicious' | ||
| }); | ||
| return { | ||
| success: false, | ||
| action: 'skip', | ||
| reason: 'Not suspicious' | ||
| }; | ||
| } | ||
| } else if (warning.type === 'vulnerability') { | ||
| // Security vulnerabilities are handled by npm audit fix | ||
| if (!dryRun) { | ||
| execSync('npm audit fix', { | ||
| stdio: 'pipe', | ||
| cwd: process.cwd() | ||
| }); | ||
| } | ||
| default: | ||
| this.fixesSkipped.push({ | ||
| package: warning.package, | ||
| reason: 'No auto-fix available', | ||
| warning: warning | ||
| }); | ||
| return false; | ||
| this.fixes.push({ | ||
| package: packageName, | ||
| action: 'updated', | ||
| reason: 'Security vulnerability fixed', | ||
| severity: warning.severity | ||
| }); | ||
| return { | ||
| success: true, | ||
| action: 'updated', | ||
| metadata: { | ||
| package: packageName, | ||
| severity: warning.severity | ||
| } | ||
| }; | ||
| } else { | ||
| // Unknown type - skip | ||
| this.skipped.push({ | ||
| package: packageName, | ||
| reason: `Unknown warning type: ${warning.type}` | ||
| }); | ||
| return { | ||
| success: false, | ||
| action: 'skip', | ||
| reason: 'Unknown type' | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| this.errors.push({ | ||
| package: warning.package, | ||
| package: packageName, | ||
| error: error.message | ||
| }); | ||
| report.addError(warning.package, error); | ||
| return false; | ||
| return { | ||
| success: false, | ||
| action: 'error', | ||
| reason: error.message | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * Remove malicious package | ||
| * Display summary of supply chain fixes | ||
| */ | ||
| async removeMaliciousPackage(warning, projectPath, report, progress) { | ||
| progress.update(`Removing malicious package: ${warning.package}...`); | ||
| try { | ||
| execSync(`npm uninstall ${warning.package}`, { | ||
| cwd: projectPath, | ||
| stdio: 'pipe' | ||
| displaySummary() { | ||
| console.log(chalk.bold.cyan('\n🛡️ SUPPLY CHAIN FIXES SUMMARY\n')); | ||
| if (this.fixes.length > 0) { | ||
| console.log(chalk.green(`✓ ${this.fixes.length} issue(s) resolved:\n`)); | ||
| this.fixes.forEach(fix => { | ||
| if (fix.action === 'removed') { | ||
| console.log(` ${chalk.red('✗')} Removed: ${chalk.cyan(fix.package)}`); | ||
| if (fix.correctPackage) { | ||
| console.log(` ${chalk.green('✓')} Installed: ${chalk.green(fix.correctPackage)}`); | ||
| } | ||
| console.log(` ${chalk.gray('Reason:')} ${fix.reason}`); | ||
| } else if (fix.action === 'updated') { | ||
| console.log(` ${chalk.green('↑')} Updated: ${chalk.cyan(fix.package)}`); | ||
| console.log(` ${chalk.gray('Reason:')} ${fix.reason}`); | ||
| } | ||
| }); | ||
| this.fixesApplied.push({ | ||
| type: 'malicious_removed', | ||
| package: warning.package, | ||
| action: 'Removed malicious package' | ||
| }); | ||
| report.addFix( | ||
| 'supply-chain', | ||
| warning.package, | ||
| 'Removed malicious package', | ||
| { reason: warning.reason } | ||
| ); | ||
| return true; | ||
| } catch (error) { | ||
| throw new Error(`Failed to remove ${warning.package}: ${error.message}`); | ||
| console.log(''); | ||
| } | ||
| } | ||
| /** | ||
| * Replace typosquatting package with correct one | ||
| */ | ||
| async replaceTyposquatPackage(warning, projectPath, report, progress) { | ||
| progress.update(`Fixing typosquatting: ${warning.package} → ${warning.replacement}...`); | ||
| try { | ||
| // Remove typosquat | ||
| execSync(`npm uninstall ${warning.package}`, { | ||
| cwd: projectPath, | ||
| stdio: 'pipe' | ||
| if (this.skipped.length > 0) { | ||
| console.log(chalk.yellow(`⚠️ ${this.skipped.length} warning(s) skipped:\n`)); | ||
| this.skipped.forEach(skip => { | ||
| console.log(` ${chalk.yellow(skip.package)}`); | ||
| console.log(` ${chalk.gray('Reason:')} ${skip.reason}`); | ||
| }); | ||
| // Install correct package | ||
| execSync(`npm install ${warning.replacement}`, { | ||
| cwd: projectPath, | ||
| stdio: 'pipe' | ||
| }); | ||
| this.fixesApplied.push({ | ||
| type: 'typosquat_fixed', | ||
| package: warning.package, | ||
| replacement: warning.replacement, | ||
| action: `Replaced with correct package: ${warning.replacement}` | ||
| }); | ||
| report.addFix( | ||
| 'supply-chain', | ||
| warning.package, | ||
| `Replaced typosquat with ${warning.replacement}`, | ||
| { | ||
| from: warning.package, | ||
| to: warning.replacement, | ||
| reason: warning.reason | ||
| } | ||
| ); | ||
| return true; | ||
| } catch (error) { | ||
| throw new Error(`Failed to replace ${warning.package}: ${error.message}`); | ||
| console.log(''); | ||
| } | ||
| } | ||
| /** | ||
| * Remove package with suspicious install scripts | ||
| */ | ||
| async removeSuspiciousPackage(warning, projectPath, report, progress) { | ||
| progress.update(`Removing suspicious package: ${warning.package}...`); | ||
| try { | ||
| execSync(`npm uninstall ${warning.package}`, { | ||
| cwd: projectPath, | ||
| stdio: 'pipe' | ||
| if (this.errors.length > 0) { | ||
| console.log(chalk.red(`✗ ${this.errors.length} error(s) occurred:\n`)); | ||
| this.errors.forEach(err => { | ||
| console.log(` ${chalk.red(err.package)}`); | ||
| console.log(` ${chalk.gray('Error:')} ${err.error}`); | ||
| }); | ||
| this.fixesApplied.push({ | ||
| type: 'suspicious_removed', | ||
| package: warning.package, | ||
| action: 'Removed package with suspicious install script' | ||
| }); | ||
| report.addFix( | ||
| 'supply-chain', | ||
| warning.package, | ||
| 'Removed package with suspicious install script', | ||
| { | ||
| script: warning.script, | ||
| patterns: warning.patterns, | ||
| reason: warning.reason | ||
| } | ||
| ); | ||
| return true; | ||
| } catch (error) { | ||
| throw new Error(`Failed to remove ${warning.package}: ${error.message}`); | ||
| console.log(''); | ||
| } | ||
| } | ||
| /** | ||
| * Get summary of fixes | ||
| * Get summary statistics | ||
| */ | ||
| getSummary() { | ||
| return { | ||
| applied: this.fixesApplied.length, | ||
| skipped: this.fixesSkipped.length, | ||
| errors: this.errors.length, | ||
| details: { | ||
| maliciousRemoved: this.fixesApplied.filter(f => f.type === 'malicious_removed').length, | ||
| typosquatsFixed: this.fixesApplied.filter(f => f.type === 'typosquat_fixed').length, | ||
| suspiciousRemoved: this.fixesApplied.filter(f => f.type === 'suspicious_removed').length | ||
| } | ||
| totalFixes: this.fixes.length, | ||
| totalSkipped: this.skipped.length, | ||
| totalErrors: this.errors.length, | ||
| fixes: this.fixes, | ||
| skipped: this.skipped, | ||
| errors: this.errors | ||
| }; | ||
| } | ||
| /** | ||
| * Display summary | ||
| * Reset fixer state | ||
| */ | ||
| displaySummary() { | ||
| const summary = this.getSummary(); | ||
| if (summary.applied > 0) { | ||
| console.log(chalk.green.bold(`\n✓ Supply Chain Fixes Applied: ${summary.applied}`)); | ||
| if (summary.details.maliciousRemoved > 0) { | ||
| console.log(chalk.red(` • Malicious packages removed: ${summary.details.maliciousRemoved}`)); | ||
| } | ||
| if (summary.details.typosquatsFixed > 0) { | ||
| console.log(chalk.yellow(` • Typosquats fixed: ${summary.details.typosquatsFixed}`)); | ||
| } | ||
| if (summary.details.suspiciousRemoved > 0) { | ||
| console.log(chalk.yellow(` • Suspicious packages removed: ${summary.details.suspiciousRemoved}`)); | ||
| } | ||
| } | ||
| if (summary.skipped > 0) { | ||
| console.log(chalk.yellow(`\n⊘ Supply Chain Fixes Skipped: ${summary.skipped}`)); | ||
| this.fixesSkipped.forEach(skip => { | ||
| console.log(chalk.gray(` • ${skip.package}: ${skip.reason}`)); | ||
| }); | ||
| } | ||
| if (summary.errors > 0) { | ||
| console.log(chalk.red(`\n✗ Supply Chain Fix Errors: ${summary.errors}`)); | ||
| this.errors.forEach(err => { | ||
| console.log(chalk.red(` • ${err.package}: ${err.error}`)); | ||
| }); | ||
| } | ||
| reset() { | ||
| this.fixes = []; | ||
| this.skipped = []; | ||
| this.errors = []; | ||
| } | ||
@@ -215,0 +199,0 @@ } |
| { | ||
| "malicious_packages": [ | ||
| "epress", | ||
| "expres", | ||
| "expresss", | ||
| "reqest", | ||
| "requet", | ||
| "lodas", | ||
| "loadsh", | ||
| "axois", | ||
| "axioss", | ||
| "webpak", | ||
| "webpackk", | ||
| "reactt", | ||
| "vuee", | ||
| "angularr" | ||
| ], | ||
| "typosquat_patterns": { | ||
| "express": ["epress", "expres", "expresss", "exprss"], | ||
| "request": ["reqest", "requet", "requets"], | ||
| "lodash": ["lodas", "loadsh", "lodahs", "lodsh"], | ||
| "axios": ["axois", "axioss", "axos", "axious"], | ||
| "webpack": ["webpak", "webpackk", "wepback"], | ||
| "react": ["reactt", "reakt", "raect"], | ||
| "vue": ["vuee", "veu", "vuw"], | ||
| "angular": ["angularr", "anguler", "angulr"], | ||
| "next": ["nextt", "nxt", "nex"], | ||
| "typescript": ["typscript", "typescrpt", "typescrip"], | ||
| "eslint": ["esslint", "elint", "eslnt"], | ||
| "prettier": ["pretier", "prettir", "pretter"], | ||
| "jest": ["jst", "jestt", "jест"], | ||
| "mocha": ["mocha", "mоcha", "mосha"], | ||
| "chai": ["chаi", "сhai", "chаi"] | ||
| }, | ||
| "suspicious_patterns": { | ||
| "install_scripts": [ | ||
| "curl", | ||
| "wget", | ||
| "powershell", | ||
| "eval", | ||
| "exec", | ||
| "child_process", | ||
| "/bin/sh", | ||
| "/bin/bash", | ||
| "http://", | ||
| "https://" | ||
| ], | ||
| "suspicious_dependencies": [ | ||
| "bitcoin", | ||
| "cryptocurrency", | ||
| "mining", | ||
| "miner", | ||
| "keylogger", | ||
| "backdoor" | ||
| ] | ||
| } | ||
| } |
| { | ||
| "gpl_alternatives": { | ||
| "readline": { | ||
| "license": "GPL-3.0", | ||
| "alternatives": [ | ||
| { | ||
| "name": "readline-sync", | ||
| "license": "MIT", | ||
| "description": "Synchronous readline for interactively running" | ||
| } | ||
| ] | ||
| }, | ||
| "node-gtk": { | ||
| "license": "GPL-3.0", | ||
| "alternatives": [ | ||
| { | ||
| "name": "electron", | ||
| "license": "MIT", | ||
| "description": "Build cross-platform desktop apps" | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| "agpl_alternatives": { | ||
| "ghost": { | ||
| "license": "AGPL-3.0", | ||
| "alternatives": [ | ||
| { | ||
| "name": "hexo", | ||
| "license": "MIT", | ||
| "description": "A fast, simple & powerful blog framework" | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| "lgpl_alternatives": { | ||
| "sharp": { | ||
| "license": "LGPL-3.0", | ||
| "alternatives": [ | ||
| { | ||
| "name": "jimp", | ||
| "license": "MIT", | ||
| "description": "An image processing library written entirely in JavaScript" | ||
| }, | ||
| { | ||
| "name": "gm", | ||
| "license": "MIT", | ||
| "description": "GraphicsMagick and ImageMagick for node" | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| "unlicensed_alternatives": { | ||
| "default": [ | ||
| { | ||
| "suggestion": "Find well-maintained MIT/Apache-2.0 alternative", | ||
| "action": "Search npm for similar packages with permissive licenses" | ||
| } | ||
| ] | ||
| }, | ||
| "compatible_licenses": { | ||
| "MIT": ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "CC0-1.0", "Unlicense"], | ||
| "Apache-2.0": ["Apache-2.0", "MIT", "BSD-2-Clause", "BSD-3-Clause", "ISC"], | ||
| "GPL-2.0": ["GPL-2.0", "GPL-3.0"], | ||
| "GPL-3.0": ["GPL-3.0"], | ||
| "AGPL-3.0": ["AGPL-3.0"] | ||
| } | ||
| } |
| { | ||
| "abandoned_alternatives": { | ||
| "request": { | ||
| "alternatives": ["axios", "got", "ky"], | ||
| "recommended": "axios", | ||
| "reason": "request is deprecated since 2020", | ||
| "migration_guide": "https://github.com/request/request/issues/3142" | ||
| }, | ||
| "moment": { | ||
| "alternatives": ["dayjs", "date-fns", "luxon"], | ||
| "recommended": "dayjs", | ||
| "reason": "moment is in maintenance mode", | ||
| "migration_guide": "https://momentjs.com/docs/#/-project-status/" | ||
| }, | ||
| "tslint": { | ||
| "alternatives": ["eslint"], | ||
| "recommended": "eslint", | ||
| "reason": "tslint is deprecated", | ||
| "migration_guide": "https://github.com/palantir/tslint/issues/4534" | ||
| }, | ||
| "node-sass": { | ||
| "alternatives": ["sass", "dart-sass"], | ||
| "recommended": "sass", | ||
| "reason": "node-sass is deprecated, use Dart Sass", | ||
| "migration_guide": "https://sass-lang.com/blog/libsass-is-deprecated" | ||
| }, | ||
| "babel-core": { | ||
| "alternatives": ["@babel/core"], | ||
| "recommended": "@babel/core", | ||
| "reason": "babel-core is deprecated", | ||
| "migration_guide": "https://babeljs.io/docs/en/v7-migration" | ||
| }, | ||
| "colors": { | ||
| "alternatives": ["chalk", "colorette", "picocolors"], | ||
| "recommended": "chalk", | ||
| "reason": "colors had a security incident in 2022", | ||
| "migration_guide": "https://github.com/Marak/colors.js/issues/285" | ||
| }, | ||
| "mkdirp": { | ||
| "alternatives": ["native fs.mkdir with recursive option"], | ||
| "recommended": "native", | ||
| "reason": "mkdirp is obsolete, use native fs.mkdir({recursive: true})", | ||
| "migration_guide": "https://nodejs.org/api/fs.html#fspromisesmkdirpath-options" | ||
| }, | ||
| "rimraf": { | ||
| "alternatives": ["native fs.rm with recursive option"], | ||
| "recommended": "native", | ||
| "reason": "rimraf is obsolete, use native fs.rm({recursive: true})", | ||
| "migration_guide": "https://nodejs.org/api/fs.html#fspromisesrmpath-options" | ||
| }, | ||
| "node-uuid": { | ||
| "alternatives": ["uuid"], | ||
| "recommended": "uuid", | ||
| "reason": "node-uuid is deprecated", | ||
| "migration_guide": "https://github.com/kelektiv/node-uuid" | ||
| }, | ||
| "validator": { | ||
| "alternatives": ["validator (different package)", "joi", "yup"], | ||
| "recommended": "joi", | ||
| "reason": "Old validator package is abandoned", | ||
| "migration_guide": "https://joi.dev/api/" | ||
| }, | ||
| "babel-preset-es2015": { | ||
| "alternatives": ["@babel/preset-env"], | ||
| "recommended": "@babel/preset-env", | ||
| "reason": "babel-preset-es2015 is deprecated", | ||
| "migration_guide": "https://babeljs.io/docs/en/babel-preset-env" | ||
| }, | ||
| "babel-preset-react": { | ||
| "alternatives": ["@babel/preset-react"], | ||
| "recommended": "@babel/preset-react", | ||
| "reason": "babel-preset-react is deprecated", | ||
| "migration_guide": "https://babeljs.io/docs/en/babel-preset-react" | ||
| }, | ||
| "react-addons-test-utils": { | ||
| "alternatives": ["react-dom/test-utils", "@testing-library/react"], | ||
| "recommended": "@testing-library/react", | ||
| "reason": "react-addons-test-utils is deprecated", | ||
| "migration_guide": "https://testing-library.com/docs/react-testing-library/migrate-from-enzyme" | ||
| }, | ||
| "prop-types": { | ||
| "alternatives": ["TypeScript", "Zod"], | ||
| "recommended": "typescript", | ||
| "reason": "Consider TypeScript for type safety", | ||
| "migration_guide": "https://react-typescript-cheatsheet.netlify.app/" | ||
| }, | ||
| "react-router-redux": { | ||
| "alternatives": ["connected-react-router", "react-router v6"], | ||
| "recommended": "react-router", | ||
| "reason": "react-router-redux is deprecated", | ||
| "migration_guide": "https://reactrouter.com/en/main/upgrading/v5" | ||
| }, | ||
| "redux-devtools-extension": { | ||
| "alternatives": ["@redux-devtools/extension"], | ||
| "recommended": "@redux-devtools/extension", | ||
| "reason": "redux-devtools-extension is deprecated", | ||
| "migration_guide": "https://github.com/reduxjs/redux-devtools" | ||
| }, | ||
| "enzyme": { | ||
| "alternatives": ["@testing-library/react", "react-testing-library"], | ||
| "recommended": "@testing-library/react", | ||
| "reason": "enzyme is no longer maintained", | ||
| "migration_guide": "https://testing-library.com/docs/react-testing-library/migrate-from-enzyme" | ||
| }, | ||
| "faker": { | ||
| "alternatives": ["@faker-js/faker"], | ||
| "recommended": "@faker-js/faker", | ||
| "reason": "faker was archived, use @faker-js/faker", | ||
| "migration_guide": "https://fakerjs.dev/guide/upgrading.html" | ||
| }, | ||
| "gulp-util": { | ||
| "alternatives": ["individual gulp utilities"], | ||
| "recommended": "native", | ||
| "reason": "gulp-util is deprecated", | ||
| "migration_guide": "https://github.com/gulpjs/gulp-util" | ||
| }, | ||
| "native-promise-only": { | ||
| "alternatives": ["native Promise"], | ||
| "recommended": "native", | ||
| "reason": "native-promise-only is obsolete", | ||
| "migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" | ||
| }, | ||
| "q": { | ||
| "alternatives": ["native Promise", "bluebird"], | ||
| "recommended": "native", | ||
| "reason": "Q promise library is obsolete", | ||
| "migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" | ||
| }, | ||
| "coffee-script": { | ||
| "alternatives": ["coffeescript"], | ||
| "recommended": "coffeescript", | ||
| "reason": "coffee-script is renamed to coffeescript", | ||
| "migration_guide": "https://coffeescript.org/" | ||
| }, | ||
| "node-pre-gyp": { | ||
| "alternatives": ["@mapbox/node-pre-gyp"], | ||
| "recommended": "@mapbox/node-pre-gyp", | ||
| "reason": "node-pre-gyp moved to @mapbox scope", | ||
| "migration_guide": "https://github.com/mapbox/node-pre-gyp" | ||
| }, | ||
| "bower": { | ||
| "alternatives": ["npm", "yarn"], | ||
| "recommended": "npm", | ||
| "reason": "bower is deprecated", | ||
| "migration_guide": "https://bower.io/blog/2017/how-to-migrate-away-from-bower/" | ||
| }, | ||
| "grunt": { | ||
| "alternatives": ["gulp", "webpack", "vite"], | ||
| "recommended": "vite", | ||
| "reason": "grunt is largely unmaintained", | ||
| "migration_guide": "https://vitejs.dev/guide/migration.html" | ||
| }, | ||
| "phantomjs": { | ||
| "alternatives": ["puppeteer", "playwright"], | ||
| "recommended": "playwright", | ||
| "reason": "phantomjs is abandoned", | ||
| "migration_guide": "https://playwright.dev/docs/intro" | ||
| }, | ||
| "protractor": { | ||
| "alternatives": ["cypress", "playwright", "@playwright/test"], | ||
| "recommended": "playwright", | ||
| "reason": "protractor is deprecated", | ||
| "migration_guide": "https://playwright.dev/docs/protractor" | ||
| }, | ||
| "karma": { | ||
| "alternatives": ["vitest", "jest", "playwright"], | ||
| "recommended": "vitest", | ||
| "reason": "karma is in maintenance mode", | ||
| "migration_guide": "https://vitest.dev/guide/migration.html" | ||
| }, | ||
| "istanbul": { | ||
| "alternatives": ["nyc", "c8"], | ||
| "recommended": "c8", | ||
| "reason": "istanbul is deprecated, use nyc or c8", | ||
| "migration_guide": "https://github.com/bcoe/c8" | ||
| }, | ||
| "grunt-contrib-uglify": { | ||
| "alternatives": ["terser", "esbuild"], | ||
| "recommended": "esbuild", | ||
| "reason": "grunt-contrib-uglify is deprecated", | ||
| "migration_guide": "https://esbuild.github.io/" | ||
| }, | ||
| "uglify-js": { | ||
| "alternatives": ["terser", "esbuild"], | ||
| "recommended": "terser", | ||
| "reason": "uglify-js is no longer maintained", | ||
| "migration_guide": "https://terser.org/" | ||
| }, | ||
| "node-fetch": { | ||
| "alternatives": ["native fetch", "undici"], | ||
| "recommended": "native", | ||
| "reason": "Node.js 18+ has native fetch", | ||
| "migration_guide": "https://nodejs.org/dist/latest-v18.x/docs/api/globals.html#fetch" | ||
| }, | ||
| "isomorphic-fetch": { | ||
| "alternatives": ["native fetch", "cross-fetch"], | ||
| "recommended": "native", | ||
| "reason": "isomorphic-fetch is obsolete with native fetch", | ||
| "migration_guide": "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API" | ||
| }, | ||
| "cross-env": { | ||
| "alternatives": ["dotenv", "native support"], | ||
| "recommended": "dotenv", | ||
| "reason": "cross-env is less needed with modern Node.js", | ||
| "migration_guide": "https://github.com/motdotla/dotenv" | ||
| }, | ||
| "nodemon": { | ||
| "alternatives": ["tsx", "node --watch"], | ||
| "recommended": "tsx", | ||
| "reason": "Node.js 18+ has native --watch flag", | ||
| "migration_guide": "https://nodejs.org/api/cli.html#--watch" | ||
| }, | ||
| "body-parser": { | ||
| "alternatives": ["express.json() built-in"], | ||
| "recommended": "native", | ||
| "reason": "body-parser is built into Express 4.16+", | ||
| "migration_guide": "https://expressjs.com/en/api.html#express.json" | ||
| }, | ||
| "morgan": { | ||
| "alternatives": ["pino-http", "winston"], | ||
| "recommended": "pino-http", | ||
| "reason": "morgan is basic, modern alternatives are better", | ||
| "migration_guide": "https://github.com/pinojs/pino-http" | ||
| }, | ||
| "async": { | ||
| "alternatives": ["native async/await", "Promise"], | ||
| "recommended": "native", | ||
| "reason": "async library is obsolete with native async/await", | ||
| "migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function" | ||
| }, | ||
| "bluebird": { | ||
| "alternatives": ["native Promise"], | ||
| "recommended": "native", | ||
| "reason": "bluebird is mostly obsolete with native Promises", | ||
| "migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" | ||
| }, | ||
| "jade": { | ||
| "alternatives": ["pug"], | ||
| "recommended": "pug", | ||
| "reason": "jade was renamed to pug", | ||
| "migration_guide": "https://pugjs.org/" | ||
| }, | ||
| "ejs": { | ||
| "alternatives": ["handlebars", "pug", "react/vue"], | ||
| "recommended": "handlebars", | ||
| "reason": "ejs is functional but alternatives are more modern", | ||
| "migration_guide": "https://handlebarsjs.com/guide/" | ||
| }, | ||
| "underscore": { | ||
| "alternatives": ["lodash", "native methods"], | ||
| "recommended": "lodash", | ||
| "reason": "underscore is less maintained than lodash", | ||
| "migration_guide": "https://lodash.com/docs/" | ||
| }, | ||
| "harmony-reflect": { | ||
| "alternatives": ["native Reflect"], | ||
| "recommended": "native", | ||
| "reason": "harmony-reflect is obsolete with native Reflect", | ||
| "migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect" | ||
| }, | ||
| "core-js": { | ||
| "alternatives": ["native features", "modern browsers"], | ||
| "recommended": "native", | ||
| "reason": "core-js is often unnecessary with modern browsers", | ||
| "migration_guide": "https://github.com/zloirock/core-js" | ||
| }, | ||
| "left-pad": { | ||
| "alternatives": ["String.prototype.padStart"], | ||
| "recommended": "native", | ||
| "reason": "left-pad is obsolete with native padStart", | ||
| "migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart" | ||
| }, | ||
| "cheerio": { | ||
| "alternatives": ["jsdom", "linkedom"], | ||
| "recommended": "linkedom", | ||
| "reason": "cheerio is slow, linkedom is faster and more modern", | ||
| "migration_guide": "https://github.com/WebReflection/linkedom" | ||
| }, | ||
| "xml2js": { | ||
| "alternatives": ["fast-xml-parser", "xmldom"], | ||
| "recommended": "fast-xml-parser", | ||
| "reason": "xml2js is slower than modern alternatives", | ||
| "migration_guide": "https://github.com/NaturalIntelligence/fast-xml-parser" | ||
| }, | ||
| "should": { | ||
| "alternatives": ["chai", "jest"], | ||
| "recommended": "jest", | ||
| "reason": "should is deprecated", | ||
| "migration_guide": "https://jestjs.io/docs/getting-started" | ||
| } | ||
| }, | ||
| "stale_alternatives": { | ||
| "depcheck": { | ||
| "alternatives": ["npm-check", "knip"], | ||
| "recommended": "knip", | ||
| "reason": "depcheck hasn't been updated in 30+ months", | ||
| "migration_guide": "https://github.com/webpro/knip" | ||
| } | ||
| }, | ||
| "migration_guides": { | ||
| "request_to_axios": "https://github.com/axios/axios#example", | ||
| "moment_to_dayjs": "https://day.js.org/docs/en/parse/string-format", | ||
| "tslint_to_eslint": "https://github.com/typescript-eslint/tslint-to-eslint-config", | ||
| "node-sass_to_sass": "https://sass-lang.com/install", | ||
| "babel_migration": "https://babeljs.io/docs/en/v7-migration", | ||
| "enzyme_to_rtl": "https://testing-library.com/docs/react-testing-library/migrate-from-enzyme" | ||
| } | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
483358
1.5%55
3.77%38
-17.39%12306
-0.99%830
-2.12%12
20%