#!/usr/bin/env python3 """ Fullstack Feature Scaffolder Generates complete frontend and backend code for features """ import os import json from pathlib import Path from typing import Dict, List class FullstackScaffolder: """Generate full-stack feature boilerplate""" def __init__(self): self.templates = { 'react': self._get_react_templates(), 'nextjs': self._get_nextjs_templates(), 'express': self._get_express_templates(), 'graphql': self._get_graphql_templates() } def scaffold_feature(self, feature_name: str, options: Dict) -> Dict: """Scaffold a complete full-stack feature""" feature = { 'name': feature_name, 'frontend': {}, 'backend': {}, 'database': {}, 'tests': {}, 'documentation': {} } # Generate based on stack stack = options.get('stack', 'react-express') if 'react' in stack or 'next' in stack: feature['frontend'] = self._generate_frontend(feature_name, options) if 'express' in stack or 'node' in stack: feature['backend'] = self._generate_backend(feature_name, options) if 'graphql' in stack: feature['backend']['graphql'] = self._generate_graphql(feature_name, options) feature['database'] = self._generate_database(feature_name, options) feature['tests'] = self._generate_tests(feature_name, options) feature['documentation'] = self._generate_docs(feature_name, options) return feature def _generate_frontend(self, name: str, options: Dict) -> Dict: """Generate frontend components""" framework = options.get('frontend_framework', 'react') use_typescript = options.get('typescript', True) ext = '.tsx' if use_typescript else '.jsx' frontend = { 'component': self._create_react_component(name, use_typescript), 'styles': self._create_styles(name, options.get('css_approach', 'modules')), 'hooks': self._create_custom_hook(name, use_typescript), 'api_client': self._create_api_client(name, use_typescript), 'state': self._create_state_management(name, options.get('state_lib', 'context')), 'types': self._create_types(name) if use_typescript else None } return frontend def _generate_backend(self, name: str, options: Dict) -> Dict: """Generate backend API code""" framework = options.get('backend_framework', 'express') use_typescript = options.get('typescript', True) backend = { 'controller': self._create_controller(name, framework, use_typescript), 'service': self._create_service(name, use_typescript), 'repository': self._create_repository(name, use_typescript), 'routes': self._create_routes(name, framework), 'middleware': self._create_middleware(name), 'validation': self._create_validation(name) } return backend def _generate_graphql(self, name: str, options: Dict) -> Dict: """Generate GraphQL schema and resolvers""" return { 'schema': self._create_graphql_schema(name), 'resolvers': self._create_graphql_resolvers(name), 'dataloaders': self._create_dataloaders(name) } def _generate_database(self, name: str, options: Dict) -> Dict: """Generate database schemas and migrations""" db_type = options.get('database', 'postgresql') return { 'migration': self._create_migration(name, db_type), 'model': self._create_model(name, options.get('orm', 'prisma')), 'seed': self._create_seed_data(name) } def _generate_tests(self, name: str, options: Dict) -> Dict: """Generate test files""" return { 'unit_tests': { 'frontend': self._create_component_tests(name), 'backend': self._create_service_tests(name) }, 'integration_tests': self._create_integration_tests(name), 'e2e_tests': self._create_e2e_tests(name) } def _generate_docs(self, name: str, options: Dict) -> Dict: """Generate documentation""" return { 'api_docs': self._create_api_documentation(name), 'component_docs': self._create_component_documentation(name), 'readme': self._create_feature_readme(name) } def _create_react_component(self, name: str, typescript: bool) -> str: """Create React component""" pascal_name = self._to_pascal_case(name) if typescript: return f"""import React, {{ useState, useEffect }} from 'react'; import {{ use{pascal_name} }} from './hooks/use{pascal_name}'; import styles from './{pascal_name}.module.css'; interface {pascal_name}Props {{ id?: string; onSuccess?: (data: any) => void; onError?: (error: Error) => void; }} export const {pascal_name}: React.FC<{pascal_name}Props> = ({{ id, onSuccess, onError }}) => {{ const {{ data, loading, error, refetch }} = use{pascal_name}(id); if (loading) return
Loading...
; if (error) return
Error: {{error.message}}
; return (

{pascal_name}

{{/* Component content */}}
); }}; export default {pascal_name};""" else: return f"""import React, {{ useState, useEffect }} from 'react'; import {{ use{pascal_name} }} from './hooks/use{pascal_name}'; import styles from './{pascal_name}.module.css'; export const {pascal_name} = ({{ id, onSuccess, onError }}) => {{ const {{ data, loading, error, refetch }} = use{pascal_name}(id); if (loading) return
Loading...
; if (error) return
Error: {{error.message}}
; return (

{pascal_name}

{{/* Component content */}}
); }}; export default {pascal_name};""" def _create_custom_hook(self, name: str, typescript: bool) -> str: """Create custom React hook""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) if typescript: return f"""import {{ useState, useEffect }} from 'react'; import {{ fetch{pascal_name} }} from '../api/{camel_name}Api'; interface Use{pascal_name}Result {{ data: any | null; loading: boolean; error: Error | null; refetch: () => Promise; }} export const use{pascal_name} = (id?: string): Use{pascal_name}Result => {{ const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchData = async () => {{ try {{ setLoading(true); const result = await fetch{pascal_name}(id); setData(result); setError(null); }} catch (err) {{ setError(err as Error); }} finally {{ setLoading(false); }} }}; useEffect(() => {{ fetchData(); }}, [id]); return {{ data, loading, error, refetch: fetchData }}; }};""" else: return f"""import {{ useState, useEffect }} from 'react'; import {{ fetch{pascal_name} }} from '../api/{camel_name}Api'; export const use{pascal_name} = (id) => {{ const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchData = async () => {{ try {{ setLoading(true); const result = await fetch{pascal_name}(id); setData(result); setError(null); }} catch (err) {{ setError(err); }} finally {{ setLoading(false); }} }}; useEffect(() => {{ fetchData(); }}, [id]); return {{ data, loading, error, refetch: fetchData }}; }};""" def _create_api_client(self, name: str, typescript: bool) -> str: """Create API client""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) if typescript: return f"""import axios from 'axios'; const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001'; export interface {pascal_name}Data {{ id: string; // Add fields }} export const fetch{pascal_name} = async (id?: string): Promise<{pascal_name}Data> => {{ const url = id ? `${{API_BASE_URL}}/{camel_name}/${{id}}` : `${{API_BASE_URL}}/{camel_name}`; const response = await axios.get(url); return response.data; }}; export const create{pascal_name} = async (data: Partial<{pascal_name}Data>): Promise<{pascal_name}Data> => {{ const response = await axios.post(`${{API_BASE_URL}}/{camel_name}`, data); return response.data; }}; export const update{pascal_name} = async (id: string, data: Partial<{pascal_name}Data>): Promise<{pascal_name}Data> => {{ const response = await axios.put(`${{API_BASE_URL}}/{camel_name}/${{id}}`, data); return response.data; }}; export const delete{pascal_name} = async (id: string): Promise => {{ await axios.delete(`${{API_BASE_URL}}/{camel_name}/${{id}}`); }};""" else: return f"""import axios from 'axios'; const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001'; export const fetch{pascal_name} = async (id) => {{ const url = id ? `${{API_BASE_URL}}/{camel_name}/${{id}}` : `${{API_BASE_URL}}/{camel_name}`; const response = await axios.get(url); return response.data; }}; export const create{pascal_name} = async (data) => {{ const response = await axios.post(`${{API_BASE_URL}}/{camel_name}`, data); return response.data; }}; export const update{pascal_name} = async (id, data) => {{ const response = await axios.put(`${{API_BASE_URL}}/{camel_name}/${{id}}`, data); return response.data; }}; export const delete{pascal_name} = async (id) => {{ await axios.delete(`${{API_BASE_URL}}/{camel_name}/${{id}}`); }};""" def _create_controller(self, name: str, framework: str, typescript: bool) -> str: """Create backend controller""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) if typescript: return f"""import {{ Request, Response, NextFunction }} from 'express'; import {{ {pascal_name}Service }} from '../services/{camel_name}Service'; export class {pascal_name}Controller {{ private {camel_name}Service: {pascal_name}Service; constructor() {{ this.{camel_name}Service = new {pascal_name}Service(); }} async getAll(req: Request, res: Response, next: NextFunction) {{ try {{ const data = await this.{camel_name}Service.findAll(req.query); res.json({{ success: true, data }}); }} catch (error) {{ next(error); }} }} async getById(req: Request, res: Response, next: NextFunction) {{ try {{ const data = await this.{camel_name}Service.findById(req.params.id); if (!data) {{ return res.status(404).json({{ success: false, message: 'Not found' }}); }} res.json({{ success: true, data }}); }} catch (error) {{ next(error); }} }} async create(req: Request, res: Response, next: NextFunction) {{ try {{ const data = await this.{camel_name}Service.create(req.body); res.status(201).json({{ success: true, data }}); }} catch (error) {{ next(error); }} }} async update(req: Request, res: Response, next: NextFunction) {{ try {{ const data = await this.{camel_name}Service.update(req.params.id, req.body); res.json({{ success: true, data }}); }} catch (error) {{ next(error); }} }} async delete(req: Request, res: Response, next: NextFunction) {{ try {{ await this.{camel_name}Service.delete(req.params.id); res.status(204).send(); }} catch (error) {{ next(error); }} }} }} export default new {pascal_name}Controller();""" else: return f"""const {pascal_name}Service = require('../services/{camel_name}Service'); class {pascal_name}Controller {{ constructor() {{ this.{camel_name}Service = new {pascal_name}Service(); }} async getAll(req, res, next) {{ try {{ const data = await this.{camel_name}Service.findAll(req.query); res.json({{ success: true, data }}); }} catch (error) {{ next(error); }} }} async getById(req, res, next) {{ try {{ const data = await this.{camel_name}Service.findById(req.params.id); if (!data) {{ return res.status(404).json({{ success: false, message: 'Not found' }}); }} res.json({{ success: true, data }}); }} catch (error) {{ next(error); }} }} async create(req, res, next) {{ try {{ const data = await this.{camel_name}Service.create(req.body); res.status(201).json({{ success: true, data }}); }} catch (error) {{ next(error); }} }} async update(req, res, next) {{ try {{ const data = await this.{camel_name}Service.update(req.params.id, req.body); res.json({{ success: true, data }}); }} catch (error) {{ next(error); }} }} async delete(req, res, next) {{ try {{ await this.{camel_name}Service.delete(req.params.id); res.status(204).send(); }} catch (error) {{ next(error); }} }} }} module.exports = new {pascal_name}Controller();""" def _create_service(self, name: str, typescript: bool) -> str: """Create service layer""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) if typescript: return f"""import {{ {pascal_name}Repository }} from '../repositories/{camel_name}Repository'; export class {pascal_name}Service {{ private repository: {pascal_name}Repository; constructor() {{ this.repository = new {pascal_name}Repository(); }} async findAll(filters: any = {{}}) {{ // Add business logic here return await this.repository.findAll(filters); }} async findById(id: string) {{ // Add business logic here return await this.repository.findById(id); }} async create(data: any) {{ // Add validation and business logic return await this.repository.create(data); }} async update(id: string, data: any) {{ // Add validation and business logic return await this.repository.update(id, data); }} async delete(id: string) {{ // Add business logic here return await this.repository.delete(id); }} }} export default {pascal_name}Service;""" else: return f"""const {pascal_name}Repository = require('../repositories/{camel_name}Repository'); class {pascal_name}Service {{ constructor() {{ this.repository = new {pascal_name}Repository(); }} async findAll(filters = {{}}) {{ // Add business logic here return await this.repository.findAll(filters); }} async findById(id) {{ // Add business logic here return await this.repository.findById(id); }} async create(data) {{ // Add validation and business logic return await this.repository.create(data); }} async update(id, data) {{ // Add validation and business logic return await this.repository.update(id, data); }} async delete(id) {{ // Add business logic here return await this.repository.delete(id); }} }} module.exports = {pascal_name}Service;""" def _create_repository(self, name: str, typescript: bool) -> str: """Create repository layer""" pascal_name = self._to_pascal_case(name) snake_name = self._to_snake_case(name) if typescript: return f"""import {{ PrismaClient }} from '@prisma/client'; export class {pascal_name}Repository {{ private prisma: PrismaClient; constructor() {{ this.prisma = new PrismaClient(); }} async findAll(filters: any = {{}}) {{ return await this.prisma.{snake_name}.findMany({{ where: filters }}); }} async findById(id: string) {{ return await this.prisma.{snake_name}.findUnique({{ where: {{ id }} }}); }} async create(data: any) {{ return await this.prisma.{snake_name}.create({{ data }}); }} async update(id: string, data: any) {{ return await this.prisma.{snake_name}.update({{ where: {{ id }}, data }}); }} async delete(id: string) {{ return await this.prisma.{snake_name}.delete({{ where: {{ id }} }}); }} }} export default {pascal_name}Repository;""" else: return f"""const {{ PrismaClient }} = require('@prisma/client'); class {pascal_name}Repository {{ constructor() {{ this.prisma = new PrismaClient(); }} async findAll(filters = {{}}) {{ return await this.prisma.{snake_name}.findMany({{ where: filters }}); }} async findById(id) {{ return await this.prisma.{snake_name}.findUnique({{ where: {{ id }} }}); }} async create(data) {{ return await this.prisma.{snake_name}.create({{ data }}); }} async update(id, data) {{ return await this.prisma.{snake_name}.update({{ where: {{ id }}, data }}); }} async delete(id) {{ return await this.prisma.{snake_name}.delete({{ where: {{ id }} }}); }} }} module.exports = {pascal_name}Repository;""" def _create_routes(self, name: str, framework: str) -> str: """Create API routes""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) return f"""import {{ Router }} from 'express'; import {camel_name}Controller from '../controllers/{camel_name}Controller'; import {{ validate }} from '../middleware/validation'; import {{ authenticate }} from '../middleware/auth'; import {{ {camel_name}Schema }} from '../validations/{camel_name}Validation'; const router = Router(); router.get('/', authenticate, {camel_name}Controller.getAll); router.get('/:id', authenticate, {camel_name}Controller.getById); router.post('/', authenticate, validate({camel_name}Schema), {camel_name}Controller.create); router.put('/:id', authenticate, validate({camel_name}Schema), {camel_name}Controller.update); router.delete('/:id', authenticate, {camel_name}Controller.delete); export default router;""" def _create_middleware(self, name: str) -> str: """Create middleware""" return f"""export const {name}Middleware = (req, res, next) => {{ // Add middleware logic console.log('{name} middleware'); next(); }};""" def _create_validation(self, name: str) -> str: """Create validation schema""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) return f"""import Joi from 'joi'; export const {camel_name}Schema = Joi.object({{ name: Joi.string().required().min(3).max(100), description: Joi.string().optional().max(500), status: Joi.string().valid('active', 'inactive').default('active'), // Add more fields as needed }}); export const update{pascal_name}Schema = Joi.object({{ name: Joi.string().optional().min(3).max(100), description: Joi.string().optional().max(500), status: Joi.string().valid('active', 'inactive').optional(), }});""" def _create_graphql_schema(self, name: str) -> str: """Create GraphQL schema""" pascal_name = self._to_pascal_case(name) return f"""type {pascal_name} {{ id: ID! name: String! description: String status: Status! createdAt: DateTime! updatedAt: DateTime! }} enum Status {{ ACTIVE INACTIVE }} input {pascal_name}Input {{ name: String! description: String status: Status }} input {pascal_name}UpdateInput {{ name: String description: String status: Status }} type Query {{ {name}(id: ID!): {pascal_name} all{pascal_name}s(limit: Int, offset: Int): [{pascal_name}!]! }} type Mutation {{ create{pascal_name}(input: {pascal_name}Input!): {pascal_name}! update{pascal_name}(id: ID!, input: {pascal_name}UpdateInput!): {pascal_name}! delete{pascal_name}(id: ID!): Boolean! }}""" def _create_graphql_resolvers(self, name: str) -> str: """Create GraphQL resolvers""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) return f"""import {{ {pascal_name}Service }} from '../services/{camel_name}Service'; const {camel_name}Service = new {pascal_name}Service(); export const {camel_name}Resolvers = {{ Query: {{ {name}: async (_, {{ id }}) => {{ return await {camel_name}Service.findById(id); }}, all{pascal_name}s: async (_, {{ limit = 10, offset = 0 }}) => {{ return await {camel_name}Service.findAll({{ limit, offset }}); }} }}, Mutation: {{ create{pascal_name}: async (_, {{ input }}) => {{ return await {camel_name}Service.create(input); }}, update{pascal_name}: async (_, {{ id, input }}) => {{ return await {camel_name}Service.update(id, input); }}, delete{pascal_name}: async (_, {{ id }}) => {{ await {camel_name}Service.delete(id); return true; }} }} }};""" def _create_dataloaders(self, name: str) -> str: """Create DataLoader for N+1 query prevention""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) return f"""import DataLoader from 'dataloader'; import {{ {pascal_name}Repository }} from '../repositories/{camel_name}Repository'; const repository = new {pascal_name}Repository(); export const create{pascal_name}Loader = () => {{ return new DataLoader(async (ids) => {{ const items = await repository.findByIds(ids); const itemMap = {{}}; items.forEach(item => {{ itemMap[item.id] = item; }}); return ids.map(id => itemMap[id]); }}); }};""" def _create_migration(self, name: str, db_type: str) -> str: """Create database migration""" snake_name = self._to_snake_case(name) table_name = f"{snake_name}s" if db_type == 'postgresql': return f"""-- Migration: Create {table_name} table -- Timestamp: {{new Date().toISOString()}} CREATE TABLE IF NOT EXISTS {table_name} ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, description TEXT, status VARCHAR(50) DEFAULT 'active', created_by UUID REFERENCES users(id), updated_by UUID REFERENCES users(id), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Indexes CREATE INDEX idx_{table_name}_status ON {table_name}(status); CREATE INDEX idx_{table_name}_created_at ON {table_name}(created_at DESC); -- Trigger for updated_at CREATE TRIGGER update_{table_name}_updated_at BEFORE UPDATE ON {table_name} FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();""" else: return f"""-- Migration for {table_name} CREATE TABLE {table_name} ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, description TEXT, status VARCHAR(50) DEFAULT 'active', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );""" def _create_model(self, name: str, orm: str) -> str: """Create ORM model""" pascal_name = self._to_pascal_case(name) snake_name = self._to_snake_case(name) if orm == 'prisma': return f"""model {pascal_name} {{ id String @id @default(uuid()) name String description String? status Status @default(ACTIVE) createdBy String? updatedBy String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("{snake_name}s") }} enum Status {{ ACTIVE INACTIVE }}""" else: return f"""// Sequelize model const {{ Model, DataTypes }} = require('sequelize'); class {pascal_name} extends Model {{ static init(sequelize) {{ super.init({{ name: {{ type: DataTypes.STRING, allowNull: false }}, description: {{ type: DataTypes.TEXT, allowNull: true }}, status: {{ type: DataTypes.ENUM('active', 'inactive'), defaultValue: 'active' }} }}, {{ sequelize, modelName: '{pascal_name}', tableName: '{snake_name}s' }}); }} }} module.exports = {pascal_name};""" def _create_seed_data(self, name: str) -> str: """Create seed data""" pascal_name = self._to_pascal_case(name) return f"""// Seed data for {pascal_name} export const {name}Seeds = [ {{ name: 'Sample {pascal_name} 1', description: 'This is a sample {name}', status: 'active' }}, {{ name: 'Sample {pascal_name} 2', description: 'Another sample {name}', status: 'active' }}, {{ name: 'Sample {pascal_name} 3', description: 'Inactive sample {name}', status: 'inactive' }} ];""" def _create_component_tests(self, name: str) -> str: """Create component tests""" pascal_name = self._to_pascal_case(name) return f"""import {{ render, screen, waitFor, fireEvent }} from '@testing-library/react'; import {{ {pascal_name} }} from '../{pascal_name}'; describe('{pascal_name}', () => {{ it('renders without crashing', () => {{ render(<{pascal_name} />); expect(screen.getByText('{pascal_name}')).toBeInTheDocument(); }}); it('shows loading state', () => {{ render(<{pascal_name} />); expect(screen.getByText('Loading...')).toBeInTheDocument(); }}); it('displays error state', async () => {{ // Mock error scenario render(<{pascal_name} />); await waitFor(() => {{ expect(screen.getByText(/Error:/)).toBeInTheDocument(); }}); }}); it('handles user interaction', async () => {{ render(<{pascal_name} />); // Add interaction tests }}); }});""" def _create_service_tests(self, name: str) -> str: """Create service tests""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) return f"""import {{ {pascal_name}Service }} from '../services/{camel_name}Service'; describe('{pascal_name}Service', () => {{ let service; beforeEach(() => {{ service = new {pascal_name}Service(); }}); describe('findAll', () => {{ it('returns all items', async () => {{ const result = await service.findAll(); expect(Array.isArray(result)).toBe(true); }}); }}); describe('findById', () => {{ it('returns single item', async () => {{ const result = await service.findById('test-id'); expect(result).toBeDefined(); }}); }}); describe('create', () => {{ it('creates new item', async () => {{ const data = {{ name: 'Test' }}; const result = await service.create(data); expect(result.name).toBe('Test'); }}); }}); }});""" def _create_integration_tests(self, name: str) -> str: """Create integration tests""" camel_name = self._to_camel_case(name) return f"""import request from 'supertest'; import app from '../app'; describe('/{camel_name} endpoints', () => {{ describe('GET /{camel_name}', () => {{ it('returns list of items', async () => {{ const res = await request(app) .get('/{camel_name}') .expect(200); expect(res.body.success).toBe(true); expect(Array.isArray(res.body.data)).toBe(true); }}); }}); describe('POST /{camel_name}', () => {{ it('creates new item', async () => {{ const res = await request(app) .post('/{camel_name}') .send({{ name: 'Test Item' }}) .expect(201); expect(res.body.success).toBe(true); expect(res.body.data.name).toBe('Test Item'); }}); }}); }});""" def _create_e2e_tests(self, name: str) -> str: """Create E2E tests""" pascal_name = self._to_pascal_case(name) return f"""import {{ test, expect }} from '@playwright/test'; test.describe('{pascal_name} Feature', () => {{ test.beforeEach(async ({{ page }}) => {{ await page.goto('http://localhost:3000/{name}'); }}); test('displays {name} page', async ({{ page }}) => {{ await expect(page.locator('h2')).toContainText('{pascal_name}'); }}); test('creates new {name}', async ({{ page }}) => {{ await page.click('button:has-text("New")'); await page.fill('input[name="name"]', 'Test {pascal_name}'); await page.click('button:has-text("Save")'); await expect(page.locator('.success')).toBeVisible(); }}); test('edits existing {name}', async ({{ page }}) => {{ await page.click('.edit-button:first-child'); await page.fill('input[name="name"]', 'Updated {pascal_name}'); await page.click('button:has-text("Update")'); await expect(page.locator('.success')).toBeVisible(); }}); }});""" def _create_api_documentation(self, name: str) -> str: """Create API documentation""" camel_name = self._to_camel_case(name) pascal_name = self._to_pascal_case(name) return f"""# {pascal_name} API Documentation ## Endpoints ### GET /{camel_name} Retrieve all {name} items. **Response:** ```json {{ "success": true, "data": [ {{ "id": "uuid", "name": "string", "description": "string", "status": "active|inactive", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" }} ] }} ``` ### GET /{camel_name}/:id Retrieve single {name} by ID. ### POST /{camel_name} Create new {name}. **Request Body:** ```json {{ "name": "string", "description": "string", "status": "active|inactive" }} ``` ### PUT /{camel_name}/:id Update existing {name}. ### DELETE /{camel_name}/:id Delete {name} by ID. ## Error Responses - 400: Bad Request - Invalid input data - 401: Unauthorized - Missing or invalid authentication - 404: Not Found - Resource does not exist - 500: Internal Server Error """ def _create_component_documentation(self, name: str) -> str: """Create component documentation""" pascal_name = self._to_pascal_case(name) return f"""# {pascal_name} Component ## Usage ```jsx import {{ {pascal_name} }} from './components/{pascal_name}'; function App() {{ return ( <{pascal_name} id="optional-id" onSuccess={{handleSuccess}} onError={{handleError}} /> ); }} ``` ## Props | Prop | Type | Required | Description | |------|------|----------|-------------| | id | string | No | ID of item to display | | onSuccess | function | No | Callback on successful operation | | onError | function | No | Callback on error | ## Hooks ### use{pascal_name} Custom hook for {name} data management. ```jsx const {{ data, loading, error, refetch }} = use{pascal_name}(id); ``` """ def _create_feature_readme(self, name: str) -> str: """Create feature README""" pascal_name = self._to_pascal_case(name) return f"""# {pascal_name} Feature ## Overview Full-stack implementation of {pascal_name} functionality. ## Structure ``` {name}/ ├── frontend/ │ ├── components/ │ ├── hooks/ │ ├── api/ │ └── tests/ ├── backend/ │ ├── controllers/ │ ├── services/ │ ├── repositories/ │ ├── routes/ │ └── tests/ ├── database/ │ ├── migrations/ │ └── seeds/ └── docs/ ``` ## Setup 1. Install dependencies: ```bash npm install ``` 2. Run migrations: ```bash npm run migrate ``` 3. Seed database: ```bash npm run seed ``` 4. Start development: ```bash npm run dev ``` ## Testing ```bash # Unit tests npm run test:unit # Integration tests npm run test:integration # E2E tests npm run test:e2e ``` ## API Documentation See [API.md](./docs/API.md) ## Component Documentation See [COMPONENT.md](./docs/COMPONENT.md) """ def _create_styles(self, name: str, approach: str) -> str: """Create styles based on approach""" pascal_name = self._to_pascal_case(name) if approach == 'modules': return f""".container {{ padding: 1rem; border-radius: 8px; background: white; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); }} .loading {{ display: flex; justify-content: center; align-items: center; min-height: 200px; color: #666; }} .error {{ padding: 1rem; background: #fee; color: #c00; border-radius: 4px; margin: 1rem 0; }}""" else: return f"""import styled from 'styled-components'; export const Container = styled.div` padding: 1rem; border-radius: 8px; background: white; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); `; export const Loading = styled.div` display: flex; justify-content: center; align-items: center; min-height: 200px; color: #666; `; export const Error = styled.div` padding: 1rem; background: #fee; color: #c00; border-radius: 4px; margin: 1rem 0; `;""" def _create_state_management(self, name: str, lib: str) -> str: """Create state management""" pascal_name = self._to_pascal_case(name) camel_name = self._to_camel_case(name) if lib == 'redux': return f"""import {{ createSlice, createAsyncThunk }} from '@reduxjs/toolkit'; import {{ fetch{pascal_name} }} from '../api/{camel_name}Api'; export const get{pascal_name} = createAsyncThunk( '{camel_name}/fetch', async (id) => {{ const response = await fetch{pascal_name}(id); return response; }} ); const {camel_name}Slice = createSlice({{ name: '{camel_name}', initialState: {{ data: null, loading: false, error: null }}, reducers: {{}}, extraReducers: (builder) => {{ builder .addCase(get{pascal_name}.pending, (state) => {{ state.loading = true; state.error = null; }}) .addCase(get{pascal_name}.fulfilled, (state, action) => {{ state.loading = false; state.data = action.payload; }}) .addCase(get{pascal_name}.rejected, (state, action) => {{ state.loading = false; state.error = action.error.message; }}); }} }}); export default {camel_name}Slice.reducer;""" else: return f"""import React, {{ createContext, useContext, useReducer }} from 'react'; const {pascal_name}Context = createContext(); const initialState = {{ data: null, loading: false, error: null }}; function {camel_name}Reducer(state, action) {{ switch (action.type) {{ case 'FETCH_START': return {{ ...state, loading: true, error: null }}; case 'FETCH_SUCCESS': return {{ ...state, loading: false, data: action.payload }}; case 'FETCH_ERROR': return {{ ...state, loading: false, error: action.payload }}; default: return state; }} }} export function {pascal_name}Provider({{ children }}) {{ const [state, dispatch] = useReducer({camel_name}Reducer, initialState); return ( <{pascal_name}Context.Provider value={{ state, dispatch }}> {{children}} ); }} export const use{pascal_name}Context = () => useContext({pascal_name}Context);""" def _create_types(self, name: str) -> str: """Create TypeScript types""" pascal_name = self._to_pascal_case(name) return f"""export interface {pascal_name} {{ id: string; name: string; description?: string; status: 'active' | 'inactive'; createdAt: Date; updatedAt: Date; createdBy?: string; updatedBy?: string; }} export interface {pascal_name}Input {{ name: string; description?: string; status?: 'active' | 'inactive'; }} export interface {pascal_name}Filter {{ status?: 'active' | 'inactive'; search?: string; limit?: number; offset?: number; }} export interface {pascal_name}Response {{ success: boolean; data: {pascal_name} | {pascal_name}[]; message?: string; error?: string; }}""" def _get_react_templates(self) -> Dict: """Get React templates""" return {} def _get_nextjs_templates(self) -> Dict: """Get Next.js templates""" return {} def _get_express_templates(self) -> Dict: """Get Express templates""" return {} def _get_graphql_templates(self) -> Dict: """Get GraphQL templates""" return {} def _to_pascal_case(self, name: str) -> str: """Convert to PascalCase""" return ''.join(word.capitalize() for word in name.split('_')) def _to_camel_case(self, name: str) -> str: """Convert to camelCase""" pascal = self._to_pascal_case(name) return pascal[0].lower() + pascal[1:] if pascal else '' def _to_snake_case(self, name: str) -> str: """Convert to snake_case""" return name.lower().replace(' ', '_').replace('-', '_') def write_files(self, feature: Dict, output_dir: str = '.') -> List[str]: """Write generated files to disk""" created_files = [] base_path = Path(output_dir) # Create directory structure feature_name = feature['name'] feature_path = base_path / feature_name # Frontend files if feature['frontend']: frontend_path = feature_path / 'frontend' # Component if feature['frontend'].get('component'): comp_file = frontend_path / 'components' / f"{self._to_pascal_case(feature_name)}.tsx" comp_file.parent.mkdir(parents=True, exist_ok=True) comp_file.write_text(feature['frontend']['component']) created_files.append(str(comp_file)) # Styles if feature['frontend'].get('styles'): style_file = frontend_path / 'components' / f"{self._to_pascal_case(feature_name)}.module.css" style_file.parent.mkdir(parents=True, exist_ok=True) style_file.write_text(feature['frontend']['styles']) created_files.append(str(style_file)) # Hooks if feature['frontend'].get('hooks'): hook_file = frontend_path / 'hooks' / f"use{self._to_pascal_case(feature_name)}.ts" hook_file.parent.mkdir(parents=True, exist_ok=True) hook_file.write_text(feature['frontend']['hooks']) created_files.append(str(hook_file)) # Backend files if feature['backend']: backend_path = feature_path / 'backend' # Controller if feature['backend'].get('controller'): ctrl_file = backend_path / 'controllers' / f"{self._to_camel_case(feature_name)}Controller.ts" ctrl_file.parent.mkdir(parents=True, exist_ok=True) ctrl_file.write_text(feature['backend']['controller']) created_files.append(str(ctrl_file)) # Service if feature['backend'].get('service'): svc_file = backend_path / 'services' / f"{self._to_camel_case(feature_name)}Service.ts" svc_file.parent.mkdir(parents=True, exist_ok=True) svc_file.write_text(feature['backend']['service']) created_files.append(str(svc_file)) # Repository if feature['backend'].get('repository'): repo_file = backend_path / 'repositories' / f"{self._to_camel_case(feature_name)}Repository.ts" repo_file.parent.mkdir(parents=True, exist_ok=True) repo_file.write_text(feature['backend']['repository']) created_files.append(str(repo_file)) # Database files if feature['database']: db_path = feature_path / 'database' # Migration if feature['database'].get('migration'): mig_file = db_path / 'migrations' / f"001_create_{self._to_snake_case(feature_name)}.sql" mig_file.parent.mkdir(parents=True, exist_ok=True) mig_file.write_text(feature['database']['migration']) created_files.append(str(mig_file)) # Documentation if feature['documentation']: docs_path = feature_path / 'docs' docs_path.mkdir(parents=True, exist_ok=True) if feature['documentation'].get('readme'): readme_file = feature_path / 'README.md' readme_file.write_text(feature['documentation']['readme']) created_files.append(str(readme_file)) return created_files def main(): import sys import argparse parser = argparse.ArgumentParser(description='Scaffold full-stack features') parser.add_argument('feature_name', help='Name of the feature') parser.add_argument('--stack', default='react-express', help='Tech stack') parser.add_argument('--typescript', action='store_true', help='Use TypeScript') parser.add_argument('--graphql', action='store_true', help='Include GraphQL') parser.add_argument('--output', default='.', help='Output directory') parser.add_argument('--write', action='store_true', help='Write files to disk') args = parser.parse_args() scaffolder = FullstackScaffolder() options = { 'stack': args.stack, 'typescript': args.typescript, 'frontend_framework': 'react', 'backend_framework': 'express', 'database': 'postgresql', 'orm': 'prisma' } if args.graphql: options['stack'] += '-graphql' feature = scaffolder.scaffold_feature(args.feature_name, options) if args.write: files = scaffolder.write_files(feature, args.output) print(f"✅ Created {len(files)} files for {args.feature_name} feature") for f in files[:10]: # Show first 10 print(f" 📁 {f}") else: # Display summary print("=" * 60) print(f"FULLSTACK FEATURE: {args.feature_name}") print("=" * 60) print("\n📦 GENERATED STRUCTURE:") if feature['frontend']: print("\nFrontend:") for key in feature['frontend']: if feature['frontend'][key]: print(f" ✓ {key}") if feature['backend']: print("\nBackend:") for key in feature['backend']: if feature['backend'][key]: print(f" ✓ {key}") if feature['database']: print("\nDatabase:") for key in feature['database']: if feature['database'][key]: print(f" ✓ {key}") print(f"\nTo write files: python {sys.argv[0]} {args.feature_name} --write") if __name__ == "__main__": main()