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.

804 lines (682 loc) 25.6 kB
const fs = require('fs-extra'); const path = require('path'); const { execSync } = require('child_process'); const { logError, logInfo, i18n } = require('../../utils'); /** * Zaman İçinde Değişim Analizi Modülü */ module.exports = { name: i18n.t('modules.history.name'), description: i18n.t('modules.history.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) { // Git geçmişini kontrol et const hasGit = this.checkGitRepository(analyzer.projectPath); if (!hasGit) { return { results: { error: i18n.t('modules.history.error.noGit') }, metadata: { hasGit: false } }; } // Commit geçmişini al const commitHistory = this.getCommitHistory(analyzer.projectPath); // Versiyon karşılaştırması yap const versionComparison = await this.compareVersions(analyzer, commitHistory, options); // Trend analizi yap const trendAnalysis = this.analyzeTrends(versionComparison); return { results: { commitHistory, versionComparison, trendAnalysis }, metadata: { hasGit: true, totalCommits: commitHistory.length, analyzedCommits: versionComparison.length, firstCommitDate: commitHistory.length > 0 ? commitHistory[commitHistory.length - 1].date : null, lastCommitDate: commitHistory.length > 0 ? commitHistory[0].date : null } }; }, /** * Git deposunu kontrol eder * @param {string} projectPath - Proje yolu * @returns {boolean} - Git deposu var mı? */ checkGitRepository(projectPath) { try { const gitDir = path.join(projectPath, '.git'); return fs.existsSync(gitDir); } catch (error) { return false; } }, /** * Commit geçmişini alır * @param {string} projectPath - Proje yolu * @returns {Array<Object>} - Commit geçmişi */ getCommitHistory(projectPath) { try { // Son 20 commit'i al const command = 'git log -n 20 --pretty=format:"%h|%an|%ad|%s" --date=short'; const output = execSync(command, { cwd: projectPath, encoding: 'utf8' }); return output.split('\n').map(line => { const [hash, author, date, message] = line.split('|'); return { hash, author, date, message }; }); } catch (error) { logError('Commit geçmişi alınırken hata oluştu:', error); return []; } }, /** * Versiyonları karşılaştırır * @param {NextJsAnalyzer} analyzer - Analyzer instance * @param {Array<Object>} commitHistory - Commit geçmişi * @param {Object} options - Analiz seçenekleri * @returns {Array<Object>} - Versiyon karşılaştırması */ async compareVersions(analyzer, commitHistory, options) { const versionComparison = []; // Analiz edilecek commit sayısını belirle const maxCommits = options.maxCommits || 5; const commitsToAnalyze = commitHistory.slice(0, Math.min(maxCommits, commitHistory.length)); // Mevcut durumu kaydet const currentBranch = this.getCurrentBranch(analyzer.projectPath); try { // Her commit için analiz yap for (const commit of commitsToAnalyze) { logInfo(`${commit.hash} commit'i analiz ediliyor...`); // Commit'e geçiş yap this.checkoutCommit(analyzer.projectPath, commit.hash); // Analiz sonuçlarını al const analysisResults = await this.analyzeCommit(analyzer, commit); versionComparison.push({ commit, results: analysisResults }); } } catch (error) { logError('Versiyon karşılaştırması yapılırken hata oluştu:', error); } finally { // Orijinal branch'e geri dön this.checkoutBranch(analyzer.projectPath, currentBranch); } return versionComparison; }, /** * Mevcut branch'i alır * @param {string} projectPath - Proje yolu * @returns {string} - Mevcut branch */ getCurrentBranch(projectPath) { try { const command = 'git rev-parse --abbrev-ref HEAD'; return execSync(command, { cwd: projectPath, encoding: 'utf8' }).trim(); } catch (error) { logError('Mevcut branch alınırken hata oluştu:', error); return 'master'; // Varsayılan olarak master'a dön } }, /** * Belirli bir commit'e geçiş yapar * @param {string} projectPath - Proje yolu * @param {string} commitHash - Commit hash'i */ checkoutCommit(projectPath, commitHash) { try { const command = `git checkout ${commitHash} --force`; execSync(command, { cwd: projectPath, encoding: 'utf8' }); } catch (error) { logError(`${commitHash} commit'ine geçiş yapılırken hata oluştu:`, error); throw error; } }, /** * Belirli bir branch'e geçiş yapar * @param {string} projectPath - Proje yolu * @param {string} branch - Branch adı */ checkoutBranch(projectPath, branch) { try { const command = `git checkout ${branch} --force`; execSync(command, { cwd: projectPath, encoding: 'utf8' }); } catch (error) { logError(`${branch} branch'ine geçiş yapılırken hata oluştu:`, error); } }, /** * Belirli bir commit'i analiz eder * @param {NextJsAnalyzer} analyzer - Analyzer instance * @param {Object} commit - Commit bilgisi * @returns {Object} - Analiz sonuçları */ async analyzeCommit(analyzer, commit) { try { // Yeni bir analyzer instance oluştur const commitAnalyzer = analyzer.clone(); // Analiz işlemini başlat await commitAnalyzer.analyze(); // Temel metrikleri topla const metrics = { componentCount: commitAnalyzer.components.size, serverComponentCount: Array.from(commitAnalyzer.components.values()).filter(c => c.type === 'server').length, clientComponentCount: Array.from(commitAnalyzer.components.values()).filter(c => c.type === 'client').length, routeCount: 0, apiRouteCount: 0, pageRouteCount: 0, dynamicRouteCount: 0, staticRouteCount: 0 }; // Route sayılarını hesapla if (commitAnalyzer.appDir) { const appFiles = fs.existsSync(commitAnalyzer.appDir) ? fs.readdirSync(commitAnalyzer.appDir, { recursive: true }) : []; metrics.routeCount += appFiles.filter(file => file.endsWith('page.js') || file.endsWith('page.tsx') || file.endsWith('route.js') || file.endsWith('route.tsx') ).length; metrics.apiRouteCount += appFiles.filter(file => file.endsWith('route.js') || file.endsWith('route.tsx') ).length; metrics.pageRouteCount += appFiles.filter(file => file.endsWith('page.js') || file.endsWith('page.tsx') ).length; metrics.dynamicRouteCount += appFiles.filter(file => (file.includes('[') && file.includes(']')) && (file.endsWith('page.js') || file.endsWith('page.tsx') || file.endsWith('route.js') || file.endsWith('route.tsx')) ).length; } if (commitAnalyzer.pagesDir) { const pagesFiles = fs.existsSync(commitAnalyzer.pagesDir) ? fs.readdirSync(commitAnalyzer.pagesDir, { recursive: true }) : []; metrics.routeCount += pagesFiles.filter(file => (file.endsWith('.js') || file.endsWith('.tsx')) && !file.startsWith('_') ).length; metrics.apiRouteCount += pagesFiles.filter(file => (file.startsWith('api/') || file.includes('/api/')) && (file.endsWith('.js') || file.endsWith('.tsx')) ).length; metrics.pageRouteCount += pagesFiles.filter(file => !(file.startsWith('api/') || file.includes('/api/')) && !file.startsWith('_') && (file.endsWith('.js') || file.endsWith('.tsx')) ).length; metrics.dynamicRouteCount += pagesFiles.filter(file => (file.includes('[') && file.includes(']')) && (file.endsWith('.js') || file.endsWith('.tsx')) ).length; } metrics.staticRouteCount = metrics.routeCount - metrics.dynamicRouteCount; return { metrics, analyzer: commitAnalyzer }; } catch (error) { logError(`${commit.hash} commit'i analiz edilirken hata oluştu:`, error); return { error: error.message }; } }, /** * Trend analizi yapar * @param {Array<Object>} versionComparison - Versiyon karşılaştırması * @returns {Object} - Trend analizi */ analyzeTrends(versionComparison) { // Trend analizi için versiyon karşılaştırmasını ters çevir (en eskiden en yeniye) const sortedVersions = [...versionComparison].reverse(); // Metrik değişimlerini hesapla const metricChanges = {}; const metrics = [ 'componentCount', 'serverComponentCount', 'clientComponentCount', 'routeCount', 'apiRouteCount', 'pageRouteCount', 'dynamicRouteCount', 'staticRouteCount' ]; // Her metrik için değişimleri hesapla metrics.forEach(metric => { metricChanges[metric] = []; for (let i = 0; i < sortedVersions.length; i++) { const version = sortedVersions[i]; if (version.results && version.results.metrics) { const value = version.results.metrics[metric]; // Değişim yüzdesini hesapla let changePercent = 0; if (i > 0 && sortedVersions[i-1].results && sortedVersions[i-1].results.metrics) { const prevValue = sortedVersions[i-1].results.metrics[metric]; if (prevValue > 0) { changePercent = ((value - prevValue) / prevValue) * 100; } } metricChanges[metric].push({ commit: version.commit, value, changePercent: i > 0 ? changePercent : 0 }); } } }); // Büyüme trendlerini analiz et const growthTrends = {}; metrics.forEach(metric => { const values = metricChanges[metric].map(change => change.value); if (values.length >= 2) { const firstValue = values[0]; const lastValue = values[values.length - 1]; const totalGrowth = lastValue - firstValue; const totalGrowthPercent = firstValue > 0 ? ((lastValue - firstValue) / firstValue) * 100 : 0; // Büyüme hızını hesapla const growthRate = totalGrowthPercent / (values.length - 1); // Büyüme trendini belirle let trend = 'stable'; if (growthRate > 10) trend = 'rapid-growth'; else if (growthRate > 5) trend = 'steady-growth'; else if (growthRate < -10) trend = 'rapid-decline'; else if (growthRate < -5) trend = 'steady-decline'; else if (Math.abs(growthRate) <= 2) trend = 'stable'; growthTrends[metric] = { firstValue, lastValue, totalGrowth, totalGrowthPercent, growthRate, trend }; } else { growthTrends[metric] = { trend: 'unknown', reason: 'Yeterli veri yok' }; } }); return { metricChanges, growthTrends }; }, /** * 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) { if (results.results.error) { return `# ${i18n.t('modules.history.visualize.title')}\n\nHata: ${results.results.error}\n`; } let output = `# ${i18n.t('modules.history.visualize.title')}\n\n`; // Özet output += '## Özet\n\n'; output += `Toplam ${results.metadata.totalCommits} commit incelendi, ${results.metadata.analyzedCommits} commit analiz edildi.\n`; output += `İlk commit tarihi: ${results.metadata.firstCommitDate}\n`; output += `Son commit tarihi: ${results.metadata.lastCommitDate}\n\n`; // Commit geçmişi output += '## Commit Geçmişi\n\n'; results.results.commitHistory.slice(0, 10).forEach(commit => { output += `- **${commit.hash}** (${commit.date}) - ${commit.author}: ${commit.message}\n`; }); if (results.results.commitHistory.length > 10) { output += `- ... ve ${results.results.commitHistory.length - 10} commit daha\n`; } output += '\n'; // Metrik değişimleri output += '## Metrik Değişimleri\n\n'; const metrics = [ { key: 'componentCount', name: 'Toplam Komponent Sayısı' }, { key: 'serverComponentCount', name: 'Server Komponent Sayısı' }, { key: 'clientComponentCount', name: 'Client Komponent Sayısı' }, { key: 'routeCount', name: 'Toplam Route Sayısı' }, { key: 'apiRouteCount', name: 'API Route Sayısı' }, { key: 'pageRouteCount', name: 'Sayfa Route Sayısı' }, { key: 'dynamicRouteCount', name: 'Dinamik Route Sayısı' }, { key: 'staticRouteCount', name: 'Statik Route Sayısı' } ]; metrics.forEach(metric => { const changes = results.results.trendAnalysis.metricChanges[metric.key]; const trend = results.results.trendAnalysis.growthTrends[metric.key]; if (changes && changes.length > 0) { output += `### ${metric.name}\n\n`; // Trend özeti if (trend && trend.trend !== 'unknown') { output += `Trend: **${this.getTrendName(trend.trend)}**\n`; output += `İlk değer: ${trend.firstValue}, Son değer: ${trend.lastValue}\n`; output += `Toplam büyüme: ${trend.totalGrowth} (${trend.totalGrowthPercent.toFixed(2)}%)\n`; output += `Büyüme hızı: ${trend.growthRate.toFixed(2)}% / commit\n\n`; } // Değişim detayları output += 'Değişim Detayları:\n\n'; changes.forEach(change => { const changeSymbol = change.changePercent > 0 ? '📈' : (change.changePercent < 0 ? '📉' : '➖'); const changeText = change.changePercent !== 0 ? ` (${change.changePercent > 0 ? '+' : ''}${change.changePercent.toFixed(2)}%)` : ''; output += `- ${change.commit.hash} (${change.commit.date}): ${change.value}${changeText} ${changeSymbol}\n`; }); output += '\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) { if (results.results.error) { return ` <div class="history-container"> <h2>${i18n.t('modules.history.visualize.title')}</h2> <div class="error-message"> <p>Hata: ${results.results.error}</p> </div> </div>`; } // Chart.js için veri hazırla const metrics = [ { key: 'componentCount', name: 'Toplam Komponent Sayısı', color: '#0070f3' }, { key: 'serverComponentCount', name: 'Server Komponent Sayısı', color: '#ff0080' }, { key: 'clientComponentCount', name: 'Client Komponent Sayısı', color: '#f5a623' }, { key: 'routeCount', name: 'Toplam Route Sayısı', color: '#7928ca' }, { key: 'apiRouteCount', name: 'API Route Sayısı', color: '#00a8ff' }, { key: 'pageRouteCount', name: 'Sayfa Route Sayısı', color: '#ff4d4d' }, { key: 'dynamicRouteCount', name: 'Dinamik Route Sayısı', color: '#50e3c2' }, { key: 'staticRouteCount', name: 'Statik Route Sayısı', color: '#3291ff' } ]; // Grafik verilerini hazırla const chartData = {}; metrics.forEach(metric => { const changes = results.results.trendAnalysis.metricChanges[metric.key]; if (changes && changes.length > 0) { chartData[metric.key] = { labels: changes.map(change => change.commit.date), datasets: [{ label: metric.name, data: changes.map(change => change.value), borderColor: metric.color, backgroundColor: `${metric.color}33`, borderWidth: 2, fill: true, tension: 0.4 }] }; } }); // HTML oluştur let html = ` <div class="history-container"> <h2>${i18n.t('modules.history.visualize.title')}</h2> <!-- Özet --> <div class="section"> <h3>Özet</h3> <div class="summary"> <p>Toplam <strong>${results.metadata.totalCommits}</strong> commit incelendi, <strong>${results.metadata.analyzedCommits}</strong> commit analiz edildi.</p> <p>İlk commit tarihi: <strong>${results.metadata.firstCommitDate}</strong></p> <p>Son commit tarihi: <strong>${results.metadata.lastCommitDate}</strong></p> </div> </div> <!-- Commit Geçmişi --> <div class="section"> <h3>Commit Geçmişi</h3> <div class="commit-history"> <ul class="commit-list">`; results.results.commitHistory.slice(0, 10).forEach(commit => { html += ` <li class="commit-item"> <span class="commit-hash">${commit.hash}</span> <span class="commit-date">${commit.date}</span> <span class="commit-author">${commit.author}</span> <span class="commit-message">${commit.message}</span> </li>`; }); if (results.results.commitHistory.length > 10) { html += ` <li class="commit-item more-commits"> ... ve ${results.results.commitHistory.length - 10} commit daha </li>`; } html += ` </ul> </div> </div> <!-- Metrik Değişimleri --> <div class="section"> <h3>Metrik Değişimleri</h3>`; metrics.forEach(metric => { const changes = results.results.trendAnalysis.metricChanges[metric.key]; const trend = results.results.trendAnalysis.growthTrends[metric.key]; if (changes && changes.length > 0) { html += ` <div class="metric-section"> <h4>${metric.name}</h4>`; // Trend özeti if (trend && trend.trend !== 'unknown') { const trendClass = this.getTrendClass(trend.trend); const trendName = this.getTrendName(trend.trend); html += ` <div class="trend-summary ${trendClass}"> <p class="trend-label">Trend: <strong>${trendName}</strong></p> <p>İlk değer: <strong>${trend.firstValue}</strong>, Son değer: <strong>${trend.lastValue}</strong></p> <p>Toplam büyüme: <strong>${trend.totalGrowth}</strong> (${trend.totalGrowthPercent.toFixed(2)}%)</p> <p>Büyüme hızı: <strong>${trend.growthRate.toFixed(2)}%</strong> / commit</p> </div>`; } // Grafik html += ` <div class="chart-container"> <canvas id="chart-${metric.key}"></canvas> </div> <div class="change-details"> <h5>Değişim Detayları</h5> <ul class="change-list">`; changes.forEach(change => { const changeClass = change.changePercent > 0 ? 'increase' : (change.changePercent < 0 ? 'decrease' : 'stable'); const changeSymbol = change.changePercent > 0 ? '📈' : (change.changePercent < 0 ? '📉' : '➖'); const changeText = change.changePercent !== 0 ? ` (${change.changePercent > 0 ? '+' : ''}${change.changePercent.toFixed(2)}%)` : ''; html += ` <li class="change-item ${changeClass}"> <span class="change-commit">${change.commit.hash} (${change.commit.date})</span> <span class="change-value">${change.value}${changeText} ${changeSymbol}</span> </li>`; }); html += ` </ul> </div> </div>`; } }); html += ` </div> <!-- Chart.js --> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script>`; // Her metrik için grafik oluştur metrics.forEach(metric => { if (chartData[metric.key]) { html += ` // ${metric.name} grafiği const ${metric.key}ChartCtx = document.getElementById('chart-${metric.key}').getContext('2d'); new Chart(${metric.key}ChartCtx, { type: 'line', data: ${JSON.stringify(chartData[metric.key])}, options: { responsive: true, plugins: { title: { display: true, text: '${metric.name} Değişimi' } }, scales: { y: { beginAtZero: true } } } });`; } }); html += ` </script> <style> .history-container { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } .section { margin-bottom: 30px; } .commit-list { list-style: none; padding: 0; margin: 0; background-color: #f9f9f9; border-radius: 8px; overflow: hidden; } .commit-item { padding: 10px 15px; border-bottom: 1px solid #eaeaea; display: flex; flex-wrap: wrap; gap: 10px; } .commit-item:last-child { border-bottom: none; } .commit-hash { font-family: monospace; color: #0070f3; font-weight: bold; } .commit-date { color: #666; } .commit-author { font-weight: bold; } .metric-section { margin-bottom: 40px; padding: 20px; background-color: #f9f9f9; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .trend-summary { padding: 15px; border-radius: 8px; margin-bottom: 20px; } .trend-rapid-growth { background-color: #d4edda; color: #155724; } .trend-steady-growth { background-color: #e8f5e9; color: #2e7d32; } .trend-stable { background-color: #e8eaf6; color: #3f51b5; } .trend-steady-decline { background-color: #fff3cd; color: #856404; } .trend-rapid-decline { background-color: #f8d7da; color: #721c24; } .chart-container { height: 300px; margin-bottom: 20px; } .change-list { list-style: none; padding: 0; margin: 0; } .change-item { padding: 8px 0; border-bottom: 1px solid #eaeaea; display: flex; justify-content: space-between; } .change-item:last-child { border-bottom: none; } .change-item.increase .change-value { color: #2e7d32; } .change-item.decrease .change-value { color: #c62828; } .change-item.stable .change-value { color: #3f51b5; } .change-commit { font-family: monospace; } .error-message { padding: 20px; background-color: #f8d7da; color: #721c24; border-radius: 8px; } </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); }, /** * Trend sınıfını döndürür * @param {string} trend - Trend * @returns {string} - Trend sınıfı */ getTrendClass(trend) { const classes = { 'rapid-growth': 'trend-rapid-growth', 'steady-growth': 'trend-steady-growth', 'stable': 'trend-stable', 'steady-decline': 'trend-steady-decline', 'rapid-decline': 'trend-rapid-decline', 'unknown': '' }; return classes[trend] || ''; }, /** * Trend adını döndürür * @param {string} trend - Trend * @returns {string} - Trend adı */ getTrendName(trend) { return i18n.t(`modules.history.trends.${trend}`); } } };