#!/usr/bin/env python3 """ Framework Detection Utility for Eureka Deployments. Detects project framework from package.json, requirements.txt, go.mod, or Cargo.toml and returns appropriate Dockerfile template information. """ import json import sys from pathlib import Path from typing import Dict, Any, Optional, List # Framework definitions with Dockerfile requirements FRAMEWORKS: Dict[str, Dict[str, Any]] = { # Full-stack frameworks 'nextjs-prisma': { 'name': 'Next.js + Prisma', 'base_image': 'node:20-alpine3.18', 'port': 3000, 'requirements': ['output: standalone in next.config.js', 'openssl1.1-compat'], 'build_cmd': 'npm run build', 'start_cmd': 'node server.js', }, 'nextjs': { 'name': 'Next.js', 'base_image': 'node:20-alpine', 'port': 3000, 'requirements': ['output: standalone in next.config.js'], 'build_cmd': 'npm run build', 'start_cmd': 'node server.js', }, 'nuxt': { 'name': 'Nuxt.js 3', 'base_image': 'node:20-alpine', 'port': 3000, 'requirements': [], 'build_cmd': 'npm run build', 'start_cmd': 'node .output/server/index.mjs', }, 'remix': { 'name': 'Remix', 'base_image': 'node:20-alpine', 'port': 3000, 'requirements': [], 'build_cmd': 'npm run build', 'start_cmd': 'npm start', }, 'sveltekit': { 'name': 'SvelteKit', 'base_image': 'node:20-alpine', 'port': 3000, 'requirements': [], 'build_cmd': 'npm run build', 'start_cmd': 'node build', }, # Frontend frameworks (SPA) 'react-vite': { 'name': 'React (Vite)', 'base_image': 'nginx:alpine', 'port': 80, 'requirements': ['nginx.conf for SPA routing'], 'build_cmd': 'npm run build', 'build_output': 'dist', 'spa': True, }, 'react-cra': { 'name': 'React (Create React App)', 'base_image': 'nginx:alpine', 'port': 80, 'requirements': ['nginx.conf for SPA routing'], 'build_cmd': 'npm run build', 'build_output': 'build', 'spa': True, }, 'vue-vite': { 'name': 'Vue.js (Vite)', 'base_image': 'nginx:alpine', 'port': 80, 'requirements': ['nginx.conf for SPA routing'], 'build_cmd': 'npm run build', 'build_output': 'dist', 'spa': True, }, 'angular': { 'name': 'Angular', 'base_image': 'nginx:alpine', 'port': 80, 'requirements': ['nginx.conf for SPA routing'], 'build_cmd': 'npm run build --prod', 'build_output': 'dist/*/browser', 'spa': True, }, # Backend frameworks 'nestjs-prisma': { 'name': 'NestJS + Prisma', 'base_image': 'node:20-alpine3.18', 'port': 3000, 'requirements': ['openssl1.1-compat'], 'build_cmd': 'npm run build', 'start_cmd': 'node dist/main.js', }, 'nestjs': { 'name': 'NestJS', 'base_image': 'node:20-alpine', 'port': 3000, 'requirements': [], 'build_cmd': 'npm run build', 'start_cmd': 'node dist/main.js', }, 'fastify-prisma': { 'name': 'Fastify + Prisma', 'base_image': 'node:20-alpine3.18', 'port': 3000, 'requirements': ['openssl1.1-compat'], 'build_cmd': 'npm run build', 'start_cmd': 'node dist/index.js', }, 'fastify': { 'name': 'Fastify', 'base_image': 'node:20-alpine', 'port': 3000, 'requirements': [], 'build_cmd': 'npm run build', 'start_cmd': 'node dist/index.js', }, 'express-prisma': { 'name': 'Express + Prisma', 'base_image': 'node:20-alpine3.18', 'port': 3000, 'requirements': ['openssl1.1-compat'], 'build_cmd': 'npm run build', 'start_cmd': 'node dist/index.js', }, 'express': { 'name': 'Express.js', 'base_image': 'node:20-alpine', 'port': 3000, 'requirements': [], 'build_cmd': 'npm run build', 'start_cmd': 'node dist/index.js', }, 'hono': { 'name': 'Hono', 'base_image': 'node:20-alpine', 'port': 3000, 'requirements': [], 'build_cmd': 'npm run build', 'start_cmd': 'node dist/index.js', }, # Python frameworks 'fastapi': { 'name': 'FastAPI', 'base_image': 'python:3.12-slim', 'port': 8000, 'requirements': ['uvicorn'], 'start_cmd': 'uvicorn main:app --host 0.0.0.0 --port 8000', }, 'flask': { 'name': 'Flask', 'base_image': 'python:3.12-slim', 'port': 5000, 'requirements': ['gunicorn'], 'start_cmd': 'gunicorn --bind 0.0.0.0:5000 app:app', }, 'django': { 'name': 'Django', 'base_image': 'python:3.12-slim', 'port': 8000, 'requirements': ['gunicorn', 'collectstatic'], 'start_cmd': 'gunicorn --bind 0.0.0.0:8000 project.wsgi:application', }, # Other languages 'go': { 'name': 'Go', 'base_image': 'golang:1.22-alpine', 'runtime_image': 'alpine:latest', 'port': 8080, 'requirements': ['CGO_ENABLED=0 for static binary'], 'build_cmd': 'go build -o main .', 'start_cmd': './main', }, 'rust': { 'name': 'Rust', 'base_image': 'rust:1.75-alpine', 'runtime_image': 'alpine:latest', 'port': 8080, 'requirements': ['musl-dev for static linking'], 'build_cmd': 'cargo build --release', 'start_cmd': './app', }, } def has_prisma(project_path: Path, deps: Dict[str, str]) -> bool: """Check if project uses Prisma ORM.""" # Check for prisma in dependencies if 'prisma' in deps or '@prisma/client' in deps: return True # Check for prisma directory if (project_path / 'prisma').exists(): return True return False def detect_node_framework(project_path: Path) -> Optional[str]: """Detect Node.js framework from package.json.""" package_json_path = project_path / 'package.json' if not package_json_path.exists(): return None try: with open(package_json_path) as f: pkg = json.load(f) except (json.JSONDecodeError, IOError): return None deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})} uses_prisma = has_prisma(project_path, deps) # Full-stack frameworks (order matters - check specific ones first) if 'next' in deps: return 'nextjs-prisma' if uses_prisma else 'nextjs' if 'nuxt' in deps: return 'nuxt' if '@remix-run/node' in deps or '@remix-run/react' in deps: return 'remix' if '@sveltejs/kit' in deps: return 'sveltekit' # Frontend frameworks (SPA) if 'vite' in deps and 'react' in deps: return 'react-vite' if 'react-scripts' in deps: return 'react-cra' if 'vite' in deps and 'vue' in deps: return 'vue-vite' if '@angular/core' in deps: return 'angular' # Backend frameworks if '@nestjs/core' in deps: return 'nestjs-prisma' if uses_prisma else 'nestjs' if 'fastify' in deps: return 'fastify-prisma' if uses_prisma else 'fastify' if 'express' in deps: return 'express-prisma' if uses_prisma else 'express' if 'hono' in deps: return 'hono' return None def detect_python_framework(project_path: Path) -> Optional[str]: """Detect Python framework from requirements.txt or pyproject.toml.""" requirements_path = project_path / 'requirements.txt' pyproject_path = project_path / 'pyproject.toml' requirements_content = '' if requirements_path.exists(): try: requirements_content = requirements_path.read_text().lower() except IOError: pass if pyproject_path.exists(): try: requirements_content += pyproject_path.read_text().lower() except IOError: pass if not requirements_content: return None if 'fastapi' in requirements_content: return 'fastapi' if 'flask' in requirements_content: return 'flask' if 'django' in requirements_content: return 'django' return None def detect_go_project(project_path: Path) -> Optional[str]: """Detect Go project from go.mod.""" if (project_path / 'go.mod').exists(): return 'go' return None def detect_rust_project(project_path: Path) -> Optional[str]: """Detect Rust project from Cargo.toml.""" if (project_path / 'Cargo.toml').exists(): return 'rust' return None def detect_framework(project_path: Path) -> Dict[str, Any]: """ Detect project framework and return Dockerfile configuration. Args: project_path: Path to the project root Returns: Dictionary with framework info and Dockerfile requirements """ project_path = Path(project_path).resolve() # Try detection in order framework_id = ( detect_node_framework(project_path) or detect_python_framework(project_path) or detect_go_project(project_path) or detect_rust_project(project_path) ) if not framework_id: return { 'detected': False, 'framework_id': 'unknown', 'name': 'Unknown', 'error': 'Could not detect framework. Check package.json, requirements.txt, go.mod, or Cargo.toml', } framework = FRAMEWORKS[framework_id].copy() framework['detected'] = True framework['framework_id'] = framework_id # Add validation checks checks: List[Dict[str, Any]] = [] # Check Next.js standalone config if framework_id in ['nextjs', 'nextjs-prisma']: next_config = project_path / 'next.config.js' next_config_mjs = project_path / 'next.config.mjs' next_config_ts = project_path / 'next.config.ts' config_found = False standalone_configured = False for config_path in [next_config, next_config_mjs, next_config_ts]: if config_path.exists(): config_found = True try: content = config_path.read_text() if 'standalone' in content: standalone_configured = True except IOError: pass checks.append({ 'name': 'Next.js standalone output', 'passed': standalone_configured, 'message': 'Add output: "standalone" to next.config.js' if not standalone_configured else 'OK', 'critical': True, }) # Check nginx.conf for SPAs if framework.get('spa'): nginx_conf = project_path / 'nginx.conf' checks.append({ 'name': 'nginx.conf exists', 'passed': nginx_conf.exists(), 'message': 'Create nginx.conf for SPA routing' if not nginx_conf.exists() else 'OK', 'critical': False, # Can be generated }) # Check Dockerfile exists dockerfile = project_path / 'Dockerfile' checks.append({ 'name': 'Dockerfile exists', 'passed': dockerfile.exists(), 'message': 'Dockerfile will be generated' if not dockerfile.exists() else 'OK', 'critical': False, }) framework['checks'] = checks framework['all_checks_passed'] = all(c['passed'] or not c['critical'] for c in checks) return framework def format_output(result: Dict[str, Any], format_type: str = 'json') -> str: """Format the detection result for output.""" if format_type == 'json': return json.dumps(result, indent=2) if format_type == 'summary': lines = [] if result['detected']: lines.append(f"Framework: {result['name']}") lines.append(f"Base Image: {result['base_image']}") lines.append(f"Port: {result['port']}") if result.get('requirements'): lines.append(f"Requirements: {', '.join(result['requirements'])}") if result.get('checks'): lines.append("\nValidation Checks:") for check in result['checks']: status = "✅" if check['passed'] else "❌" lines.append(f" {status} {check['name']}: {check['message']}") else: lines.append(f"❌ {result['error']}") return '\n'.join(lines) return str(result) def main(): """CLI entry point.""" import argparse parser = argparse.ArgumentParser( description='Detect project framework for Eureka deployment' ) parser.add_argument( 'path', nargs='?', default='.', help='Project path (default: current directory)' ) parser.add_argument( '--format', choices=['json', 'summary'], default='json', help='Output format (default: json)' ) parser.add_argument( '--check', action='store_true', help='Exit with error code if critical checks fail' ) args = parser.parse_args() project_path = Path(args.path) if not project_path.exists(): print(f"Error: Path not found: {project_path}", file=sys.stderr) sys.exit(1) result = detect_framework(project_path) print(format_output(result, args.format)) if args.check and result.get('detected') and not result.get('all_checks_passed'): sys.exit(1) elif not result.get('detected'): sys.exit(1) if __name__ == '__main__': main()