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ónUso 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 eslintConfigConfiguració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 studioConfiguració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:6379Entorno de Staging
NODE_ENV=staging
ECF_ENV=CerteCF
DATABASE_URL=postgresql://staging:5432/unimast_erp_staging
REDIS_URL=redis://staging:6379Entorno 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 undefined2. 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 desarrollo3. 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 dev2. 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 status3. Error de Redis
# Verificar que Redis esté ejecutándose
redis-cli ping
# Verificar conexión desde la aplicación
redis-cli -u $REDIS_URL ping4. 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 --versionDebug 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.