#!/usr/bin/env python3 """ Frontend Project Scaffolder Generates a complete Next.js/React project structure with TypeScript, Tailwind CSS, and best practice configurations. Usage: python frontend_scaffolder.py my-app --template nextjs python frontend_scaffolder.py dashboard --template react --features auth,api python frontend_scaffolder.py landing --template nextjs --dry-run """ import argparse import json import os import sys from pathlib import Path from typing import Dict, List, Optional # Project templates TEMPLATES = { "nextjs": { "name": "Next.js 14+ App Router", "description": "Modern Next.js with App Router, Server Components, and TypeScript", "structure": { "app": { "layout.tsx": "ROOT_LAYOUT", "page.tsx": "HOME_PAGE", "globals.css": "GLOBALS_CSS", "(auth)": { "login": {"page.tsx": "AUTH_PAGE"}, "register": {"page.tsx": "AUTH_PAGE"}, }, "api": { "health": {"route.ts": "HEALTH_ROUTE"}, }, }, "components": { "ui": { "button.tsx": "UI_BUTTON", "input.tsx": "UI_INPUT", "card.tsx": "UI_CARD", "index.ts": "UI_INDEX", }, "layout": { "header.tsx": "LAYOUT_HEADER", "footer.tsx": "LAYOUT_FOOTER", "sidebar.tsx": "LAYOUT_SIDEBAR", }, }, "lib": { "utils.ts": "UTILS", "constants.ts": "CONSTANTS", }, "hooks": { "use-debounce.ts": "HOOK_DEBOUNCE", "use-local-storage.ts": "HOOK_LOCAL_STORAGE", }, "types": { "index.ts": "TYPES_INDEX", }, "public": { ".gitkeep": "EMPTY", }, }, "config_files": [ "next.config.js", "tailwind.config.ts", "tsconfig.json", "postcss.config.js", ".eslintrc.json", ".prettierrc", ".gitignore", "package.json", ], }, "react": { "name": "React + Vite", "description": "Modern React with Vite, TypeScript, and Tailwind CSS", "structure": { "src": { "App.tsx": "REACT_APP", "main.tsx": "REACT_MAIN", "index.css": "GLOBALS_CSS", "components": { "ui": { "button.tsx": "UI_BUTTON", "input.tsx": "UI_INPUT", "card.tsx": "UI_CARD", "index.ts": "UI_INDEX", }, }, "hooks": { "use-debounce.ts": "HOOK_DEBOUNCE", "use-local-storage.ts": "HOOK_LOCAL_STORAGE", }, "lib": { "utils.ts": "UTILS", }, "types": { "index.ts": "TYPES_INDEX", }, }, "public": { ".gitkeep": "EMPTY", }, }, "config_files": [ "vite.config.ts", "tailwind.config.ts", "tsconfig.json", "postcss.config.js", ".eslintrc.json", ".prettierrc", ".gitignore", "package.json", "index.html", ], }, } # Feature modules that can be added FEATURES = { "auth": { "description": "Authentication with session management", "files": { "lib/auth.ts": "AUTH_LIB", "middleware.ts": "AUTH_MIDDLEWARE", "components/auth/login-form.tsx": "LOGIN_FORM", "components/auth/register-form.tsx": "REGISTER_FORM", }, "dependencies": ["next-auth", "@auth/core"], }, "api": { "description": "API client with React Query", "files": { "lib/api-client.ts": "API_CLIENT", "lib/query-client.ts": "QUERY_CLIENT", "providers/query-provider.tsx": "QUERY_PROVIDER", }, "dependencies": ["@tanstack/react-query", "axios"], }, "forms": { "description": "Form handling with React Hook Form + Zod", "files": { "lib/form-utils.ts": "FORM_UTILS", "components/forms/form-field.tsx": "FORM_FIELD", }, "dependencies": ["react-hook-form", "@hookform/resolvers", "zod"], }, "testing": { "description": "Testing setup with Vitest and Testing Library", "files": { "vitest.config.ts": "VITEST_CONFIG", "src/test/setup.ts": "TEST_SETUP", "src/test/utils.tsx": "TEST_UTILS", }, "dependencies": ["vitest", "@testing-library/react", "@testing-library/jest-dom"], }, "storybook": { "description": "Component documentation with Storybook", "files": { ".storybook/main.ts": "STORYBOOK_MAIN", ".storybook/preview.ts": "STORYBOOK_PREVIEW", }, "dependencies": ["@storybook/react-vite", "@storybook/addon-essentials"], }, } # File content templates FILE_CONTENTS = { "ROOT_LAYOUT": '''import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }); export const metadata: Metadata = { title: 'My App', description: 'Built with Next.js', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ''', "HOME_PAGE": '''export default function Home() { return (

Welcome

Get started by editing app/page.tsx

); } ''', "GLOBALS_CSS": '''@tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ''', "UI_BUTTON": '''import { forwardRef } from 'react'; import { cn } from '@/lib/utils'; interface ButtonProps extends React.ButtonHTMLAttributes { variant?: 'default' | 'destructive' | 'outline' | 'ghost'; size?: 'default' | 'sm' | 'lg'; } const Button = forwardRef( ({ className, variant = 'default', size = 'default', ...props }, ref) => { return ( ); } export default App; ''', "REACT_MAIN": '''import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( ); ''', "EMPTY": "", } def generate_structure( base_path: Path, structure: Dict, dry_run: bool = False ) -> List[str]: """Generate directory structure recursively.""" created_files = [] for name, content in structure.items(): current_path = base_path / name if isinstance(content, dict): # It's a directory if not dry_run: current_path.mkdir(parents=True, exist_ok=True) created_files.extend(generate_structure(current_path, content, dry_run)) else: # It's a file if not dry_run: current_path.parent.mkdir(parents=True, exist_ok=True) file_content = FILE_CONTENTS.get(content, "") current_path.write_text(file_content) created_files.append(str(current_path)) return created_files def generate_config_files( project_path: Path, template: str, project_name: str, features: List[str], dry_run: bool = False ) -> List[str]: """Generate configuration files.""" created_files = [] config_templates = get_config_templates(project_name, template, features) template_config = TEMPLATES[template] for config_file in template_config["config_files"]: file_path = project_path / config_file if config_file in config_templates: if not dry_run: file_path.write_text(config_templates[config_file]) created_files.append(str(file_path)) return created_files def get_config_templates(name: str, template: str, features: List[str]) -> Dict[str, str]: """Get configuration file contents.""" deps = { "nextjs": { "dependencies": { "next": "^14.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "clsx": "^2.0.0", "tailwind-merge": "^2.0.0", }, "devDependencies": { "@types/node": "^20.0.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "autoprefixer": "^10.0.0", "eslint": "^8.0.0", "eslint-config-next": "^14.0.0", "postcss": "^8.0.0", "prettier": "^3.0.0", "tailwindcss": "^3.4.0", "typescript": "^5.0.0", }, }, "react": { "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "clsx": "^2.0.0", "tailwind-merge": "^2.0.0", }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.0.0", "autoprefixer": "^10.0.0", "eslint": "^8.0.0", "postcss": "^8.0.0", "prettier": "^3.0.0", "tailwindcss": "^3.4.0", "typescript": "^5.0.0", "vite": "^5.0.0", }, }, } # Add feature dependencies for feature in features: if feature in FEATURES: for dep in FEATURES[feature].get("dependencies", []): deps[template]["dependencies"][dep] = "latest" package_json = { "name": name, "version": "0.1.0", "private": True, "scripts": { "dev": "next dev" if template == "nextjs" else "vite", "build": "next build" if template == "nextjs" else "vite build", "start": "next start" if template == "nextjs" else "vite preview", "lint": "eslint . --ext .ts,.tsx", "format": "prettier --write .", }, "dependencies": deps[template]["dependencies"], "devDependencies": deps[template]["devDependencies"], } return { "package.json": json.dumps(package_json, indent=2), "tsconfig.json": '''{ "compilerOptions": { "target": "ES2020", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [{ "name": "next" }], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ''', "tailwind.config.ts": '''import type { Config } from 'tailwindcss'; const config: Config = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', './src/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', }, secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))', }, destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))', }, muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))', }, accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))', }, border: 'hsl(var(--border))', ring: 'hsl(var(--ring))', }, borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)', }, }, }, plugins: [], }; export default config; ''', "postcss.config.js": '''module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ''', "next.config.js": '''/** @type {import('next').NextConfig} */ const nextConfig = { images: { remotePatterns: [], formats: ['image/avif', 'image/webp'], }, experimental: { optimizePackageImports: ['lucide-react'], }, }; module.exports = nextConfig; ''', "vite.config.ts": '''import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, }); ''', ".eslintrc.json": '''{ "extends": ["next/core-web-vitals", "prettier"], "rules": { "react/no-unescaped-entities": "off" } } ''', ".prettierrc": '''{ "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "printWidth": 100 } ''', ".gitignore": '''# Dependencies node_modules/ .pnp .pnp.js # Build .next/ out/ dist/ build/ # Environment .env .env.local .env.*.local # IDE .vscode/ .idea/ # Debug npm-debug.log* yarn-debug.log* yarn-error.log* # OS .DS_Store Thumbs.db # Testing coverage/ ''', "index.html": ''' ''' + name + '''
''', } def scaffold_project( name: str, output_dir: Path, template: str = "nextjs", features: Optional[List[str]] = None, dry_run: bool = False, ) -> Dict: """Scaffold a complete frontend project.""" features = features or [] project_path = output_dir / name if project_path.exists() and not dry_run: return {"error": f"Directory already exists: {project_path}"} template_config = TEMPLATES.get(template) if not template_config: return {"error": f"Unknown template: {template}"} created_files = [] # Create project directory if not dry_run: project_path.mkdir(parents=True, exist_ok=True) # Generate base structure created_files.extend( generate_structure(project_path, template_config["structure"], dry_run) ) # Generate config files created_files.extend( generate_config_files(project_path, template, name, features, dry_run) ) # Add feature files for feature in features: if feature in FEATURES: for file_path, content_key in FEATURES[feature]["files"].items(): full_path = project_path / file_path if not dry_run: full_path.parent.mkdir(parents=True, exist_ok=True) content = FILE_CONTENTS.get(content_key, f"// TODO: Implement {content_key}") full_path.write_text(content) created_files.append(str(full_path)) return { "name": name, "template": template, "template_name": template_config["name"], "features": features, "path": str(project_path), "files_created": len(created_files), "files": created_files, "next_steps": [ f"cd {name}", "npm install", "npm run dev", ], } def print_result(result: Dict) -> None: """Print scaffolding result.""" if "error" in result: print(f"Error: {result['error']}", file=sys.stderr) return print(f"\n{'='*60}") print(f"Project Scaffolded: {result['name']}") print(f"{'='*60}") print(f"Template: {result['template_name']}") print(f"Location: {result['path']}") print(f"Files Created: {result['files_created']}") if result["features"]: print(f"Features: {', '.join(result['features'])}") print(f"\nNext Steps:") for step in result["next_steps"]: print(f" $ {step}") print(f"{'='*60}\n") def main(): parser = argparse.ArgumentParser( description="Scaffold a frontend project with best practices" ) parser.add_argument( "name", help="Project name (kebab-case recommended)" ) parser.add_argument( "--dir", "-d", default=".", help="Output directory (default: current directory)" ) parser.add_argument( "--template", "-t", choices=list(TEMPLATES.keys()), default="nextjs", help="Project template (default: nextjs)" ) parser.add_argument( "--features", "-f", help="Comma-separated features to add (auth,api,forms,testing,storybook)" ) parser.add_argument( "--list-templates", action="store_true", help="List available templates" ) parser.add_argument( "--list-features", action="store_true", help="List available features" ) parser.add_argument( "--dry-run", action="store_true", help="Show what would be created without creating files" ) parser.add_argument( "--json", action="store_true", help="Output in JSON format" ) args = parser.parse_args() if args.list_templates: print("\nAvailable Templates:") for key, template in TEMPLATES.items(): print(f" {key}: {template['name']}") print(f" {template['description']}") return if args.list_features: print("\nAvailable Features:") for key, feature in FEATURES.items(): print(f" {key}: {feature['description']}") deps = ", ".join(feature.get("dependencies", [])) if deps: print(f" Adds: {deps}") return features = [] if args.features: features = [f.strip() for f in args.features.split(",")] invalid = [f for f in features if f not in FEATURES] if invalid: print(f"Unknown features: {', '.join(invalid)}", file=sys.stderr) print(f"Valid features: {', '.join(FEATURES.keys())}") sys.exit(1) result = scaffold_project( name=args.name, output_dir=Path(args.dir), template=args.template, features=features, dry_run=args.dry_run, ) if args.json: print(json.dumps(result, indent=2)) else: print_result(result) if __name__ == "__main__": main()