Skip to Content
UNIMAST
DocumentaciónDesarrollarEntorno y Configuración

Entorno y Configuración

UNIMAST ERP está configurado para funcionar en múltiples entornos con una configuración robusta que incluye Next.js 15, TypeScript, Tailwind CSS, Prisma ORM, y servicios externos como AWS S3, Redis y PostgreSQL.

🚀 Configuración de Next.js

Configuración Principal

// app/next.config.ts import CopyPlugin from 'copy-webpack-plugin'; export default { output: 'standalone', // Build standalone para Docker devIndicators: { position: 'bottom-right' // Indicadores de desarrollo }, experimental: { nodeMiddleware: true, // Middleware de Node.js serverActions: { bodySizeLimit: '50mb' // Límite de tamaño para Server Actions } }, images: { remotePatterns: [ { protocol: 'https', hostname: 'avatars.githubusercontent.com', // GitHub avatars search: '' }, { protocol: 'https', hostname: '*.public.blob.vercel-storage.com', // Vercel Blob Storage search: '' } ] }, webpack: (config: any) => { // Plugin para copiar esquemas eCF durante el build config.plugins.push( new CopyPlugin({ patterns: [ { from: './lib/ecf/schemas', // Esquemas XML de eCF to: './schemas' // Copiados al build } ] }) ); return config; } };

Características de Build

  • Standalone Output: Optimizado para contenedores Docker
  • Server Actions: Soporte para acciones del servidor con límite de 50MB
  • Image Optimization: Patrones remotos para GitHub y Vercel
  • Webpack Customization: Copia automática de esquemas eCF

🎨 Configuración de Tailwind CSS

Configuración del Tema

// app/tailwind.config.ts import type { Config } from 'tailwindcss'; export default { darkMode: ['class'], // Modo oscuro basado en clases content: [ './pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', // App Router './src/**/*.{ts,tsx}' ], prefix: '', // Sin prefijo personalizado theme: { container: { center: true, padding: '2rem', screens: { '2xl': '1400px' } }, extend: { colors: { // Sistema de colores CSS variables border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))', 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))' }, popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))' }, card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))' } }, borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' }, keyframes: { 'accordion-down': { from: { height: '0' }, to: { height: 'var(--radix-accordion-content-height)' } }, 'accordion-up': { from: { height: 'var(--radix-accordion-content-height)' }, to: { height: '0' } } }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out' } } }, plugins: [require('tailwindcss-animate')] // Animaciones CSS } satisfies Config;

Configuración de PostCSS

// app/postcss.config.js module.exports = { plugins: { tailwindcss: {}, // Procesamiento de Tailwind autoprefixer: {} // Autoprefijos CSS } };

Configuración de shadcn/ui

// app/components.json { "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", // Estilo de componentes "rsc": true, // React Server Components "tsx": true, // Soporte TypeScript JSX "tailwind": { "config": "tailwind.config.js", "css": "app/globals.css", "baseColor": "slate", // Color base del tema "cssVariables": true, // Variables CSS habilitadas "prefix": "" }, "aliases": { "components": "@/components", // Alias para componentes "utils": "@/lib/utils" // Alias para utilidades } }

🔧 Configuración de TypeScript

Configuración del Compilador

// app/tsconfig.json { "compilerOptions": { "target": "es5", // Target ES5 para compatibilidad "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, // Permitir archivos JavaScript "skipLibCheck": true, // Saltar verificación de librerías "strict": true, // Modo estricto habilitado "forceConsistentCasingInFileNames": true, "noEmit": true, // No generar archivos de salida "esModuleInterop": true, // Interoperabilidad de módulos ES "module": "esnext", // Sistema de módulos ES "moduleResolution": "node", // Resolución de módulos Node.js "resolveJsonModule": true, // Resolver módulos JSON "isolatedModules": true, // Módulos aislados "jsx": "preserve", // Preservar JSX "incremental": true, // Compilación incremental "baseUrl": ".", // URL base para imports "paths": { "@/*": ["./*"] // Alias de paths }, "plugins": [ { "name": "next" // Plugin de Next.js } ] }, "include": [ "next-env.d.ts", // Tipos de Next.js "**/*.ts", // Archivos TypeScript "**/*.tsx", // Archivos TSX ".next/types/**/*.ts" // Tipos generados ], "exclude": ["node_modules"] }

🗄️ Configuración de Base de Datos

Configuración de Prisma

// app/prisma/schema.prisma generator client { provider = "prisma-client-js" engineType = "binary" // Motor binario para mejor performance } datasource db { provider = "postgresql" // Base de datos PostgreSQL url = env("DATABASE_URL") // URL desde variables de entorno }

Cliente de Prisma

// app/lib/prisma.ts import { PrismaClient } from '@prisma/client'; let prisma: PrismaClient; if (process.env.NODE_ENV === 'production') { prisma = new PrismaClient(); } else { // Singleton en desarrollo para evitar múltiples conexiones if (!(global as any).prisma) { (global as any).prisma = new PrismaClient(); } prisma = (global as any).prisma; } export default prisma;

Scripts de Prisma

// app/package.json { "prisma": { "seed": "tsx prisma/seeds" // Script de seeding con tsx } }

🔐 Variables de Entorno

Variables Principales

# .env # Base de Datos DATABASE_URL="postgresql://user:password@localhost:5432/unimast_erp" # Redis para Colas de Trabajo REDIS_URL="redis://localhost:6379" # Entorno eCF (TesteCF | CerteCF | eCF) ECF_ENV="TesteCF" # Clave de Encriptación para Certificados ENCRYPTION_KEY="your-secret-encryption-key" # AWS S3 para Almacenamiento AWS_REGION="us-east-1" AWS_ACCESS_KEY_ID="your-access-key" AWS_SECRET_ACCESS_KEY="your-secret-key" AWS_BUCKET_NAME="efc-unimast" AWS_ENDPOINT="" # Vacío para AWS S3 estándar # Configuración del Servidor NODE_ENV="development" PORT="3000" HOST="localhost" HTTPS_ONLY="false" # Solo en producción

Uso en el Código

// app/lib/constants.ts import { ENVIRONMENT } from 'dgii-ecf/dist/networking/restClient'; const ENV_MAP: Record<string, ENVIRONMENT> = { TesteCF: ENVIRONMENT.DEV, // Entorno de desarrollo CerteCF: ENVIRONMENT.CERT, // Entorno de certificación eCF: ENVIRONMENT.PROD // Entorno de producción }; export const ECF_ENV = ENV_MAP[process.env.ECF_ENV ?? ''] || ENVIRONMENT.DEV;
// app/lib/queue.ts export function queueSendTaxDocument(encf: string) { const connection = new IORedis(process.env.REDIS_URL as string); // ... resto del código }
// app/lib/storage/s3.ts class S3FileStorage { constructor() { this.client = new S3({ region: process.env.AWS_REGION || 'us-east-1', endpoint: process.env.ENDPOINT || '', forcePathStyle: true, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' } }); this.bucketName = process.env.AWS_BUCKET_NAME || 'efc-unimast'; } }

🐳 Configuración de Docker

Dockerfile Multi-stage

# app/Dockerfile FROM node:lts-alpine AS base # Stage 1: Instalar dependencias FROM base AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN corepack enable pnpm && pnpm install --frozen-lockfile COPY prisma ./prisma RUN pnpm prisma generate # Stage 2: Construir la aplicación FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN corepack enable pnpm && pnpm run build # Stage 3: Servidor de producción FROM base AS runner WORKDIR /app ARG PORT=3000 ENV PORT=$PORT \ NODE_ENV=production COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/lib ./lib COPY --from=builder /app/tsconfig.json ./tsconfig.json RUN if [ -d "/app/public" ]; then cp -r /app/public ./public; fi EXPOSE ${PORT} CMD ["node", "server.js"]

Docker Compose (Recomendado)

# docker-compose.yml version: '3.8' services: app: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DATABASE_URL=postgresql://user:password@db:5432/unimast_erp - REDIS_URL=redis://redis:6379 depends_on: - db - redis db: image: postgres:15-alpine environment: - POSTGRES_DB=unimast_erp - POSTGRES_USER=user - POSTGRES_PASSWORD=password volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis_data:/data volumes: postgres_data: redis_data:

📦 Dependencias del Proyecto

Dependencias Principales

// app/package.json { "dependencies": { // Framework y React "next": "15.2.2-canary.1", "react": "19.0.0", "react-dom": "19.0.0", // TypeScript y Build Tools "typescript": "5.7.2", "@types/node": "20.17.6", "@types/react": "19.0.0", // UI y Styling "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^2.6.0", "framer-motion": "^12.4.7", // Componentes UI "@radix-ui/react-*": "^1.1.0", "lucide-react": "^0.400.0", "sonner": "^2.0.1", // Formularios y Validación "react-hook-form": "^7.54.2", "@hookform/resolvers": "^4.1.3", "zod": "^3.25.76", // Base de Datos "@prisma/client": "6.4.1", "pg": "^8.13.3", // Colas de Trabajo "bullmq": "^5.53.1", "ioredis": "^5.6.1", // Almacenamiento "@aws-sdk/client-s3": "^3.798.0", // Facturación Electrónica "dgii-ecf": "^1.5.3", // Utilidades "bcryptjs": "^3.0.2", "date-fns": "^4.1.0", "dayjs": "^1.11.13" }, "devDependencies": { "prisma": "6.4.1", "eslint": "^9", "eslint-config-next": "15.2.2", "tsx": "^4.19.3" } }

Gestor de Paquetes

// app/package.json { "scripts": { "dev": "next dev --turbopack", // Desarrollo con Turbopack "build": "next build", // Build de producción "start": "next start", // Servidor de producción "lint": "eslint . --ext .ts,.tsx", // Linting "lint:fix": "eslint . --ext .ts,.tsx --fix" // Linting con auto-fix } }

🔍 Configuración de ESLint

Configuración Flat Config

// app/eslint.config.mjs import { FlatCompat } from '@eslint/eslintrc' const compat = new FlatCompat({ baseDirectory: import.meta.dirname, }) const eslintConfig = [ ...compat.config({ plugins: ['simple-import-sort'], // Plugin de ordenamiento extends: ['next'], // Configuración base de Next.js rules: { 'react/no-unescaped-entities': 'off', '@next/next/no-page-custom-font': 'off', 'react-hooks/exhaustive-deps': 0, // Deshabilitar regla de dependencias 'simple-import-sort/imports': 'error', // Ordenar imports 'simple-import-sort/exports': 'error', // Ordenar exports 'no-unused-vars': 'warn', // Warning para variables no usadas 'eol-last': ['error', 'always'] // Nueva línea al final }, ignorePatterns: [ 'next.config.ts', 'tailwind.config.js', '.next', 'node_modules' ], }), ] export default eslintConfig

Configuración de Prettier

// app/package.json { "prettier": { "arrowParens": "always", // Paréntesis siempre en arrow functions "singleQuote": true, // Comillas simples "tabWidth": 2, // Ancho de tabulación "trailingComma": "none", // Sin coma final "semi": true // Punto y coma al final } }

🚀 Scripts de Desarrollo

Scripts Disponibles

# Instalar dependencias pnpm install # Desarrollo con Turbopack pnpm dev # Build de producción pnpm build # Servidor de producción pnpm start # Linting pnpm lint # Linting con auto-fix pnpm lint:fix # Generar cliente de Prisma pnpm prisma generate # Ejecutar migraciones pnpm prisma migrate dev # Ejecutar seed de base de datos pnpm prisma db seed # Studio de Prisma pnpm prisma studio

Configuración de Turbopack

# app/package.json "dev": "next dev --turbopack"

Turbopack es el nuevo bundler de Next.js que ofrece:

  • Hot Reload más rápido que Webpack
  • Mejor performance en desarrollo
  • Compatibilidad con la mayoría de configuraciones de Webpack

🌍 Entornos de Despliegue

Entorno de Desarrollo

NODE_ENV=development ECF_ENV=TesteCF DATABASE_URL=postgresql://localhost:5432/unimast_erp_dev REDIS_URL=redis://localhost:6379

Entorno de Staging

NODE_ENV=staging ECF_ENV=CerteCF DATABASE_URL=postgresql://staging:5432/unimast_erp_staging REDIS_URL=redis://staging:6379

Entorno de Producción

NODE_ENV=production ECF_ENV=eCF DATABASE_URL=postgresql://prod:5432/unimast_erp_prod REDIS_URL=redis://prod:6379 HTTPS_ONLY=true

🔧 Configuración de Middleware

Middleware de Next.js

// app/middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { // Lógica de middleware para autenticación y autorización // Ver implementación en auth-and-authorization.mdx } export const config = { matcher: [ // Rutas que requieren middleware '/((?!api|_next/static|_next/image|favicon.ico).*)', ], };

📱 Configuración de Imágenes

Optimización de Imágenes

// app/next.config.ts images: { remotePatterns: [ { protocol: 'https', hostname: 'avatars.githubusercontent.com', // GitHub avatars search: '' }, { protocol: 'https', hostname: '*.public.blob.vercel-storage.com', // Vercel Blob Storage search: '' } ] }

Uso en Componentes

import Image from 'next/image'; // Imagen local <Image src="/logo.png" alt="Logo" width={200} height={100} /> // Imagen remota (desde patrones configurados) <Image src="https://avatars.githubusercontent.com/u/12345?v=4" alt="Avatar" width={40} height={40} />

🚀 Mejores Prácticas

1. Variables de Entorno

// ✅ CORRECTO: Validar variables críticas al inicio const requiredEnvVars = [ 'DATABASE_URL', 'REDIS_URL', 'ENCRYPTION_KEY' ] as const; requiredEnvVars.forEach(envVar => { if (!process.env[envVar]) { throw new Error(`Missing required environment variable: ${envVar}`); } }); // ❌ INCORRECTO: Usar variables sin validar const dbUrl = process.env.DATABASE_URL; // Podría ser undefined

2. Configuración de Base de Datos

// ✅ CORRECTO: Singleton pattern para Prisma let prisma: PrismaClient; if (process.env.NODE_ENV === 'production') { prisma = new PrismaClient(); } else { if (!(global as any).prisma) { (global as any).prisma = new PrismaClient(); } prisma = (global as any).prisma; } // ❌ INCORRECTO: Crear nueva instancia en cada import export default new PrismaClient(); // Múltiples conexiones en desarrollo

3. Configuración de Tailwind

// ✅ CORRECTO: Usar CSS variables para temas colors: { primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' } } // ❌ INCORRECTO: Colores hardcodeados colors: { primary: '#007bff', // No es flexible para temas primaryDark: '#0056b3' }

4. Configuración de Docker

# ✅ CORRECTO: Multi-stage build FROM base AS deps COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile FROM base AS builder COPY --from=deps /app/node_modules ./node_modules COPY . . RUN pnpm run build # ❌ INCORRECTO: Build monolítico FROM node:lts-alpine COPY . . RUN pnpm install && pnpm run build

🔧 Troubleshooting

Problemas Comunes

1. Error de Variables de Entorno

# Verificar que el archivo .env esté en la raíz del proyecto ls -la .env # Verificar que las variables estén definidas echo $DATABASE_URL echo $REDIS_URL # En desarrollo, reiniciar el servidor después de cambios pnpm dev

2. Error de Base de Datos

# Verificar conexión a PostgreSQL psql $DATABASE_URL # Verificar que Prisma esté generado pnpm prisma generate # Verificar migraciones pnpm prisma migrate status

3. Error de Redis

# Verificar que Redis esté ejecutándose redis-cli ping # Verificar conexión desde la aplicación redis-cli -u $REDIS_URL ping

4. Error de Build

# Limpiar cache de Next.js rm -rf .next # Limpiar node_modules rm -rf node_modules pnpm install # Verificar versión de Node.js (requerido: 18.17+) node --version

Debug de Configuración

// Función de debug para configuración export function debugConfiguration() { console.log('=== UNIMAST ERP Configuration Debug ==='); // Variables de entorno console.log('Environment Variables:'); console.log('- NODE_ENV:', process.env.NODE_ENV); console.log('- DATABASE_URL:', process.env.DATABASE_URL ? 'Set' : 'Not Set'); console.log('- REDIS_URL:', process.env.REDIS_URL ? 'Set' : 'Not Set'); console.log('- ECF_ENV:', process.env.ECF_ENV); console.log('- AWS_REGION:', process.env.AWS_REGION); // Configuración de Next.js console.log('\nNext.js Configuration:'); console.log('- Output:', 'standalone'); console.log('- Server Actions Limit:', '50mb'); // Configuración de Tailwind console.log('\nTailwind Configuration:'); console.log('- Dark Mode:', 'class-based'); console.log('- CSS Variables:', 'enabled'); console.log('- Animations:', 'enabled'); // Configuración de Prisma console.log('\nPrisma Configuration:'); console.log('- Provider:', 'postgresql'); console.log('- Engine Type:', 'binary'); }

¿Necesitas más detalles sobre algún aspecto específico? Revisa la documentación de eCF para entender cómo se configura el entorno de facturación electrónica, o la documentación de background jobs para aprender sobre la configuración de Redis y colas de trabajo.