@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
1,149 lines โข 73.4 kB
JavaScript
"use strict";
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.RouteParser = void 0;
exports.parseNextJsRoutes = parseNextJsRoutes;
const glob_1 = require("glob");
const path_1 = require("path");
const fs_1 = require("fs");
class RouteParser {
constructor(projectPath, options = {}) {
this.componentUsages = new Map();
this.hookUsages = new Map();
this.routeConstants = new Map();
this.projectPath = (0, path_1.resolve)(projectPath);
this.options = Object.assign({ depth: 10, exclude: ["node_modules", ".git", ".next", "build", "dist"], includeMetadata: true, analyzeNavigation: true }, options);
this.excludePatterns = this.options.exclude || [];
}
parse() {
return __awaiter(this, void 0, void 0, function* () {
console.log(`๐ Parsing Next.js project at: ${this.projectPath}`);
// 1. Next.js ํ๋ก์ ํธ ์ ํจ์ฑ ๊ฒ์ฌ
if (!this.isValidNextJsProject()) {
throw new Error("Invalid Next.js project: package.json or next.config.js not found");
}
// 2. ์ค์ ํ์ด์ง ํ์ผ๋ค๋ง ์ค์บ (์ฐ์ ์์: pages/ > src/pages/)
const pageFiles = yield this.scanPageFiles();
console.log(`๐ Found ${pageFiles.length} page files`);
// 3. ํ์ด์ง ๋
ธ๋๋ค ์์ฑ
const nodes = yield this.createPageNodes(pageFiles);
console.log(`๐ณ Created ${nodes.length} page nodes`);
// 4. ์ปดํฌ๋ํธ ์ฌ์ฉ ๊ด๊ณ ๋ถ์
if (this.options.analyzeNavigation) {
yield this.analyzeComponentUsages(pageFiles);
// 5. Routes ์์ ํ์ผ ๋ถ์ (์ฐ์ ์์: ์ฌ์ฉ์ ์ง์ > ์๋ ๊ฐ์ง)
if (this.options.routesFile) {
yield this.analyzeSpecificRouteFile(this.options.routesFile);
}
else {
yield this.analyzeRouteConstants();
}
// 6. ํ
์ฌ์ฉ ๊ด๊ณ ๋ถ์
yield this.analyzeHookUsages(pageFiles);
}
// 7. ๋ค๋น๊ฒ์ด์
๊ด๊ณ ๋ถ์
const edges = this.options.analyzeNavigation
? yield this.analyzeNavigation(pageFiles)
: [];
console.log(`๐ Found ${edges.length} navigation connections`);
// 8. ํต๊ณ ๊ณ์ฐ
const summary = this.calculateSummary(nodes, edges);
return {
nodes,
edges,
summary,
};
});
}
isValidNextJsProject() {
const packageJsonPath = (0, path_1.join)(this.projectPath, "package.json");
const nextConfigPaths = [
(0, path_1.join)(this.projectPath, "next.config.js"),
(0, path_1.join)(this.projectPath, "next.config.ts"),
(0, path_1.join)(this.projectPath, "next.config.mjs"),
];
if (!(0, fs_1.existsSync)(packageJsonPath)) {
return false;
}
// Check if Next.js is in dependencies
try {
const packageJson = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, "utf-8"));
const hasNext = (packageJson.dependencies && packageJson.dependencies.next) ||
(packageJson.devDependencies && packageJson.devDependencies.next);
if (hasNext)
return true;
}
catch (error) {
console.warn("Could not parse package.json:", error.message);
}
// Check for next.config files
return nextConfigPaths.some((configPath) => (0, fs_1.existsSync)(configPath));
}
scanPageFiles() {
return __awaiter(this, void 0, void 0, function* () {
// ์ฐ์ ์์์ ๋ฐ๋ผ pages ๋๋ ํ ๋ฆฌ ๊ฒฐ์
const pagesRoot = (0, fs_1.existsSync)((0, path_1.join)(this.projectPath, "pages"))
? "pages"
: "src/pages";
const patterns = [`${pagesRoot}/**/*.{js,jsx,ts,tsx}`];
const allFiles = [];
for (const pattern of patterns) {
const files = yield (0, glob_1.glob)(pattern, {
cwd: this.projectPath,
absolute: true,
ignore: [
...this.excludePatterns.map((p) => `**/${p}/**`),
"**/api/**", // API ๋ผ์ฐํธ ์ ์ธ
"**/**/api/**",
],
});
allFiles.push(...files);
}
// Remove duplicates and filter valid page files
const uniqueFiles = [...new Set(allFiles)];
const validFiles = uniqueFiles.filter((file) => this.isValidPageFile(file));
return validFiles;
});
}
scanComponentFiles() {
return __awaiter(this, void 0, void 0, function* () {
const patterns = [
"components/**/*.{js,jsx,ts,tsx}",
"src/components/**/*.{js,jsx,ts,tsx}",
"lib/**/*.{js,jsx,ts,tsx}",
"src/lib/**/*.{js,jsx,ts,tsx}",
"utils/**/*.{js,jsx,ts,tsx}",
"src/utils/**/*.{js,jsx,ts,tsx}",
];
const allFiles = [];
for (const pattern of patterns) {
const files = yield (0, glob_1.glob)(pattern, {
cwd: this.projectPath,
absolute: true,
ignore: this.excludePatterns.map((p) => `**/${p}/**`),
});
allFiles.push(...files);
}
return [...new Set(allFiles)];
});
}
isValidPageFile(filePath) {
const relativePath = (0, path_1.relative)(this.projectPath, filePath);
const fileName = (0, path_1.basename)(filePath, (0, path_1.extname)(filePath));
// API ๋ผ์ฐํธ ์ ์ธ
if (relativePath.includes("/api/") || relativePath.includes("\\api\\")) {
return false;
}
// Next.js ํน์ ํ์ผ๋ค ์ ์ธ
const excludeFiles = [
"_app",
"_document",
"_error",
"404",
"500",
"middleware",
];
if (excludeFiles.includes(fileName)) {
return false;
}
return true;
}
createPageNodes(files) {
return __awaiter(this, void 0, void 0, function* () {
const nodes = [];
for (const filePath of files) {
const node = yield this.createPageNode(filePath);
if (node) {
nodes.push(node);
}
}
// Sort by path for consistent ordering
return nodes.sort((a, b) => a.path.localeCompare(b.path));
});
}
createPageNode(filePath) {
return __awaiter(this, void 0, void 0, function* () {
try {
const relativePath = (0, path_1.relative)(this.projectPath, filePath);
const stats = (0, fs_1.statSync)(filePath);
// Convert file path to route path
const routePath = this.filePathToRoute(relativePath);
const routeType = this.determineRouteType(routePath);
const segments = this.extractRouteSegments(routePath);
return {
id: routePath,
path: routePath,
label: routePath,
type: routeType,
filePath: relativePath,
fileSize: stats.size,
lastModified: stats.mtime,
children: [],
metadata: {
hasParams: this.hasRouteParams(routePath),
isCatchAll: this.isCatchAllRoute(routePath),
isOptionalCatchAll: this.isOptionalCatchAllRoute(routePath),
depth: segments.length,
segments,
},
};
}
catch (error) {
console.warn(`Warning: Could not process file ${filePath}:`, error.message);
return null;
}
});
}
filePathToRoute(filePath) {
let routePath = filePath;
// Remove pages/ or src/pages/ prefix
routePath = routePath.replace(/^(src\/)?pages\//, "");
// Remove file extension
routePath = routePath.replace(/\.(js|jsx|ts|tsx)$/, "");
// Handle index files
if (routePath === "index") {
return "/";
}
if (routePath.endsWith("/index")) {
routePath = routePath.replace(/\/index$/, "");
}
// Ensure starts with /
if (!routePath.startsWith("/")) {
routePath = "/" + routePath;
}
// Handle empty string case (from index processing)
if (routePath === "/" || routePath === "") {
return "/";
}
return routePath;
}
determineRouteType(routePath) {
if (this.hasRouteParams(routePath)) {
return "dynamic";
}
return "page";
}
extractRouteSegments(routePath) {
if (routePath === "/")
return [""];
return routePath.split("/").filter(Boolean);
}
hasRouteParams(routePath) {
return routePath.includes("[") && routePath.includes("]");
}
isCatchAllRoute(routePath) {
return routePath.includes("[...") && routePath.includes("]");
}
isOptionalCatchAllRoute(routePath) {
return routePath.includes("[[...") && routePath.includes("]]");
}
analyzeComponentUsages(pageFiles) {
return __awaiter(this, void 0, void 0, function* () {
const componentFiles = yield this.scanComponentFiles();
// ๊ฐ ์ปดํฌ๋ํธ์ ๋ํด ์ฌ์ฉ์ฒ์ ๋ค๋น๊ฒ์ด์
๋ถ์
for (const componentPath of componentFiles) {
const relativePath = (0, path_1.relative)(this.projectPath, componentPath);
const componentName = (0, path_1.basename)(componentPath, (0, path_1.extname)(componentPath));
try {
const content = (0, fs_1.readFileSync)(componentPath, "utf-8");
// ์ปดํฌ๋ํธ์์ ๋ค๋น๊ฒ์ด์
ํ๊ฒ ์ถ์ถ
const navigationTargets = this.extractNavigationTargets(content);
// ์ด ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ ํ์ด์ง๋ค ์ฐพ๊ธฐ
const usedInPages = [];
for (const pageFile of pageFiles) {
const pageContent = (0, fs_1.readFileSync)(pageFile, "utf-8");
const pageRelativePath = (0, path_1.relative)(this.projectPath, pageFile);
// ์ปดํฌ๋ํธ import ๋๋ ์ฌ์ฉ ํ์ธ
if (this.isComponentUsedInPage(pageContent, componentName)) {
const routePath = this.filePathToRoute(pageRelativePath);
usedInPages.push(routePath);
}
}
if (navigationTargets.length > 0 && usedInPages.length > 0) {
this.componentUsages.set(relativePath, {
componentPath: relativePath,
usedInPages,
navigationTargets,
});
}
}
catch (error) {
console.warn(`Warning: Could not analyze component ${relativePath}:`, error.message);
}
}
});
}
isComponentUsedInPage(pageContent, componentName) {
// import ๋ฌธ ํ์ธ
const importPatterns = [
new RegExp(`import.*${componentName}.*from`, "i"),
new RegExp(`import.*{[^}]*${componentName}[^}]*}.*from`, "i"),
];
// JSX ์ฌ์ฉ ํ์ธ
const usagePatterns = [
new RegExp(`<${componentName}[\\s>]`, "i"),
new RegExp(`<${componentName}/>`, "i"),
];
// import ํ์ธ
const hasImport = importPatterns.some((pattern) => pattern.test(pageContent));
// ์ฌ์ฉ ํ์ธ
const hasUsage = usagePatterns.some((pattern) => pattern.test(pageContent));
return hasImport && hasUsage;
}
extractNavigationTargets(content) {
const targets = [];
const lines = content.split("\n");
lines.forEach((line) => {
// Link components
const linkMatches = line.matchAll(/<Link\s+href=["']([^"']+)["']/g);
for (const match of linkMatches) {
targets.push(match[1]);
}
// router.push calls
const pushMatches = line.matchAll(/router\.push\(["']([^"']+)["']\)/g);
for (const match of pushMatches) {
targets.push(match[1]);
}
// router.replace calls
const replaceMatches = line.matchAll(/router\.replace\(["']([^"']+)["']\)/g);
for (const match of replaceMatches) {
targets.push(match[1]);
}
// useRouter().push calls
const useRouterPushMatches = line.matchAll(/\.push\(["']([^"']+)["']\)/g);
for (const match of useRouterPushMatches) {
targets.push(match[1]);
}
// useRouter().replace calls
const useRouterReplaceMatches = line.matchAll(/\.replace\(["']([^"']+)["']\)/g);
for (const match of useRouterReplaceMatches) {
targets.push(match[1]);
}
});
return [...new Set(targets)]; // ์ค๋ณต ์ ๊ฑฐ
}
analyzeRouteConstants() {
return __awaiter(this, void 0, void 0, function* () {
const routeFiles = yield this.scanRouteConstantFiles();
// ๊ฐ routes ์์ ํ์ผ์ ๋ํด ๋ถ์
for (const routeFilePath of routeFiles) {
const relativePath = (0, path_1.relative)(this.projectPath, routeFilePath);
const fileName = (0, path_1.basename)(routeFilePath, (0, path_1.extname)(routeFilePath));
try {
const content = (0, fs_1.readFileSync)(routeFilePath, "utf-8");
// ์์ ๊ฐ์ฒด๋ค์ ์ถ์ถ
const routeConstants = this.extractRouteConstants(content);
if (routeConstants.size > 0) {
// ๊ฐ์ฅ ๊ฐ๋ฅ์ฑ ๋์ export ๊ฐ์ฒด๋ช
์ถ๋ก
const exportedObject = this.inferExportedObjectName(content, fileName);
this.routeConstants.set(relativePath, {
filePath: relativePath,
constants: routeConstants,
exportedObject,
});
}
}
catch (error) {
console.warn(`Warning: Could not analyze route constants ${relativePath}:`, error.message);
}
}
});
}
/**
* Routes ์์ ํ์ผ๋ค์ ์ค์บํฉ๋๋ค.
*/
scanRouteConstantFiles() {
return __awaiter(this, void 0, void 0, function* () {
const patterns = [
"constants/**/*.{js,jsx,ts,tsx}",
"src/constants/**/*.{js,jsx,ts,tsx}",
"config/**/*.{js,jsx,ts,tsx}",
"src/config/**/*.{js,jsx,ts,tsx}",
"routes/**/*.{js,jsx,ts,tsx}",
"src/routes/**/*.{js,jsx,ts,tsx}",
"lib/routes/**/*.{js,jsx,ts,tsx}",
"src/lib/routes/**/*.{js,jsx,ts,tsx}",
"utils/routes/**/*.{js,jsx,ts,tsx}",
"src/utils/routes/**/*.{js,jsx,ts,tsx}",
// ์ถ๊ฐ: src ํด๋์ routes ๊ด๋ จ ํ์ผ๋ค
"src/**/*route*.{js,jsx,ts,tsx}",
"src/**/*path*.{js,jsx,ts,tsx}",
"src/**/*url*.{js,jsx,ts,tsx}",
];
const allFiles = [];
for (const pattern of patterns) {
const files = yield (0, glob_1.glob)(pattern, {
cwd: this.projectPath,
absolute: true,
ignore: this.excludePatterns.map((p) => `**/${p}/**`),
});
allFiles.push(...files);
}
// routes ๊ด๋ จ ํค์๋๊ฐ ํฌํจ๋ ํ์ผ๋ค๋ง ํํฐ๋ง (๋ ํฌ๊ด์ ์ผ๋ก)
const routeFiles = [...new Set(allFiles)].filter((file) => {
const fileName = (0, path_1.basename)(file, (0, path_1.extname)(file)).toLowerCase();
const fileContent = this.tryReadFileContent(file);
// ํ์ผ๋ช
๊ธฐ๋ฐ ํํฐ๋ง
const routeKeywords = ["route", "path", "url", "endpoint", "navigation"];
const hasRouteKeyword = routeKeywords.some((keyword) => fileName.includes(keyword));
// ํ์ผ ๋ด์ฉ ๊ธฐ๋ฐ ํํฐ๋ง - ROUTES, PATHS ๋ฑ์ export ํ์ธ
const hasRouteExport = fileContent &&
fileContent.includes("export") &&
(fileContent.includes("ROUTES") ||
fileContent.includes("PATHS") ||
fileContent.includes("URLS") ||
/export\s+const\s+\w*[Rr]oute/i.test(fileContent) ||
// V2_Routes, ApiRoutes ๋ฑ ์ถ๊ฐ ํจํด (NEW)
/export\s+const\s+\w*Routes/i.test(fileContent) ||
/export\s+const\s+V\d+_\w*/i.test(fileContent) ||
/export\s+const\s+Api\w*/i.test(fileContent));
return hasRouteKeyword || hasRouteExport;
});
return routeFiles;
});
}
/**
* ํ์ผ ๋ด์ฉ์ ์์ ํ๊ฒ ์ฝ์ด์ค๋ ํฌํผ ๋ฉ์๋
*/
tryReadFileContent(filePath) {
try {
return (0, fs_1.readFileSync)(filePath, "utf-8");
}
catch (_a) {
return null;
}
}
/**
* ํ์ผ ๋ด์ฉ์์ route ์์๋ค์ ์ถ์ถํฉ๋๋ค.
*/
extractRouteConstants(content) {
const constants = new Map();
// ๊ฐ์ฒด ์ ์ ํจํด๋ค - ์ฌ๋ฌ export๋ ๊ฐ์ฒด ๋ชจ๋ ๊ฐ์ง (NEW)
const objectPatterns = [
// export const ROUTES = { HOME: '/', ... }
/export\s+const\s+(\w+)\s*=\s*\{([\s\S]*?)\}/g,
// const ROUTES = { HOME: '/', ... }; export { ROUTES };
/const\s+(\w+)\s*=\s*\{([\s\S]*?)\}/g,
// export default { HOME: '/', ... }
/export\s+default\s*\{([\s\S]*?)\}/g,
];
objectPatterns.forEach((pattern) => {
const matches = content.matchAll(pattern);
for (const match of matches) {
let objectBody = "";
let objectName = "DEFAULT";
if (match.length === 3) {
// Named export: export const ROUTES = { ... }
objectName = match[1];
objectBody = match[2];
// Routes ๊ด๋ จ ๊ฐ์ฒด์ธ์ง ํ์ธ (NEW)
const normalizedName = objectName.toLowerCase();
const isRoutesObject = normalizedName.includes("route") ||
normalizedName.includes("path") ||
normalizedName.includes("url") ||
normalizedName.includes("api") ||
normalizedName.includes("endpoint");
// Routes ๊ด๋ จ ๊ฐ์ฒด๊ฐ ์๋๋ฉด ๊ฑด๋๋ฐ๊ธฐ
if (!isRoutesObject) {
console.log(`โญ๏ธ ๊ฑด๋๋ฐ๊ธฐ: ${objectName} (Routes ๊ฐ์ฒด๊ฐ ์๋)`);
continue;
}
console.log(`๐ Routes ๊ฐ์ฒด ๋ฐ๊ฒฌ: ${objectName}`);
}
else if (match.length === 2) {
// Default export: export default { ... }
objectBody = match[1];
}
// ๊ฐ์ฒด ๋ด๋ถ์ key-value ์๋ค์ ์ถ์ถ
const keyValuePairs = this.extractKeyValuePairs(objectBody);
keyValuePairs.forEach((value, key) => {
const fullKey = objectName === "DEFAULT" ? key : `${objectName}.${key}`;
constants.set(fullKey, value);
// ๋จ์ ํค๋ก๋ ์ ์ฅ (์ค๋ณต ๋ฐฉ์ง๋ฅผ ์ํด ๊ธฐ์กด ๊ฐ์ด ์์ ๋๋ง)
if (!constants.has(key)) {
constants.set(key, value);
}
console.log(`โ
์์ ๋ฑ๋ก: ${fullKey} = ${value}`);
});
}
});
return constants;
}
/**
* ๊ฐ์ฒด ๋ด๋ถ์ key-value ์๋ค์ ์ถ์ถํฉ๋๋ค.
*/
extractKeyValuePairs(objectBody) {
const pairs = new Map();
// key: 'value' ๋๋ key: "value" ํจํด (๊ธฐ์กด)
const keyValuePattern = /(\w+)\s*:\s*['"`]([^'"`]+)['"`]/g;
const matches = objectBody.matchAll(keyValuePattern);
for (const match of matches) {
const key = match[1];
const value = match[2];
// ๊ฒฝ๋ก์ฒ๋ผ ๋ณด์ด๋ ๊ฐ๋ค๋ง ์์ง (/ ๋ก ์์ํ๊ฑฐ๋ ํฌํจ)
if (value.includes("/") || value.startsWith("/")) {
pairs.set(key, value);
console.log(`โ
๋ฆฌํฐ๋ด ์์: ${key} = ${value}`);
}
}
// ํ์ดํ ํจ์ ํจํด (NEW): key: () => 'path'
const arrowFunctionPattern = /(\w+)\s*:\s*\([^)]*\)\s*=>\s*['"`]([^'"`]+)['"`]/g;
const arrowMatches = objectBody.matchAll(arrowFunctionPattern);
for (const match of arrowMatches) {
const key = match[1];
const value = match[2];
if (value.includes("/") || value.startsWith("/")) {
pairs.set(key, value);
console.log(`โ
ํ์ดํ ํจ์ ์์: ${key} = ${value}`);
}
}
// ํจ์ ํธ์ถ ํจํด (๊ธฐ์กด): key: functionName("path", ...)
const functionCallPattern = /(\w+)\s*:\s*(\w+)\s*\(\s*['"`]([^'"`]+)['"`][^)]*\)/g;
const functionMatches = objectBody.matchAll(functionCallPattern);
for (const match of functionMatches) {
const key = match[1];
const firstArg = match[3];
// ํจ์์ ์ฒซ ๋ฒ์งธ ์ธ์๊ฐ ๊ฒฝ๋ก์ธ ๊ฒฝ์ฐ (createRoute, buildPath ๋ฑ)
if (firstArg.includes("/") || firstArg.startsWith("/")) {
pairs.set(key, firstArg);
pairs.set(`${key}`, firstArg); // ํจ์ํ Routes๋ฅผ ์ํด ๊ฐ์ฒด.ํค ํํ๋ ์ ์ฅ
console.log(`โ
ํจ์ ํธ์ถ ์์: ${key} = ${firstArg}`);
}
}
// ๋ ๋ณต์กํ ํจ์ ํธ์ถ ํจํด: key: createRoute("/path", {...})
const complexFunctionPattern = /(\w+)\s*:\s*\w+\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*\{[^}]*\}\s*\)/g;
const complexMatches = objectBody.matchAll(complexFunctionPattern);
for (const match of complexMatches) {
const key = match[1];
const pathArg = match[2];
if (pathArg.includes("/") || pathArg.startsWith("/")) {
pairs.set(key, pathArg);
console.log(`โ
๋ณต์กํ ํจ์ ์์: ${key} = ${pathArg}`);
}
}
return pairs;
}
/**
* Export๋ ๊ฐ์ฒด๋ช
์ ์ถ๋ก ํฉ๋๋ค.
*/
inferExportedObjectName(content, fileName) {
// Named export ํจํด ํ์ธ
const namedExportMatch = content.match(/export\s+const\s+(\w+)\s*=/);
if (namedExportMatch) {
return namedExportMatch[1];
}
// export { ROUTES } ํจํด ํ์ธ
const exportedNameMatch = content.match(/export\s*\{\s*(\w+)\s*\}/);
if (exportedNameMatch) {
return exportedNameMatch[1];
}
// ํ์ผ๋ช
๊ธฐ๋ฐ ์ถ๋ก
const upperFileName = fileName.toUpperCase();
if (upperFileName.includes("ROUTE"))
return "ROUTES";
if (upperFileName.includes("PATH"))
return "PATHS";
if (upperFileName.includes("URL"))
return "URLS";
if (upperFileName.includes("ENDPOINT"))
return "ENDPOINTS";
return "ROUTES"; // ๊ธฐ๋ณธ๊ฐ
}
analyzeNavigation(pageFiles) {
return __awaiter(this, void 0, void 0, function* () {
const edges = [];
console.log(`๐ Analyzing navigation in ${pageFiles.length} page files`);
// 1. ํ์ด์ง์์ ์ง์ ๋ค๋น๊ฒ์ด์
๋ถ์
for (const filePath of pageFiles) {
try {
const content = (0, fs_1.readFileSync)(filePath, "utf-8");
const pageEdges = this.extractNavigationFromFile(filePath, content);
edges.push(...pageEdges);
}
catch (error) {
console.warn(`Warning: Could not analyze navigation in ${filePath}:`, error.message);
}
}
// 2. ์ปดํฌ๋ํธ ์ฌ์ฉ์ผ๋ก ์ธํ ๊ฐ์ ๋ค๋น๊ฒ์ด์
์ถ๊ฐ
for (const [componentPath, usage] of this.componentUsages) {
for (const sourcePage of usage.usedInPages) {
for (const target of usage.navigationTargets) {
edges.push({
source: sourcePage,
target: target,
method: "component",
codeSnippet: `via ${(0, path_1.basename)(componentPath)}`,
lineNumber: 0,
fileName: componentPath,
});
}
}
}
// 3. ํ
์ฌ์ฉ์ผ๋ก ์ธํ ๋ค๋น๊ฒ์ด์
์ถ๊ฐ (์๋ก ์ถ๊ฐ)
for (const [hookPath, usage] of this.hookUsages) {
for (const sourcePage of usage.usedInPages) {
for (const [functionName, targets] of usage.navigationFunctions) {
for (const target of targets) {
edges.push({
source: sourcePage,
target: target,
method: "push", // ํ
์ ๋ค๋น๊ฒ์ด์
์ ์ผ๋ฐ์ ์ผ๋ก push
codeSnippet: `via ${(0, path_1.basename)(hookPath)}.${functionName}()`,
lineNumber: 0,
fileName: hookPath,
});
}
}
}
}
// 4. ์ค์ ์กด์ฌํ๋ ํ์ด์ง๋ก์ ๋งํฌ๋ง ์ ์ง
const validPages = new Set(pageFiles.map((file) => {
const relativePath = (0, path_1.relative)(this.projectPath, file);
return this.filePathToRoute(relativePath);
}));
const validEdges = edges.filter((edge) => {
const sourceExists = validPages.has(edge.source);
const targetExists = validPages.has(edge.target) ||
this.isValidDynamicRoute(edge.target, validPages);
return sourceExists && targetExists;
});
return this.deduplicateEdges(validEdges);
});
}
isValidDynamicRoute(targetPath, validPages) {
// ๋์ ๋ผ์ฐํธ ํจํด๊ณผ ๋งค์นญ๋๋์ง ํ์ธ
for (const validPage of validPages) {
if (validPage.includes("[") && validPage.includes("]")) {
const pattern = validPage.replace(/\[[\w.[\]]*\]/g, "[^/]+");
const regex = new RegExp(`^${pattern}$`);
if (regex.test(targetPath)) {
return true;
}
}
}
return false;
}
extractNavigationFromFile(filePath, content, processedFiles) {
if (!content.trim())
return [];
const relativePath = (0, path_1.relative)(this.projectPath, filePath);
const sourceRoute = this.filePathToRoute(relativePath);
const edges = [];
// ๋ฌดํ ์ฌ๊ท ๋ฐฉ์ง
if (!processedFiles) {
processedFiles = new Set();
}
if (processedFiles.has(filePath)) {
console.log(`๐ Circular re-export detected, skipping: ${filePath}`);
return edges;
}
processedFiles.add(filePath);
// re-export ํจํด ๊ฐ์ง ๋ฐ ์ค์ ํ์ผ ๋ถ์
const isReExport = content.includes("export { default } from") ||
content.includes("export {default} from");
if (isReExport) {
// ๋จ์ ๋ฌธ์์ด ํ์ฑ์ผ๋ก ๊ฒฝ๋ก ์ถ์ถ
let reExportPath = "";
const fromIndex = content.indexOf(" from ");
if (fromIndex !== -1) {
const afterFrom = content.substring(fromIndex + 6).trim();
const quoteMatch = afterFrom.match(/["']([^"']+)["']/);
if (quoteMatch) {
reExportPath = quoteMatch[1];
}
}
if (reExportPath) {
// ํ์ฌ ํ์ผ์ ๋๋ ํฐ๋ฆฌ ๊ฒฝ๋ก ๊ตฌํ๊ธฐ
const currentDir = filePath.substring(0, filePath.lastIndexOf("/"));
// .tsx, .ts, .jsx, .js ํ์ฅ์๋ก ์๋
const possibleExtensions = [".tsx", ".ts", ".jsx", ".js"];
let actualPath = null;
for (const ext of possibleExtensions) {
const tryPath = reExportPath.includes(".")
? (0, path_1.resolve)(currentDir, reExportPath)
: (0, path_1.resolve)(currentDir, reExportPath + ext);
try {
if ((0, fs_1.existsSync)(tryPath)) {
actualPath = tryPath;
break;
}
}
catch (_a) {
// ๊ณ์ ์๋
}
}
if (actualPath) {
try {
const actualContent = (0, fs_1.readFileSync)(actualPath, "utf-8");
const actualEdges = this.extractNavigationFromFile(actualPath, actualContent, processedFiles);
// ์์ค ๊ฒฝ๋ก๋ฅผ ์๋ ํ์ด์ง ํ์ผ๋ก ๋ณ๊ฒฝ
const correctedEdges = actualEdges.map((edge) => (Object.assign(Object.assign({}, edge), { source: sourceRoute, fileName: relativePath })));
edges.push(...correctedEdges);
return edges;
}
catch (error) {
console.warn(`Warning: Could not read re-exported file ${actualPath}:`, error.message);
}
}
}
}
// ๋ฉํฐ๋ผ์ธ Link ์ปดํฌ๋ํธ ๊ฐ์ง (ES2015 ํธํ)
const linkComponentRegex = /<Link\s+[^>]*href=["']([^"']+)["'][^>]*>/g;
const multilineContent = content.replace(/\n/g, " ");
let linkMatch;
while ((linkMatch = linkComponentRegex.exec(multilineContent)) !== null) {
const originalIndex = linkMatch.index;
const lineNumber = this.getLineNumber(content, originalIndex);
edges.push({
source: sourceRoute,
target: linkMatch[1],
method: "link",
codeSnippet: `Link href="${linkMatch[1]}"`,
lineNumber,
fileName: relativePath,
});
}
// ๋ฉํฐ๋ผ์ธ Link ์ปดํฌ๋ํธ์์ routes ์์ ์ฐธ์กฐ ๊ฐ์ง (NEW)
const linkConstantRegex = /<Link\s+[^>]*href=\{([A-Z_][A-Z0-9_]*(?:\.[A-Z_][A-Z0-9_]*)*)\}[^>]*>/g;
let linkConstantMatch;
while ((linkConstantMatch = linkConstantRegex.exec(multilineContent)) !== null) {
const variableName = linkConstantMatch[1];
const resolvedPath = this.resolveRouteConstant(variableName);
if (resolvedPath) {
const originalIndex = linkConstantMatch.index;
const lineNumber = this.getLineNumber(content, originalIndex);
edges.push({
source: sourceRoute,
target: resolvedPath,
method: "link",
codeSnippet: `Link href={${variableName}}`,
lineNumber,
fileName: relativePath,
routeVariable: variableName,
});
}
}
// ๋ฉํฐ๋ผ์ธ Link ์ปดํฌ๋ํธ์์ ํจ์ํ routes ์์ ์ฐธ์กฐ ๊ฐ์ง (NEW)
const linkFunctionRegex = /<Link\s+[^>]*href=\{([A-Za-z][A-Za-z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*)\(\s*\)\}[^>]*>/g;
let linkFunctionMatch;
while ((linkFunctionMatch = linkFunctionRegex.exec(multilineContent)) !== null) {
const functionCall = linkFunctionMatch[1];
const resolvedPath = this.resolveRouteConstant(functionCall);
if (resolvedPath) {
const originalIndex = linkFunctionMatch.index;
const lineNumber = this.getLineNumber(content, originalIndex);
edges.push({
source: sourceRoute,
target: resolvedPath,
method: "link",
codeSnippet: `Link href={${functionCall}()}`,
lineNumber,
fileName: relativePath,
routeVariable: functionCall,
});
}
}
const lines = content.split("\n");
// ๊ธฐ๋ณธ ํจํด๋ค (๊ธฐ์กด ๋ก์ง)
lines.forEach((line, index) => {
const lineNumber = index + 1;
// Extract router.push calls (๊ธฐ๋ณธ ๋ฐ ์กฐ๊ฑด๋ถ)
const pushMatches = line.matchAll(/(?:router|Router)\.push\(["']([^"']+)["']\)/g);
for (const match of pushMatches) {
edges.push({
source: sourceRoute,
target: match[1],
method: "push",
codeSnippet: match[0],
lineNumber,
fileName: relativePath,
});
}
// Extract router.push with route constants (NEW)
const pushConstantMatches = line.matchAll(/(?:router|Router)\.push\(([A-Z_][A-Z0-9_]*(?:\.[A-Z_][A-Z0-9_]*)*)\)/g);
for (const match of pushConstantMatches) {
const variableName = match[1];
const resolvedPath = this.resolveRouteConstant(variableName);
if (resolvedPath) {
edges.push({
source: sourceRoute,
target: resolvedPath,
method: "push",
codeSnippet: match[0],
lineNumber,
fileName: relativePath,
routeVariable: variableName,
});
}
}
// Extract router.push with route constants - ๋ ํฌ๊ด์ ์ธ ํจํด (NEW)
const pushVariableMatches = line.matchAll(/(?:router|Router)\.push\(([A-Za-z][A-Za-z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*)\)/g);
for (const match of pushVariableMatches) {
const variableName = match[1];
const resolvedPath = this.resolveRouteConstant(variableName);
if (resolvedPath) {
console.log(`๐ฏ Routes ๋ณ์ ํด์: ${variableName} โ ${resolvedPath}`);
edges.push({
source: sourceRoute,
target: resolvedPath,
method: "push",
codeSnippet: match[0],
lineNumber,
fileName: relativePath,
routeVariable: variableName,
});
}
}
// Extract Router.push with function calls - ํจ์ ๋ด๋ถ ๋ถ์ํ์ฌ ๊ฐ๋ฅํ ๋ชจ๋ ๊ฒฝ๋ก ์ถ์ถ
const pushFunctionCallMatches = line.matchAll(/(?:router|Router)\.push\((\w+)\(\s*\)\)/g);
for (const match of pushFunctionCallMatches) {
const functionName = match[1];
// ํด๋น ํจ์ ์ ์๋ฅผ ์ฐพ์์ ๋ด๋ถ์์ ๋ฐํ๋๋ ๋ชจ๋ ๊ฒฝ๋ก ์ถ์ถ
const possiblePaths = this.analyzeFunctionReturns(content, functionName);
for (const path of possiblePaths) {
edges.push({
source: sourceRoute,
target: path,
method: "push",
codeSnippet: `${match[0]} โ ${functionName}() returns ${path}`,
lineNumber,
fileName: relativePath,
});
}
}
// Extract router.push with function-style route constants (Routes.FUNC())
const pushRouteFunctionMatches = line.matchAll(/(?:router|Router)\.push\(([A-Za-z][A-Za-z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*)\(\s*\)\)/g);
for (const match of pushRouteFunctionMatches) {
const functionCall = match[1]; // Routes.DASHBOARD
const resolvedPath = this.resolveRouteConstant(functionCall);
if (resolvedPath) {
console.log(`๐ฏ Routes ํจ์ ํด์: ${functionCall}() โ ${resolvedPath}`);
edges.push({
source: sourceRoute,
target: resolvedPath,
method: "push",
codeSnippet: match[0],
lineNumber,
fileName: relativePath,
routeVariable: functionCall,
});
}
}
// Extract router.replace calls
const replaceMatches = line.matchAll(/(?:router|Router)\.replace\(["']([^"']+)["']\)/g);
for (const match of replaceMatches) {
edges.push({
source: sourceRoute,
target: match[1],
method: "replace",
codeSnippet: match[0],
lineNumber,
fileName: relativePath,
});
}
// Extract router.replace with route constants (NEW)
const replaceConstantMatches = line.matchAll(/(?:router|Router)\.replace\(([A-Z_][A-Z0-9_]*(?:\.[A-Z_][A-Z0-9_]*)*)\)/g);
for (const match of replaceConstantMatches) {
const variableName = match[1];
const resolvedPath = this.resolveRouteConstant(variableName);
if (resolvedPath) {
edges.push({
source: sourceRoute,
target: resolvedPath,
method: "replace",
codeSnippet: match[0],
lineNumber,
fileName: relativePath,
routeVariable: variableName,
});
}
}
// Extract useRouter().push calls
const useRouterPushMatches = line.matchAll(/\.push\(["']([^"']+)["']\)/g);
for (const match of useRouterPushMatches) {
edges.push({
source: sourceRoute,
target: match[1],
method: "push",
codeSnippet: match[0],
lineNumber,
fileName: relativePath,
});
}
// Extract useRouter().replace calls
const useRouterReplaceMatches = line.matchAll(/\.replace\(["']([^"']+)["']\)/g);
for (const match of useRouterReplaceMatches) {
edges.push({
source: sourceRoute,
target: match[1],
method: "replace",
codeSnippet: match[0],
lineNumber,
fileName: relativePath,
});
}
// ํ
ํจ์ ํธ์ถ ํจํด ๊ฐ์ง (์๋ก ์ถ๊ฐ)
const hookFunctionMatches = line.matchAll(/(\w+)\s*\([^)]*\)/g);
for (const match of hookFunctionMatches) {
const functionName = match[1];
// ์ด ํจ์๊ฐ ์ฌ์ฉ ์ค์ธ ํ
์์ ์ ์๋ ๋ค๋น๊ฒ์ด์
ํจ์์ธ์ง ํ์ธ
for (const [hookPath, usage] of this.hookUsages) {
if (usage.usedInPages.includes(sourceRoute) &&
usage.navigationFunctions.has(functionName)) {
const targets = usage.navigationFunctions.get(functionName) || [];
for (const target of targets) {
edges.push({
source: sourceRoute,
target: target,
method: "push",
codeSnippet: `${functionName}() from ${(0, path_1.basename)(hookPath)}`,
lineNumber,
fileName: relativePath,
});
}
}
}
}
});
// ์ถ๊ฐ: ๊ตฌ์กฐ๋ถํดํ ๋น์ผ๋ก ๊ฐ์ ธ์จ ํ
ํจ์ ํธ์ถ ๊ฐ์ง
const destructuredHookCalls = this.analyzeDestructuredHookCalls(content, sourceRoute, relativePath);
edges.push(...destructuredHookCalls);
// ๊ณ ๊ธ ํจํด ๋ถ์: ๋ฉํฐ๋ผ์ธ ์กฐ๊ฑด๋ถ ๋ค๋น๊ฒ์ด์
, ํ
ํ๋ฆฟ ๋ฆฌํฐ๋ด, ๋์ ๊ฒฝ๋ก
const enhancedEdges = this.analyzeComplexNavigationPatterns(content, sourceRoute, relativePath);
edges.push(...enhancedEdges);
// 1. getServerSideProps์์์ redirect ๋ถ์
edges.push(...this.analyzeServerSideRedirects(content, sourceRoute, relativePath));
// 2. ๊ธฐ๋ณธ ๋ค๋น๊ฒ์ด์
ํจํด๋ค
const basicPatterns = [
// Router push/replace
/(?:router|Router)\.(?:push|replace)\s*\(\s*[`'""]([^`'""]+)[`'""]\s*\)/g,
// Link href
/<Link[^>]+href\s*=\s*[`'""]([^`'""]+)[`'""]/g,
// Template literals
/(?:router|Router)\.(?:push|replace)\s*\(\s*`([^`]+)`\s*\)/g,
];
basicPatterns.forEach((pattern) => {
let match;
while ((match = pattern.exec(content)) !== null) {
const targetPath = match[1];
// ํ
ํ๋ฆฟ ๋ฆฌํฐ๋ด์์ ๊ธฐ๋ณธ ๊ฒฝ๋ก ์ถ์ถ
const basePath = this.extractBasePathFromTemplate(targetPath);
const resolvedPath = this.resolveRouteConstant(basePath || targetPath) ||
basePath ||
targetPath;
if (resolvedPath && this.isValidRoute(resolvedPath)) {
edges.push({
source: sourceRoute,
target: resolvedPath,
method: "push",
lineNumber: this.getLineNumber(content, match.index),
codeSnippet: match[0],
fileName: relativePath,
});
}
}
});
// 3. ํจ์ ๋ด๋ถ ๋ฐ ์กฐ๊ฑด๋ถ ๋ค๋น๊ฒ์ด์
ํจํด ๋ถ์
edges.push(...this.analyzeFunctionBasedNavigation(content, sourceRoute, relativePath));
// 4. ๋ณต์กํ ๋ค๋น๊ฒ์ด์
ํจํด ๋ถ์ (๊ธฐ์กด)
edges.push(...this.analyzeComplexNavigationPatterns(content, sourceRoute, relativePath));
return edges;
}
/**
* ๋ณต์กํ ๋ค๋น๊ฒ์ด์
ํจํด์ ๋ถ์ํฉ๋๋ค.
* - ์กฐ๊ฑด๋ถ ๋ค๋น๊ฒ์ด์
(if/else ๋ธ๋ก)
* - ํ
ํ๋ฆฟ ๋ฆฌํฐ๋ด ์ฌ์ฉ
* - useEffect ๋ด๋ถ์ ๋ค๋น๊ฒ์ด์
* - ๋ณดํธ๋ ๊ฒฝ๋ก ๋ก์ง
*/
analyzeComplexNavigationPatterns(content, sourceRoute, relativePath) {
const edges = [];
// 1. ํ
ํ๋ฆฟ ๋ฆฌํฐ๋ด ํจํด ๊ฐ์ง
const templateLiteralPatterns = [
/router\.push\(`([^`]+)`\)/g,
/router\.replace\(`([^`]+)`\)/g,
/\.push\(`([^`]+)`\)/g,
/\.replace\(`([^`]+)`\)/g,
];
templateLiteralPatterns.forEach((pattern) => {
const matches = content.matchAll(pattern);
for (const match of matches) {
const template = match[1];
// ๊ฐ๋จํ ํ
ํ๋ฆฟ์ ๊ฒฝ๋ก๋ก ๋ณํ (์: `/login?redirect=${path}` -> `/login`)
const basePath = this.extractBasePathFromTemplate(template);
if (basePath) {
const lineNumber = this.getLineNumber(content, match.index || 0);
edges.push({
source: sourceRoute,
target: basePath,
method: "push",
codeSnippet: match[0],
lineNumber,
fileName: relativePath,
});
}
}
});
// 2. ์กฐ๊ฑด๋ถ ๋ค๋น๊ฒ์ด์
๋ธ๋ก ๋ถ์
const conditionalBlocks = this.extractConditionalNavigationBlocks(content);
conditionalBlocks.forEach((block) => {
const blockEdges = this.analyzeNavigationInBlock(block, sourceRoute, relativePath);
edges.push(...blockEdges);
});
// 3. useEffect ๋ด๋ถ ๋ค๋น๊ฒ์ด์
๋ถ์
const useEffectBlocks = this.extractUseEffectBlocks(content);
useEffectBlocks.forEach((block) => {
const effectEdges = this.analyzeNavigationInBlock(block, sourceRoute, relativePath);
edges.push(...effectEdges);
});
// 4. ๋ณดํธ๋ ๊ฒฝ๋ก ํจํด ๋ถ์
const protectedRouteEdges = this.analyzeProtectedRoutePatterns(content, sourceRoute, relativePath);
edges.push(...protectedRouteEdges);
return edges;
}
/**
* ํ
ํ๋ฆฟ ๋ฆฌํฐ๋ด์์ ๊ธฐ๋ณธ ๊ฒฝ๋ก๋ฅผ ์ถ์ถํฉ๋๋ค.
*/
extractBasePathFromTemplate(template) {
// `/login?redirect=${path}` -> `/login`
// `/dashboard?userId=${userId}` -> `/dashboard`
// `/analytics#${section}` -> `/analytics`
const basePathMatch = template.match(/^([^?#$]+)/);
if (basePathMatch) {
const basePath = basePathMatch[1];
// ๋ณ์ ์ฐธ์กฐ๊ฐ ํฌํจ๋ ๊ฒฝ์ฐ ๋ฌด์
if (basePath.includes("${")) {
return null;
}
return basePath;
}
return null;
}
/**
* ์กฐ๊ฑด๋ถ ๋ค๋น๊ฒ์ด์
๋ธ๋ก์ ์ถ์ถํฉ๋๋ค.
*/
extractConditionalNavigationBlocks(content) {
const blocks = [];
// if ๋ธ๋ก ํจํด ๋งค์นญ
const ifBlockPattern = /if\s*\([^)]+\)\s*\{[^}]*router\.[^}]+\}/g;
const ifMatches = content.matchAll(ifBlockPattern);
for (const match of ifMatches) {
blocks.push(match[0]);
}
// else ๋ธ๋ก ํจํด ๋งค์นญ
const elseBlockPattern = /else\s*\{[^}]*router\.[^}]+\}/g;
const elseMatches = content.matchAll(elseBlockPattern);
for (const match of elseMatches) {
blocks.push(match[0]);
}
return blocks;
}
/**
* useEffect ๋ธ๋ก์ ์ถ์ถํฉ๋๋ค.
*/
extractUseEffectBlocks(content) {
const blocks = [];
// useEffect ํจํด ๋งค์นญ (๊ฐ๋จํ ๋ฒ์ )
const useEffectPattern = /useEffect\s*\(\s*\(\s*\)\s*=>\s*\{[^}]*router\.[^}]+\}/g;
const matches = content.matchAll(useEffectPattern);
for (const match of matches) {
blocks.push(match[0]);
}
return blocks;
}
/**
* ๋ณดํธ๋ ๊ฒฝ๋ก ํจํด์ ๋ถ์ํฉ๋๋ค.
*/
analyzeProtectedRoutePatterns(content, sourceRoute, relativePath) {
const edges = [];
// ๋ณดํธ๋ ๊ฒฝ๋ก ๋ฐฐ์ด ํจํด ๊ฐ์ง
const protectedArrayPattern = /protectedRoutes\s*=\s*\[([\s\S]*?)\]/;
const protectedMatch = content.match(protectedArrayPattern);
if (protectedMatch) {
// ๋ฐฐ์ด ๋ด์ ๊ฒฝ๋ก๋ค์ ์ถ์ถ
const routesString = protectedMatch[1];
const routeMatches = routesString.matchAll(/["']([^"']+)["']/g);
for (const routeMatch of routeMatches) {
const route = routeMatch[1];
// ๋ณดํธ๋ ๊ฒฝ๋ก๋ก์ ๊ฐ์ ์ ์ฐ๊ฒฐ์ ์๋ฏธํ๋ฏ๋ก login ํ์ด์ง๋ก์ ์ฐ๊ฒฐ๋ก ํํ
const lineNumber = this.getLineNumber(content, protectedMatch.index || 0);
edges.push({
source: sourceRoute,
target: "/login",
method: "push",
codeSnippet: `protected route: ${route}`,
lineNumber,
fileName: relativePath,
});
}
}
return edges;
}
/**
* ์ฝ๋ ๋ธ๋ก ๋ด์ ๋ค๋น๊ฒ์ด์
์ ๋ถ์ํฉ๋๋ค.
*/
analyzeNavigationInBlock(block, sourceRoute, relativePath) {
const edges = [];
// ๋ธ๋ก ๋ด์ router.push/replace ํจํด ๋ถ์
const patterns = [
{ regex: /router\.push\(["']([^"']+)["']\)/g, method: "push" },
{
regex: /router\.replace\(["']([^"']+)["']\)/g,
method: "replace",
},
{ regex: /\.push\(["']([^"']+)["']\)/g, method: "push" },
{ regex: /\.replace\(["']([^"']+)["']\)/g, method: "replace" },
];
patterns.forEach(({ regex, method }) => {
const matches = block.matchAll(regex);
for (const match of matches) {
edges.push({
source: sourceRoute,
target: match[1],
method: method,
codeSnippet: match[0],
lineNumber: 0, // ๋ธ๋ก ๋ด์์๋ ์ ํํ ๋ผ์ธ ๋ฒํธ ๊ณ์ฐ์ด ๋ณต์ก
fileName: relativePath,
});
}
});
// ํ
ํ๋ฆฟ ๋ฆฌํฐ๋ด ํจํด๋ ๋ธ๋ก ๋ด์์ ๋ถ์
const templatePatterns = [
{ regex: /router\.push\(`([^`]+)`\)/g, method: "push" },
{ regex: /router\.replace\(`([^`]+)`\)/g, method: "replace" },
{ regex: /\.push\(`([^`]+)`\)/g, method: "push" },
{ regex: /\.replace\(`([^`]+)`\)/g, method: "replace" },
];
templatePatterns.forEach(({ regex, method }) => {
const matches = block.matchAll(regex);
for (const match of matches) {
const template = match[1];
const basePath = this.extractBasePathFromTemplate(template);
if (basePath) {
edges.push({
source: sourceRoute,
target: basePath,
method: method,
codeSnippet: match[0],
lineNumber: 0,
fileName: relativePath,
});
}
}
});
return edges;
}
/**
* ์ฝ๋ ๋ธ๋ก์์ ๊ตฌ์กฐ๋ถํดํ ๋น์ผ๋ก ๊ฐ์ ธ์จ ํ
ํจ์ ํธ์ถ์ ๋ถ์ํฉ๋๋ค.
*/
analyzeDestructuredHookCalls(content, sourceRoute, relativePath) {
const edges = [];
// 1. ๊ตฌ์กฐ๋ถํดํ ๋น๋ ํ
ํจ