UNPKG

nextjs-analyzer

Version:

A modular tool that comprehensively analyzes Next.js projects. Includes component, performance, security, SEO, data fetching, code quality, and historical analysis features.

759 lines (640 loc) 28.9 kB
const fs = require('fs-extra'); const path = require('path'); const { logSuccess, logError, i18n } = require('../../utils'); /** * Gelişmiş Görselleştirme Modülü */ module.exports = { name: i18n.t('modules.visualization.name'), description: i18n.t('modules.visualization.description'), /** * Analiz işlemini gerçekleştirir * @param {NextJsAnalyzer} analyzer - Analyzer instance * @param {Object} options - Analiz seçenekleri * @returns {Object} - Analiz sonuçları */ async analyze(analyzer, options) { // Tüm modül sonuçlarını topla const allModuleResults = analyzer.getAllModuleResults(); // Grafik verilerini oluştur const chartData = this.generateChartData(allModuleResults); // Filtreleme seçeneklerini oluştur const filterOptions = this.generateFilterOptions(allModuleResults); return { results: { chartData, filterOptions, allModuleResults }, metadata: { totalModules: Object.keys(allModuleResults).length, totalIssues: this.countTotalIssues(allModuleResults) } }; }, /** * Grafik verilerini oluşturur * @param {Object} allModuleResults - Tüm modül sonuçları * @returns {Object} - Grafik verileri */ generateChartData(allModuleResults) { const moduleNames = Object.keys(allModuleResults); const issueCountsByModule = {}; const issueTypeDistribution = {}; const severityDistribution = {}; // Modül başına sorun sayılarını hesapla for (const moduleName of moduleNames) { const moduleResults = allModuleResults[moduleName]; // Modül başına sorun sayısı let issueCount = 0; // Sorun türlerini ve önem derecelerini topla if (moduleResults.results) { // Component modülü if (moduleName === 'component' && moduleResults.results.serverComponents) { issueCount += moduleResults.results.serverComponents.length; issueTypeDistribution['Server Component'] = (issueTypeDistribution['Server Component'] || 0) + moduleResults.results.serverComponents.length; } // Code quality modülü if (moduleName === 'code-quality' && moduleResults.results.unusedComponents) { issueCount += moduleResults.results.unusedComponents.length; issueTypeDistribution['Unused Component'] = (issueTypeDistribution['Unused Component'] || 0) + moduleResults.results.unusedComponents.length; } // Routing modülü if (moduleName === 'routing' && moduleResults.results.routes) { const dynamicRoutes = moduleResults.results.routes.filter(r => r.isDynamic); issueCount += dynamicRoutes.length; issueTypeDistribution['Dynamic Route'] = (issueTypeDistribution['Dynamic Route'] || 0) + dynamicRoutes.length; } // Performance modülü if (moduleName === 'performance') { if (moduleResults.results.imageOptimization && moduleResults.results.imageOptimization.nonOptimizedImages) { issueCount += moduleResults.results.imageOptimization.nonOptimizedImages.length; issueTypeDistribution['Non-Optimized Image'] = (issueTypeDistribution['Non-Optimized Image'] || 0) + moduleResults.results.imageOptimization.nonOptimizedImages.length; } if (moduleResults.results.bundleSize) { if (moduleResults.results.bundleSize.largeComponents) { issueCount += moduleResults.results.bundleSize.largeComponents.length; issueTypeDistribution['Large Component'] = (issueTypeDistribution['Large Component'] || 0) + moduleResults.results.bundleSize.largeComponents.length; } if (moduleResults.results.bundleSize.largeImports) { issueCount += moduleResults.results.bundleSize.largeImports.length; issueTypeDistribution['Large Import'] = (issueTypeDistribution['Large Import'] || 0) + moduleResults.results.bundleSize.largeImports.length; } } } // Data fetching modülü if (moduleName === 'data-fetching') { if (moduleResults.results.cacheStrategies && moduleResults.results.cacheStrategies.issues) { issueCount += moduleResults.results.cacheStrategies.issues.length; issueTypeDistribution['Cache Strategy Issue'] = (issueTypeDistribution['Cache Strategy Issue'] || 0) + moduleResults.results.cacheStrategies.issues.length; } } // Security modülü if (moduleName === 'security') { if (moduleResults.results.serverComponentSecurity && moduleResults.results.serverComponentSecurity.issues) { issueCount += moduleResults.results.serverComponentSecurity.issues.length; // Önem derecelerine göre dağılım moduleResults.results.serverComponentSecurity.issues.forEach(issue => { if (issue.severity) { severityDistribution[issue.severity] = (severityDistribution[issue.severity] || 0) + 1; } }); issueTypeDistribution['Server Security Issue'] = (issueTypeDistribution['Server Security Issue'] || 0) + moduleResults.results.serverComponentSecurity.issues.length; } if (moduleResults.results.apiRouteSecurity && moduleResults.results.apiRouteSecurity.issues) { issueCount += moduleResults.results.apiRouteSecurity.issues.length; // Önem derecelerine göre dağılım moduleResults.results.apiRouteSecurity.issues.forEach(issue => { if (issue.severity) { severityDistribution[issue.severity] = (severityDistribution[issue.severity] || 0) + 1; } }); issueTypeDistribution['API Security Issue'] = (issueTypeDistribution['API Security Issue'] || 0) + moduleResults.results.apiRouteSecurity.issues.length; } if (moduleResults.results.generalSecurity && moduleResults.results.generalSecurity.issues) { issueCount += moduleResults.results.generalSecurity.issues.length; // Önem derecelerine göre dağılım moduleResults.results.generalSecurity.issues.forEach(issue => { if (issue.severity) { severityDistribution[issue.severity] = (severityDistribution[issue.severity] || 0) + 1; } }); issueTypeDistribution['General Security Issue'] = (issueTypeDistribution['General Security Issue'] || 0) + moduleResults.results.generalSecurity.issues.length; } } // SEO modülü if (moduleName === 'seo') { if (moduleResults.results.metaTags && moduleResults.results.metaTags.issues) { issueCount += moduleResults.results.metaTags.issues.length; issueTypeDistribution['Meta Tag Issue'] = (issueTypeDistribution['Meta Tag Issue'] || 0) + moduleResults.results.metaTags.issues.length; } if (moduleResults.results.semanticHtml && moduleResults.results.semanticHtml.issues) { issueCount += moduleResults.results.semanticHtml.issues.length; issueTypeDistribution['Semantic HTML Issue'] = (issueTypeDistribution['Semantic HTML Issue'] || 0) + moduleResults.results.semanticHtml.issues.length; } if (moduleResults.results.accessibility && moduleResults.results.accessibility.issues) { issueCount += moduleResults.results.accessibility.issues.length; issueTypeDistribution['Accessibility Issue'] = (issueTypeDistribution['Accessibility Issue'] || 0) + moduleResults.results.accessibility.issues.length; } } } issueCountsByModule[moduleName] = issueCount; } return { issueCountsByModule, issueTypeDistribution, severityDistribution }; }, /** * Filtreleme seçeneklerini oluşturur * @param {Object} allModuleResults - Tüm modül sonuçları * @returns {Object} - Filtreleme seçenekleri */ generateFilterOptions(allModuleResults) { const moduleNames = Object.keys(allModuleResults); const issueTypes = new Set(); const severities = new Set(); const files = new Set(); // Tüm modülleri tara for (const moduleName of moduleNames) { const moduleResults = allModuleResults[moduleName]; if (moduleResults.results) { // Component modülü if (moduleName === 'component' && moduleResults.results.serverComponents) { issueTypes.add('Server Component'); moduleResults.results.serverComponents.forEach(component => { files.add(component.file); }); } // Code quality modülü if (moduleName === 'code-quality' && moduleResults.results.unusedComponents) { issueTypes.add('Unused Component'); moduleResults.results.unusedComponents.forEach(component => { files.add(component.path); }); } // Security modülü if (moduleName === 'security') { if (moduleResults.results.serverComponentSecurity && moduleResults.results.serverComponentSecurity.issues) { issueTypes.add('Server Security Issue'); moduleResults.results.serverComponentSecurity.issues.forEach(issue => { if (issue.severity) severities.add(issue.severity); if (issue.file) files.add(issue.file); }); } if (moduleResults.results.apiRouteSecurity && moduleResults.results.apiRouteSecurity.issues) { issueTypes.add('API Security Issue'); moduleResults.results.apiRouteSecurity.issues.forEach(issue => { if (issue.severity) severities.add(issue.severity); if (issue.file) files.add(issue.file); }); } if (moduleResults.results.generalSecurity && moduleResults.results.generalSecurity.issues) { issueTypes.add('General Security Issue'); moduleResults.results.generalSecurity.issues.forEach(issue => { if (issue.severity) severities.add(issue.severity); if (issue.file) files.add(issue.file); }); } } // SEO modülü if (moduleName === 'seo') { if (moduleResults.results.metaTags && moduleResults.results.metaTags.issues) { issueTypes.add('Meta Tag Issue'); moduleResults.results.metaTags.issues.forEach(issue => { if (issue.file) files.add(issue.file); }); } if (moduleResults.results.semanticHtml && moduleResults.results.semanticHtml.issues) { issueTypes.add('Semantic HTML Issue'); moduleResults.results.semanticHtml.issues.forEach(issue => { if (issue.file) files.add(issue.file); }); } if (moduleResults.results.accessibility && moduleResults.results.accessibility.issues) { issueTypes.add('Accessibility Issue'); moduleResults.results.accessibility.issues.forEach(issue => { if (issue.file) files.add(issue.file); }); } } } } return { modules: moduleNames, issueTypes: Array.from(issueTypes), severities: Array.from(severities), files: Array.from(files) }; }, /** * Toplam sorun sayısını hesaplar * @param {Object} allModuleResults - Tüm modül sonuçları * @returns {number} - Toplam sorun sayısı */ countTotalIssues(allModuleResults) { let totalIssues = 0; for (const moduleName in allModuleResults) { const moduleResults = allModuleResults[moduleName]; if (moduleResults.metadata && moduleResults.metadata.totalIssues) { totalIssues += moduleResults.metadata.totalIssues; } else if (moduleResults.results) { // Component modülü if (moduleName === 'component' && moduleResults.results.serverComponents) { totalIssues += moduleResults.results.serverComponents.length; } // Code quality modülü if (moduleName === 'code-quality' && moduleResults.results.unusedComponents) { totalIssues += moduleResults.results.unusedComponents.length; } // Security modülü if (moduleName === 'security') { if (moduleResults.results.serverComponentSecurity && moduleResults.results.serverComponentSecurity.issues) { totalIssues += moduleResults.results.serverComponentSecurity.issues.length; } if (moduleResults.results.apiRouteSecurity && moduleResults.results.apiRouteSecurity.issues) { totalIssues += moduleResults.results.apiRouteSecurity.issues.length; } if (moduleResults.results.generalSecurity && moduleResults.results.generalSecurity.issues) { totalIssues += moduleResults.results.generalSecurity.issues.length; } } // SEO modülü if (moduleName === 'seo') { if (moduleResults.results.metaTags && moduleResults.results.metaTags.issues) { totalIssues += moduleResults.results.metaTags.issues.length; } if (moduleResults.results.semanticHtml && moduleResults.results.semanticHtml.issues) { totalIssues += moduleResults.results.semanticHtml.issues.length; } if (moduleResults.results.accessibility && moduleResults.results.accessibility.issues) { totalIssues += moduleResults.results.accessibility.issues.length; } } } } return totalIssues; }, /** * Görselleştirme fonksiyonları */ visualize: { /** * Metin formatında görselleştirme * @param {Object} results - Analiz sonuçları * @returns {string} - Metin formatında görselleştirme */ text(results) { let output = `# ${i18n.t('modules.visualization.visualize.title')}\n\n`; // Özet output += `## ${i18n.t('modules.visualization.visualize.summary')}\n\n`; output += `${i18n.t('modules.visualization.summary.totalModules', { totalModules: results.metadata.totalModules })} ve ${i18n.t('modules.visualization.summary.totalIssues', { totalIssues: results.metadata.totalIssues })}\n\n`; // Modül başına sorun sayıları output += `## ${i18n.t('modules.visualization.visualize.moduleIssues')}\n\n`; for (const [moduleName, issueCount] of Object.entries(results.results.chartData.issueCountsByModule)) { output += `- ${moduleName}: ${issueCount} sorun\n`; } output += '\n'; // Sorun türü dağılımı output += `## ${i18n.t('modules.visualization.visualize.issueTypeDistribution')}\n\n`; for (const [issueType, count] of Object.entries(results.results.chartData.issueTypeDistribution)) { const translatedIssueType = i18n.t(`modules.visualization.issueTypes.${issueType.toLowerCase().replace(/\s+/g, '')}`, { defaultValue: issueType }); output += `- ${translatedIssueType}: ${count} sorun\n`; } output += '\n'; // Önem derecesi dağılımı if (Object.keys(results.results.chartData.severityDistribution).length > 0) { output += `## ${i18n.t('modules.visualization.visualize.severityDistribution')}\n\n`; for (const [severity, count] of Object.entries(results.results.chartData.severityDistribution)) { output += `- ${severity}: ${count} sorun\n`; } output += '\n'; } // Filtreleme seçenekleri output += `## ${i18n.t('modules.visualization.visualize.filterOptions')}\n\n`; output += `### ${i18n.t('modules.visualization.visualize.modules')}\n\n`; results.results.filterOptions.modules.forEach(module => { output += `- ${module}\n`; }); output += `\n### ${i18n.t('modules.visualization.visualize.issueTypes')}\n\n`; results.results.filterOptions.issueTypes.forEach(issueType => { const translatedIssueType = i18n.t(`modules.visualization.issueTypes.${issueType.toLowerCase().replace(/\s+/g, '')}`, { defaultValue: issueType }); output += `- ${translatedIssueType}\n`; }); if (results.results.filterOptions.severities.length > 0) { output += `\n### ${i18n.t('modules.visualization.visualize.severities')}\n\n`; results.results.filterOptions.severities.forEach(severity => { output += `- ${severity}\n`; }); } output += `\n### ${i18n.t('modules.visualization.visualize.files')}\n\n`; results.results.filterOptions.files.slice(0, 10).forEach(file => { output += `- ${file}\n`; }); if (results.results.filterOptions.files.length > 10) { output += `- ${i18n.t('modules.visualization.visualize.andMore', { count: results.results.filterOptions.files.length - 10 })}\n`; } return output; }, /** * HTML formatında görselleştirme * @param {Object} results - Analiz sonuçları * @returns {string} - HTML formatında görselleştirme */ html(results) { // Chart.js için veri hazırla const moduleNames = Object.keys(results.results.chartData.issueCountsByModule); const issueCounts = Object.values(results.results.chartData.issueCountsByModule); const issueTypes = Object.keys(results.results.chartData.issueTypeDistribution); const issueTypeCounts = Object.values(results.results.chartData.issueTypeDistribution); const severities = Object.keys(results.results.chartData.severityDistribution); const severityCounts = Object.values(results.results.chartData.severityDistribution); // Renk paleti const colors = [ '#0070f3', '#ff0080', '#f5a623', '#7928ca', '#00a8ff', '#ff4d4d', '#50e3c2', '#3291ff', '#ff0080', '#7928ca' ]; // Modül başına sorun sayıları için veri const moduleChartData = { labels: moduleNames, datasets: [{ label: 'Sorun Sayısı', data: issueCounts, backgroundColor: colors.slice(0, moduleNames.length) }] }; // Sorun türü dağılımı için veri const issueTypeChartData = { labels: issueTypes, datasets: [{ label: 'Sorun Sayısı', data: issueTypeCounts, backgroundColor: colors.slice(0, issueTypes.length) }] }; // Önem derecesi dağılımı için veri const severityChartData = { labels: severities, datasets: [{ label: 'Sorun Sayısı', data: severityCounts, backgroundColor: { 'critical': '#ff4d4d', 'high': '#f5a623', 'medium': '#ffcc00', 'low': '#50e3c2' } }] }; // HTML oluştur let html = ` <div class="visualization-container"> <h2>${i18n.t('modules.visualization.visualize.title')}</h2> <!-- Özet --> <div class="section"> <h3>${i18n.t('modules.visualization.visualize.summary')}</h3> <div class="summary"> <p>${i18n.t('modules.visualization.summary.totalModules', { totalModules: `<strong>${results.metadata.totalModules}</strong>` })} ve ${i18n.t('modules.visualization.summary.totalIssues', { totalIssues: `<strong>${results.metadata.totalIssues}</strong>` })}</p> </div> </div> <!-- Grafikler --> <div class="section"> <h3>Grafikler</h3> <div class="chart-container"> <h4>${i18n.t('modules.visualization.charts.moduleIssues.title')}</h4> <div class="chart"> <canvas id="moduleChart"></canvas> </div> </div> <div class="chart-container"> <h4>${i18n.t('modules.visualization.charts.issueTypes.title')}</h4> <div class="chart"> <canvas id="issueTypeChart"></canvas> </div> </div>`; if (severities.length > 0) { html += ` <div class="chart-container"> <h4>${i18n.t('modules.visualization.charts.severityDistribution.title')}</h4> <div class="chart"> <canvas id="severityChart"></canvas> </div> </div>`; } html += ` </div> <!-- Filtreleme --> <div class="section"> <h3>${i18n.t('modules.visualization.filtering.title')}</h3> <div class="filter-container"> <div class="filter-group"> <label for="moduleFilter">${i18n.t('modules.visualization.filtering.module')}</label> <select id="moduleFilter" class="filter-select"> <option value="all">Tümü</option>`; results.results.filterOptions.modules.forEach(module => { html += ` <option value="${module}">${module}</option>`; }); html += ` </select> </div> <div class="filter-group"> <label for="issueTypeFilter">${i18n.t('modules.visualization.filtering.issueType')}</label> <select id="issueTypeFilter" class="filter-select"> <option value="all">Tümü</option>`; results.results.filterOptions.issueTypes.forEach(issueType => { html += ` <option value="${issueType}">${issueType}</option>`; }); html += ` </select> </div>`; if (results.results.filterOptions.severities.length > 0) { html += ` <div class="filter-group"> <label for="severityFilter">${i18n.t('modules.visualization.filtering.severity')}</label> <select id="severityFilter" class="filter-select"> <option value="all">Tümü</option>`; results.results.filterOptions.severities.forEach(severity => { html += ` <option value="${severity}">${severity}</option>`; }); html += ` </select> </div>`; } html += ` <div class="filter-group"> <label for="fileFilter">${i18n.t('modules.visualization.filtering.file')}</label> <select id="fileFilter" class="filter-select"> <option value="all">Tümü</option>`; results.results.filterOptions.files.forEach(file => { html += ` <option value="${file}">${file}</option>`; }); html += ` </select> </div> <button id="applyFilter" class="filter-button">${i18n.t('modules.visualization.filtering.applyFilter')}</button> </div> <div id="filteredResults" class="filtered-results"> <p>${i18n.t('modules.visualization.filtering.instructions')}</p> </div> </div> <!-- Chart.js --> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> // Modül başına sorun sayıları grafiği const moduleChartCtx = document.getElementById('moduleChart').getContext('2d'); new Chart(moduleChartCtx, { type: 'bar', data: ${JSON.stringify(moduleChartData)}, options: { responsive: true, plugins: { legend: { display: false }, title: { display: true, text: 'Modül Başına Sorun Sayıları' } } } }); // Sorun türü dağılımı grafiği const issueTypeChartCtx = document.getElementById('issueTypeChart').getContext('2d'); new Chart(issueTypeChartCtx, { type: 'pie', data: ${JSON.stringify(issueTypeChartData)}, options: { responsive: true, plugins: { legend: { position: 'right' }, title: { display: true, text: 'Sorun Türü Dağılımı' } } } });`; if (severities.length > 0) { html += ` // Önem derecesi dağılımı grafiği const severityChartCtx = document.getElementById('severityChart').getContext('2d'); new Chart(severityChartCtx, { type: 'pie', data: ${JSON.stringify(severityChartData)}, options: { responsive: true, plugins: { legend: { position: 'right' }, title: { display: true, text: 'Önem Derecesi Dağılımı' } } } });`; } html += ` // Filtreleme işlevi document.getElementById('applyFilter').addEventListener('click', function() { const moduleFilter = document.getElementById('moduleFilter').value; const issueTypeFilter = document.getElementById('issueTypeFilter').value; const severityFilter = document.getElementById('severityFilter')?.value; const fileFilter = document.getElementById('fileFilter').value; const filteredResults = document.getElementById('filteredResults'); // i18n.t() fonksiyonu doğrudan JavaScript içinde kullanılamaz // Çünkü i18n modülü sadece Node.js tarafında çalışır, tarayıcıda değil // Bu nedenle, bu değeri önceden HTML içinde gömmeliyiz filteredResults.innerHTML = '<p>Filtreleme sonuçları yükleniyor...</p>'; // Gerçek uygulamada burada AJAX isteği yapılabilir // Bu örnekte sadece filtreleme seçeneklerini gösteriyoruz setTimeout(() => { filteredResults.innerHTML = \` <h4>Filtreleme Sonuçları</h4> <ul> <li><strong>Modül:</strong> \${moduleFilter === 'all' ? 'Tümü' : moduleFilter}</li> <li><strong>Sorun Türü:</strong> \${issueTypeFilter === 'all' ? 'Tümü' : issueTypeFilter}</li> \${severityFilter ? \`<li><strong>Önem Derecesi:</strong> \${severityFilter === 'all' ? 'Tümü' : severityFilter}</li>\` : ''} <li><strong>Dosya:</strong> \${fileFilter === 'all' ? 'Tümü' : fileFilter}</li> </ul> <p>Bu filtreleme kriterlerine göre sonuçlar burada gösterilecektir.</p> \`; }, 500); }); </script> <style> .visualization-container { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } .section { margin-bottom: 30px; } .chart-container { margin-bottom: 30px; padding: 20px; background-color: #f9f9f9; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .chart { height: 300px; } .filter-container { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 20px; padding: 20px; background-color: #f9f9f9; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .filter-group { display: flex; flex-direction: column; min-width: 200px; } .filter-select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-top: 5px; } .filter-button { padding: 8px 16px; background-color: #0070f3; color: white; border: none; border-radius: 4px; cursor: pointer; align-self: flex-end; margin-top: auto; } .filtered-results { padding: 20px; background-color: #f9f9f9; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } </style> </div>`; return html; }, /** * JSON formatında görselleştirme * @param {Object} results - Analiz sonuçları * @returns {string} - JSON formatında görselleştirme */ json(results) { return JSON.stringify(results, null, 2); } } };