Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:46 +08:00
commit c47723919d
6 changed files with 1339 additions and 0 deletions

View File

@@ -0,0 +1,464 @@
#!/usr/bin/env python3
"""
Analyze Next.js routes and Server Actions to provide optimization recommendations.
Usage:
python analyze_routes.py --path /path/to/app --output analysis-report.md
python analyze_routes.py --path ./app --format json
"""
import os
import re
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
from dataclasses import dataclass, asdict
@dataclass
class RouteInfo:
"""Information about a route or Server Action."""
path: str
type: str # 'api_route' or 'server_action'
methods: List[str] # HTTP methods for API routes
has_auth: bool
has_revalidation: bool
has_external_api: bool
has_form_data: bool
has_streaming: bool
has_cookies: bool
has_headers: bool
recommendation: str
reason: str
migration_complexity: str # 'low', 'medium', 'high'
class RouteAnalyzer:
"""Analyze Next.js routes and Server Actions."""
def __init__(self, app_path: str):
self.app_path = Path(app_path)
self.routes: List[RouteInfo] = []
def analyze(self) -> List[RouteInfo]:
"""Analyze all routes in the app directory."""
# Find all route.ts/js files (API routes)
for route_file in self.app_path.rglob('route.*'):
if route_file.suffix in ['.ts', '.tsx', '.js', '.jsx']:
self._analyze_api_route(route_file)
# Find Server Actions (files with 'use server')
for file in self.app_path.rglob('*.*'):
if file.suffix in ['.ts', '.tsx', '.js', '.jsx']:
if self._has_server_directive(file):
self._analyze_server_actions(file)
return self.routes
def _has_server_directive(self, file_path: Path) -> bool:
"""Check if file has 'use server' directive."""
try:
content = file_path.read_text(encoding='utf-8')
return "'use server'" in content or '"use server"' in content
except:
return False
def _analyze_api_route(self, file_path: Path):
"""Analyze an API route file."""
try:
content = file_path.read_text(encoding='utf-8')
except:
return
# Detect HTTP methods
methods = []
for method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:
if re.search(rf'export\s+async\s+function\s+{method}', content):
methods.append(method)
if not methods:
return
# Analyze patterns
has_auth = self._detect_auth(content)
has_revalidation = self._detect_revalidation(content)
has_external_api = self._detect_external_api(content)
has_form_data = self._detect_form_data(content)
has_streaming = self._detect_streaming(content)
has_cookies = self._detect_cookies(content)
has_headers = self._detect_custom_headers(content)
# Generate recommendation
recommendation, reason, complexity = self._recommend_for_api_route(
methods=methods,
has_auth=has_auth,
has_revalidation=has_revalidation,
has_external_api=has_external_api,
has_form_data=has_form_data,
has_streaming=has_streaming,
has_cookies=has_cookies,
has_headers=has_headers,
)
route_info = RouteInfo(
path=str(file_path.relative_to(self.app_path)),
type='api_route',
methods=methods,
has_auth=has_auth,
has_revalidation=has_revalidation,
has_external_api=has_external_api,
has_form_data=has_form_data,
has_streaming=has_streaming,
has_cookies=has_cookies,
has_headers=has_headers,
recommendation=recommendation,
reason=reason,
migration_complexity=complexity,
)
self.routes.append(route_info)
def _analyze_server_actions(self, file_path: Path):
"""Analyze Server Actions in a file."""
try:
content = file_path.read_text(encoding='utf-8')
except:
return
# Find exported async functions
function_pattern = re.compile(
r'export\s+(?:async\s+)?function\s+(\w+)',
re.MULTILINE
)
for match in function_pattern.finditer(content):
function_name = match.group(1)
# Try to extract function body (basic approach)
start = match.start()
# Find the opening brace
brace_pos = content.find('{', start)
if brace_pos == -1:
continue
# Simple brace matching (not perfect but works for most cases)
brace_count = 1
pos = brace_pos + 1
while pos < len(content) and brace_count > 0:
if content[pos] == '{':
brace_count += 1
elif content[pos] == '}':
brace_count -= 1
pos += 1
function_body = content[brace_pos:pos]
# Analyze patterns in function body
has_auth = self._detect_auth(function_body)
has_revalidation = self._detect_revalidation(function_body)
has_external_api = self._detect_external_api(function_body)
has_form_data = 'FormData' in function_body
# Generate recommendation
recommendation, reason, complexity = self._recommend_for_server_action(
function_name=function_name,
has_auth=has_auth,
has_revalidation=has_revalidation,
has_external_api=has_external_api,
has_form_data=has_form_data,
)
route_info = RouteInfo(
path=f"{file_path.relative_to(self.app_path)}::{function_name}",
type='server_action',
methods=['POST'], # Server Actions are POST-only
has_auth=has_auth,
has_revalidation=has_revalidation,
has_external_api=has_external_api,
has_form_data=has_form_data,
has_streaming=False,
has_cookies=False,
has_headers=False,
recommendation=recommendation,
reason=reason,
migration_complexity=complexity,
)
self.routes.append(route_info)
def _detect_auth(self, content: str) -> bool:
"""Detect authentication patterns."""
patterns = [
r'auth\(',
r'getServerSession',
r'getSession',
r'session',
r'currentUser',
r'verifyToken',
r'authorization',
r'Authorization',
]
return any(re.search(pattern, content, re.IGNORECASE) for pattern in patterns)
def _detect_revalidation(self, content: str) -> bool:
"""Detect revalidation calls."""
return 'revalidatePath' in content or 'revalidateTag' in content
def _detect_external_api(self, content: str) -> bool:
"""Detect external API calls."""
patterns = [
r'fetch\([\'"]https?://',
r'axios\.',
r'\.get\([\'"]https?://',
r'\.post\([\'"]https?://',
]
return any(re.search(pattern, content) for pattern in patterns)
def _detect_form_data(self, content: str) -> bool:
"""Detect FormData handling."""
return 'FormData' in content or 'formData' in content
def _detect_streaming(self, content: str) -> bool:
"""Detect streaming responses."""
return 'ReadableStream' in content or 'TransformStream' in content or 'stream' in content.lower()
def _detect_cookies(self, content: str) -> bool:
"""Detect cookie operations."""
return 'cookies()' in content or 'setCookie' in content
def _detect_custom_headers(self, content: str) -> bool:
"""Detect custom header operations."""
patterns = [
r'headers\(\)',
r'\.headers\.',
r'setHeader',
r'Response\.json\([^,]+,\s*\{[^}]*headers',
]
return any(re.search(pattern, content) for pattern in patterns)
def _recommend_for_api_route(
self,
methods: List[str],
has_auth: bool,
has_revalidation: bool,
has_external_api: bool,
has_form_data: bool,
has_streaming: bool,
has_cookies: bool,
has_headers: bool,
) -> Tuple[str, str, str]:
"""Generate recommendation for an API route."""
reasons = []
# Strong indicators to keep as API route
if has_external_api:
reasons.append("proxies external API")
if has_streaming:
reasons.append("uses streaming responses")
if has_custom_headers or has_cookies:
reasons.append("requires custom headers/cookies")
if 'GET' in methods or len(methods) > 1:
reasons.append(f"uses multiple HTTP methods ({', '.join(methods)})")
# Strong indicators to convert to Server Action
if methods == ['POST'] and has_revalidation and not has_external_api:
if has_form_data:
return (
'Convert to Server Action',
'Simple POST with form data and revalidation - ideal for Server Action',
'low'
)
else:
return (
'Convert to Server Action',
'POST-only mutation with revalidation - better as Server Action',
'low'
)
# Keep as API route
if reasons:
return (
'Keep as API Route',
'Better as API route: ' + ', '.join(reasons),
'n/a'
)
# Neutral case - could go either way
if methods == ['POST']:
return (
'Consider Server Action',
'Simple POST endpoint could be simplified as Server Action',
'low'
)
return (
'Keep as API Route',
'Current implementation is appropriate',
'n/a'
)
def _recommend_for_server_action(
self,
function_name: str,
has_auth: bool,
has_revalidation: bool,
has_external_api: bool,
has_form_data: bool,
) -> Tuple[str, str, str]:
"""Generate recommendation for a Server Action."""
# Check if Server Action might be better as API route
if has_external_api and not has_revalidation:
return (
'Consider API Route',
'External API calls without revalidation might be better as API route for reusability',
'medium'
)
# Good use of Server Action
if has_revalidation or has_form_data:
return (
'Keep as Server Action',
'Good use of Server Action - leverages revalidation and/or form handling',
'n/a'
)
# Neutral
return (
'Keep as Server Action',
'Current implementation is appropriate',
'n/a'
)
def generate_report(self, format: str = 'markdown') -> str:
"""Generate analysis report."""
if format == 'json':
return json.dumps([asdict(route) for route in self.routes], indent=2)
# Markdown report
lines = [
'# Next.js Route Analysis Report',
'',
f'Analyzed {len(self.routes)} routes/actions',
'',
]
# Summary statistics
api_routes = [r for r in self.routes if r.type == 'api_route']
server_actions = [r for r in self.routes if r.type == 'server_action']
needs_migration = [r for r in self.routes if 'Convert' in r.recommendation or 'Consider' in r.recommendation]
lines.extend([
'## Summary',
'',
f'- **API Routes**: {len(api_routes)}',
f'- **Server Actions**: {len(server_actions)}',
f'- **Recommended Migrations**: {len(needs_migration)}',
'',
])
# Recommendations for migration
if needs_migration:
lines.extend([
'## Recommended Migrations',
'',
])
for route in needs_migration:
lines.extend([
f'### {route.path}',
'',
f'**Current**: {route.type.replace("_", " ").title()}',
f'**Recommendation**: {route.recommendation}',
f'**Reason**: {route.reason}',
f'**Complexity**: {route.migration_complexity}',
'',
])
# Detailed analysis
lines.extend([
'## Detailed Analysis',
'',
])
for route in self.routes:
lines.extend([
f'### {route.path}',
'',
f'- **Type**: {route.type.replace("_", " ").title()}',
f'- **Methods**: {", ".join(route.methods)}',
f'- **Authentication**: {"Yes" if route.has_auth else "No"}',
f'- **Revalidation**: {"Yes" if route.has_revalidation else "No"}',
f'- **External API**: {"Yes" if route.has_external_api else "No"}',
f'- **Form Data**: {"Yes" if route.has_form_data else "No"}',
])
if route.type == 'api_route':
lines.extend([
f'- **Streaming**: {"Yes" if route.has_streaming else "No"}',
f'- **Custom Headers/Cookies**: {"Yes" if (route.has_cookies or route.has_headers) else "No"}',
])
lines.extend([
'',
f'**Recommendation**: {route.recommendation}',
f'**Reason**: {route.reason}',
'',
])
return '\n'.join(lines)
def main():
parser = argparse.ArgumentParser(
description='Analyze Next.js routes and provide optimization recommendations'
)
parser.add_argument(
'--path',
required=True,
help='Path to the Next.js app directory'
)
parser.add_argument(
'--output',
help='Output file path for the report'
)
parser.add_argument(
'--format',
choices=['markdown', 'json'],
default='markdown',
help='Output format (default: markdown)'
)
args = parser.parse_args()
# Validate path
app_path = Path(args.path)
if not app_path.exists():
print(f"Error: Path does not exist: {args.path}")
return 1
# Analyze routes
analyzer = RouteAnalyzer(args.path)
routes = analyzer.analyze()
if not routes:
print("No routes or Server Actions found.")
return 0
# Generate report
report = analyzer.generate_report(format=args.format)
# Write output
if args.output:
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(report, encoding='utf-8')
print(f"Report written to: {args.output}")
else:
print(report)
return 0
if __name__ == '__main__':
exit(main())