UNPKG

@choi2021/next-route-visualizer

Version:

πŸ—ΊοΈ Visualize Next.js routes as an interactive diagram with advanced function analysis. Supports dynamic route detection, function-based navigation patterns, and comprehensive Next.js route mapping. Generate JSON data and beautiful HTML reports to explore

517 lines (460 loc) β€’ 16.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateHTMLReport = generateHTMLReport; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const child_process_1 = require("child_process"); const routeParser_js_1 = require("../parser/routeParser.js"); function generateHTMLReport(projectPath, outputFile, routesFile) { return __awaiter(this, void 0, void 0, function* () { try { console.log("πŸ”„ 라우트 뢄석 쀑..."); // 라우트 데이터 νŒŒμ‹± - routesFile μ˜΅μ…˜ μΆ”κ°€ const routeData = yield (0, routeParser_js_1.parseNextJsRoutes)(projectPath, { analyzeNavigation: true, routesFile, }); console.log("πŸ“Š HTML 리포트 생성 쀑..."); // HTML 생성 const htmlContent = generateHTML(routeData); // 파일 μ €μž₯ const outputPath = path.resolve(outputFile); fs.writeFileSync(outputPath, htmlContent); console.log(`βœ… HTML λ¦¬ν¬νŠΈκ°€ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€: ${outputFile}`); console.log(`πŸ“Š 총 ${routeData.nodes.length}개 νŽ˜μ΄μ§€, ${routeData.edges.length}개 μ—°κ²° 뢄석 μ™„λ£Œ`); // λΈŒλΌμš°μ €μ—μ„œ μ—΄κΈ° openInBrowser(outputPath); } catch (error) { console.error(`❌ 리포트 생성 쀑 였λ₯˜: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }); } function openInBrowser(filePath) { const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; (0, child_process_1.exec)(`${command} "${filePath}"`, (error) => { if (error) { console.log(`⚠️ λΈŒλΌμš°μ €λ₯Ό μžλ™μœΌλ‘œ μ—΄ 수 μ—†μŠ΅λ‹ˆλ‹€. μˆ˜λ™μœΌλ‘œ ${path.basename(filePath)} νŒŒμΌμ„ μ—¬μ„Έμš”.`); } else { console.log("🌐 λΈŒλΌμš°μ €μ—μ„œ 리포트λ₯Ό μ—΄μ—ˆμŠ΅λ‹ˆλ‹€!"); } }); } function generateHTML(data) { // μ—°κ²° 정보 ꡬ성 const pageConnections = {}; data.nodes.forEach((node) => { pageConnections[node.path] = Object.assign(Object.assign({}, node), { outgoing: [], incoming: [] }); }); data.edges.forEach((edge) => { if (pageConnections[edge.source]) { pageConnections[edge.source].outgoing.push(edge); } if (pageConnections[edge.target]) { pageConnections[edge.target].incoming.push(edge); } }); const templateHTML = `<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Next.js Route Map - Interactive Report</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; color: #333; } .container { max-width: 1400px; margin: 0 auto; padding: 20px; display: grid; grid-template-columns: 350px 1fr; gap: 20px; min-height: 100vh; } .sidebar { background: white; border-radius: 16px; padding: 20px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); backdrop-filter: blur(10px); height: fit-content; position: sticky; top: 20px; } .main-content { background: white; border-radius: 16px; padding: 30px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); backdrop-filter: blur(10px); min-height: calc(100vh - 40px); } .header { text-align: center; margin-bottom: 30px; } .title { font-size: 2.2rem; font-weight: 700; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 10px; } .subtitle { color: #666; font-size: 1.1rem; } .stats-box { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 16px; border-radius: 12px; margin-bottom: 20px; text-align: center; } .stats-number { font-size: 2rem; font-weight: 700; margin-bottom: 4px; } .stats-label { font-size: 0.9rem; opacity: 0.9; } .page-list { max-height: 70vh; overflow-y: auto; } .page-item { padding: 12px 16px; border: 1px solid #e1e5e9; border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.3s; display: flex; justify-content: space-between; align-items: center; } .page-item:hover { border-color: #667eea; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); } .page-item.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-color: transparent; } .page-path { font-weight: 500; font-size: 14px; } .connection-badge { background: #f1f3f4; color: #333; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; } .page-item.active .connection-badge { background: rgba(255,255,255,0.2); color: white; } .page-detail { display: none; } .page-detail.active { display: block; } .detail-header { border-bottom: 2px solid #e1e5e9; padding-bottom: 20px; margin-bottom: 30px; } .detail-title { font-size: 1.8rem; font-weight: 700; color: #333; margin-bottom: 8px; } .detail-meta { color: #666; font-size: 14px; } .connections-section { margin-bottom: 40px; } .section-title { font-size: 1.3rem; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; } .connection-cards { display: grid; gap: 12px; } .connection-card { background: #f8f9fa; border: 1px solid #e1e5e9; border-radius: 12px; padding: 16px; transition: all 0.3s; cursor: pointer; } .connection-card:hover { border-color: #667eea; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); } .connection-path { font-weight: 600; color: #333; margin-bottom: 8px; } .route-variable { background: #e3f2fd; color: #1976d2; padding: 3px 8px; border-radius: 8px; font-size: 11px; font-weight: 500; margin-bottom: 8px; display: inline-block; } .connection-info { display: flex; justify-content: space-between; align-items: center; } .method-tag { padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: 600; color: white; text-transform: uppercase; } .method-link { background: #3b82f6; } .method-router { background: #8b5cf6; } .method-component { background: #f59e0b; } .method-replace { background: #ef4444; } .method-push { background: #10b981; } .empty-state { text-align: center; padding: 60px 20px; color: #666; } .empty-icon { font-size: 3rem; margin-bottom: 16px; } .no-connections { text-align: center; padding: 40px; color: #666; background: #f8f9fa; border-radius: 12px; } @media (max-width: 768px) { .container { grid-template-columns: 1fr; gap: 16px; padding: 16px; } .sidebar { position: static; order: 2; } } </style> </head> <body> <div class="container"> <div class="sidebar"> <div class="header"> <div class="title">πŸ—ΊοΈ</div> <div class="subtitle">Route Explorer</div> </div> <div class="stats-box"> <div class="stats-number">${data.nodes.length}</div> <div class="stats-label">Total Pages</div> </div> <div class="page-list" id="pageList"> <!-- νŽ˜μ΄μ§€ λͺ©λ‘μ΄ 여기에 μƒμ„±λ©λ‹ˆλ‹€ --> </div> </div> <div class="main-content"> <div class="empty-state" id="emptyState"> <div class="empty-icon">πŸ‘ˆ</div> <h3>νŽ˜μ΄μ§€λ₯Ό μ„ νƒν•΄μ£Όμ„Έμš”</h3> <p>μ™Όμͺ½ λͺ©λ‘μ—μ„œ νŽ˜μ΄μ§€λ₯Ό ν΄λ¦­ν•˜λ©΄<br>μ—°κ²° 정보λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.</p> </div> <div id="pageDetails"> <!-- νŽ˜μ΄μ§€ 상세 정보가 여기에 μƒμ„±λ©λ‹ˆλ‹€ --> </div> </div> </div> <script> const routeData = ${JSON.stringify(data)}; let currentPage = null; let pageConnections = {}; // 데이터 μ΄ˆκΈ°ν™” function initializeData() { // μ—°κ²° 정보 ꡬ성 routeData.nodes.forEach(node => { pageConnections[node.path] = { ...node, outgoing: [], incoming: [] }; }); routeData.edges.forEach(edge => { if (pageConnections[edge.source]) { pageConnections[edge.source].outgoing.push(edge); } if (pageConnections[edge.target]) { pageConnections[edge.target].incoming.push(edge); } }); // νŽ˜μ΄μ§€ λͺ©λ‘ λ Œλ”λ§ renderPageList(); } function renderPageList() { const pageList = document.getElementById('pageList'); const sortedPages = Object.values(pageConnections).sort((a, b) => { const aTotal = a.outgoing.length; const bTotal = b.outgoing.length; return bTotal - aTotal; }); pageList.innerHTML = sortedPages.map(page => { const outgoingCount = page.outgoing.length; return \` <div class="page-item" data-path="\${page.path}" onclick="selectPage('\${page.path}')"> <div class="page-path">\${page.path}</div> <div class="connection-badge">\${outgoingCount}</div> </div> \`; }).join(''); } function selectPage(pagePath) { currentPage = pagePath; // νŽ˜μ΄μ§€ 선택 μƒνƒœ μ—…λ°μ΄νŠΈ document.querySelectorAll('.page-item').forEach(item => { item.classList.remove('active'); }); document.querySelector(\`[data-path="\${pagePath}"]\`).classList.add('active'); // 빈 μƒνƒœ 숨기기 document.getElementById('emptyState').style.display = 'none'; // νŽ˜μ΄μ§€ 상세 정보 λ Œλ”λ§ renderPageDetails(pageConnections[pagePath]); } function renderPageDetails(page) { const pageDetails = document.getElementById('pageDetails'); pageDetails.innerHTML = \` <div class="detail-header"> <div class="detail-title">\${page.path}</div> <div class="detail-meta"> πŸ“ \${page.filePath} β€’ πŸ“ \${(page.fileSize / 1024).toFixed(1)}KB </div> </div> \${page.outgoing.length > 0 ? \` <div class="connections-section"> <div class="section-title"> πŸ“€ 이 νŽ˜μ΄μ§€μ—μ„œ 이동 (\${page.outgoing.length}개) </div> <div class="connection-cards"> \${page.outgoing.map(edge => \` <div class="connection-card" onclick="selectPage('\${edge.target}')"> <div class="connection-path">\${edge.target}</div> \${edge.routeVariable ? \` <div class="route-variable">πŸ“ \${edge.routeVariable}</div> \` : ''} <div class="connection-info"> <span class="method-tag method-\${edge.method.replace('.', '')}">\${edge.method}</span> </div> </div> \`).join('')} </div> </div> \` : ''} \${page.outgoing.length === 0 ? \` <div class="no-connections"> <div style="font-size: 2rem; margin-bottom: 16px;">🏝️</div> <h3>이동 κ°€λŠ₯ν•œ νŽ˜μ΄μ§€κ°€ μ—†μŠ΅λ‹ˆλ‹€</h3> <p>이 νŽ˜μ΄μ§€μ—μ„œ λ‹€λ₯Έ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•˜λŠ” λ§ν¬λ‚˜ λ„€λΉ„κ²Œμ΄μ…˜μ΄ μ—†μŠ΅λ‹ˆλ‹€.</p> </div> \` : ''} \`; } // μ΄ˆκΈ°ν™” initializeData(); </script> </body> </html>`; return templateHTML; }