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

1,149 lines โ€ข 73.4 kB
"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. ๊ตฌ์กฐ๋ถ„ํ•ดํ• ๋‹น๋œ ํ›… ํ•จ