@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
JavaScript
"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 = `
<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;
}