@jeffrey2423/coding-standards 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -0
- package/bin/cli.js +35 -0
- package/package.json +19 -0
- package/standards/architecture-patterns.md +444 -0
- package/standards/backend-standards.md +812 -0
- package/standards/database-conventions.md +667 -0
- package/standards/frontend-standards.md +1199 -0
- package/standards/mobile-flutter-standards.md +1292 -0
- package/standards/mobile-react-native-standards.md +1288 -0
- package/standards/technical-preferences-ux.md +400 -0
- package/standards/technology-stack.md +294 -0
- package/standards/vite-config-standard.md +531 -0
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
# Frontend Development Standards
|
|
2
|
+
|
|
3
|
+
## Resumen
|
|
4
|
+
|
|
5
|
+
Este documento define los estándares de desarrollo frontend para aplicaciones empresariales usando **Vite** como bundler, **TanStack Router** para enrutamiento type-safe, **Zustand** para estado global, y **Clean Architecture** con estructura modular preparada para microfrontends.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Tabla de Contenidos
|
|
10
|
+
|
|
11
|
+
1. [Principios de Arquitectura](#1-principios-de-arquitectura)
|
|
12
|
+
2. [Stack Tecnológico](#2-stack-tecnológico)
|
|
13
|
+
3. [Convenciones de Routing](#3-convenciones-de-routing)
|
|
14
|
+
4. [Organización de Archivos](#4-organización-de-archivos)
|
|
15
|
+
5. [Estándares de Componentes](#5-estándares-de-componentes)
|
|
16
|
+
6. [Patrones de Estado](#6-patrones-de-estado)
|
|
17
|
+
7. [Estándares de Testing](#7-estándares-de-testing)
|
|
18
|
+
8. [Accesibilidad](#8-accesibilidad)
|
|
19
|
+
9. [Rendimiento](#9-rendimiento)
|
|
20
|
+
10. [Seguridad](#10-seguridad)
|
|
21
|
+
11. [Manejo de Errores](#11-manejo-de-errores)
|
|
22
|
+
12. [Progressive Web App](#12-progressive-web-app)
|
|
23
|
+
13. [Estándares de Idioma](#13-estándares-de-idioma)
|
|
24
|
+
14. [Consideraciones Generales](#14-consideraciones-generales)
|
|
25
|
+
15. [Configuración Base](#15-configuración-base)
|
|
26
|
+
16. [Checklist de Implementación](#16-checklist-de-implementación)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Principios de Arquitectura
|
|
31
|
+
|
|
32
|
+
### 1.1 Implementación de Clean Architecture
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
36
|
+
│ PRESENTATION LAYER │
|
|
37
|
+
│ React components, pages, UI logic, routes │
|
|
38
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
39
|
+
│
|
|
40
|
+
▼
|
|
41
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
42
|
+
│ APPLICATION LAYER │
|
|
43
|
+
│ Use cases, custom hooks, Zustand stores │
|
|
44
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
48
|
+
│ INFRASTRUCTURE LAYER │
|
|
49
|
+
│ API clients, repositories impl, external adapters │
|
|
50
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
54
|
+
│ DOMAIN LAYER │
|
|
55
|
+
│ Business entities, value objects, business rules │
|
|
56
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Capa | Responsabilidad | Contenido |
|
|
60
|
+
|------|-----------------|-----------|
|
|
61
|
+
| **Domain** | Reglas de negocio puras | Entities, value objects, interfaces de repositorios |
|
|
62
|
+
| **Application** | Orquestación de casos de uso | Use cases, hooks, stores Zustand |
|
|
63
|
+
| **Infrastructure** | Implementaciones externas | API clients, repositorios concretos, adapters |
|
|
64
|
+
| **Presentation** | Interfaz de usuario | Componentes React, páginas, estilos |
|
|
65
|
+
|
|
66
|
+
### 1.2 Reglas de Dependencia
|
|
67
|
+
|
|
68
|
+
- Las capas internas **NO deben conocer** las capas externas
|
|
69
|
+
- Las dependencias apuntan hacia adentro (de externo a interno)
|
|
70
|
+
- Usar inversión de dependencias para concerns externos
|
|
71
|
+
- Domain layer no importa nada de otras capas
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 2. Stack Tecnológico
|
|
76
|
+
|
|
77
|
+
### 2.1 Tecnologías Core
|
|
78
|
+
|
|
79
|
+
| Categoría | Tecnología | Versión | Notas |
|
|
80
|
+
|-----------|------------|---------|-------|
|
|
81
|
+
| **Bundler** | Vite | 5+ | Build tool y dev server |
|
|
82
|
+
| **Framework** | React | 18+ | Functional components y hooks |
|
|
83
|
+
| **Router** | TanStack Router | 1+ | File-based routing con type-safety |
|
|
84
|
+
| **Lenguaje** | TypeScript | 5+ | Strict mode, sin `any` |
|
|
85
|
+
| **Estilos** | TailwindCSS | 4+ | Utility-first CSS |
|
|
86
|
+
| **Componentes** | shadcn/ui + Radix UI | - | Base de componentes |
|
|
87
|
+
| **Estado** | Zustand | 4+ | Estado global por feature |
|
|
88
|
+
| **Data Fetching** | TanStack Query | 5+ | Cache y sincronización |
|
|
89
|
+
|
|
90
|
+
### 2.2 Reglas de Selección de Framework
|
|
91
|
+
|
|
92
|
+
| Escenario | Framework | Razón |
|
|
93
|
+
|-----------|-----------|-------|
|
|
94
|
+
| **Default** | Vite + TanStack Router | Mejor DX, type-safety, preparado para microfrontends |
|
|
95
|
+
| **MFE (Default)** | Vite + Single-SPA | Aislamiento completo, CSS lifecycle, error boundaries built-in |
|
|
96
|
+
| **Módulo Federable** | Vite + Module Federation | SOLO para módulos transversales explícitamente marcados |
|
|
97
|
+
|
|
98
|
+
### 2.3 Regla Crítica: Single-SPA vs Module Federation
|
|
99
|
+
|
|
100
|
+
> ⚠️ **IMPORTANTE**: Por defecto, todos los microfrontends usan **Single-SPA**. Module Federation se usa **SOLO** cuando el ingeniero define explícitamente que un módulo debe ser compartido/federable.
|
|
101
|
+
|
|
102
|
+
| Tipo | Tecnología | Uso |
|
|
103
|
+
|------|------------|-----|
|
|
104
|
+
| **MFE de Negocio** | `vite-plugin-single-spa` + `single-spa-react` | ✅ DEFAULT - finance, hr, inventory, etc. |
|
|
105
|
+
| **Módulo Compartido** | `@module-federation/vite` | ⚠️ SOLO cuando explícitamente requerido |
|
|
106
|
+
|
|
107
|
+
**Dependencias Single-SPA (DEFAULT):**
|
|
108
|
+
```bash
|
|
109
|
+
npm install single-spa-react
|
|
110
|
+
npm install vite-plugin-single-spa --save-dev
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Beneficios de Single-SPA:**
|
|
114
|
+
- CSS isolation automático via `cssLifecycleFactory`
|
|
115
|
+
- Error boundaries built-in
|
|
116
|
+
- Lifecycle completo (bootstrap, mount, unmount)
|
|
117
|
+
- Mejor hot reload
|
|
118
|
+
- Menor complejidad de configuración
|
|
119
|
+
|
|
120
|
+
Ver `vite-config-standard.md` para configuración detallada.
|
|
121
|
+
|
|
122
|
+
### 2.3 Herramientas de Desarrollo
|
|
123
|
+
|
|
124
|
+
| Herramienta | Propósito |
|
|
125
|
+
|-------------|-----------|
|
|
126
|
+
| **Vite** | Build system y dev server |
|
|
127
|
+
| **Vitest** | Unit e integration testing |
|
|
128
|
+
| **React Testing Library** | Component testing |
|
|
129
|
+
| **MSW** | API mocking para tests |
|
|
130
|
+
| **ESLint + Prettier** | Code quality y formatting |
|
|
131
|
+
| **TypeScript** | Type checking |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 3. Convenciones de Routing
|
|
136
|
+
|
|
137
|
+
TanStack Router usa prefijos especiales en nombres de archivo para definir comportamientos.
|
|
138
|
+
|
|
139
|
+
### 3.1 Prefijo `_` (Underscore) - Pathless Layout Routes
|
|
140
|
+
|
|
141
|
+
**Propósito:** Agrupar rutas bajo un layout compartido **SIN agregar segmentos a la URL**.
|
|
142
|
+
|
|
143
|
+
#### ❌ El Problema (sin `_`)
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
routes/
|
|
147
|
+
├── __root.tsx
|
|
148
|
+
├── auth/
|
|
149
|
+
│ └── login.tsx → URL: /auth/login ❌
|
|
150
|
+
├── app/
|
|
151
|
+
│ ├── dashboard.tsx → URL: /app/dashboard ❌
|
|
152
|
+
│ └── orders.tsx → URL: /app/orders ❌
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Resultado:** Las URLs incluyen el nombre de la carpeta, lo cual es indeseable.
|
|
156
|
+
|
|
157
|
+
#### ✅ La Solución (con `_`)
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
routes/
|
|
161
|
+
├── __root.tsx
|
|
162
|
+
├── _auth.tsx → NO agrega nada a la URL (solo layout)
|
|
163
|
+
├── _auth/
|
|
164
|
+
│ └── login.tsx → URL: /login ✅
|
|
165
|
+
│
|
|
166
|
+
├── _app.tsx → NO agrega nada a la URL (solo layout)
|
|
167
|
+
├── _app/
|
|
168
|
+
│ ├── dashboard.tsx → URL: /dashboard ✅
|
|
169
|
+
│ └── orders.tsx → URL: /orders ✅
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Ejemplo de Implementación
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// routes/_app.tsx - Layout protegido
|
|
176
|
+
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
|
|
177
|
+
import { Sidebar, TopNav } from '@/shared/components/layout';
|
|
178
|
+
import { useAuthStore } from '@/modules/users/authentication/login/application/store';
|
|
179
|
+
|
|
180
|
+
export const Route = createFileRoute('/_app')({
|
|
181
|
+
beforeLoad: () => {
|
|
182
|
+
const { isAuthenticated } = useAuthStore.getState();
|
|
183
|
+
if (!isAuthenticated) {
|
|
184
|
+
throw redirect({ to: '/login' });
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
component: AppLayout,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
function AppLayout() {
|
|
191
|
+
return (
|
|
192
|
+
<div className="flex h-screen">
|
|
193
|
+
<Sidebar />
|
|
194
|
+
<div className="flex-1 flex flex-col">
|
|
195
|
+
<TopNav />
|
|
196
|
+
<main className="flex-1 overflow-auto p-6">
|
|
197
|
+
<Outlet />
|
|
198
|
+
</main>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 3.2 Prefijo `.` (Punto) - Flat Routing
|
|
206
|
+
|
|
207
|
+
**Propósito:** Definir rutas anidadas **sin crear carpetas**.
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
routes/
|
|
211
|
+
├── orders.tsx → /orders (layout)
|
|
212
|
+
├── orders.index.tsx → /orders
|
|
213
|
+
├── orders.$orderId.tsx → /orders/:orderId
|
|
214
|
+
├── orders.$orderId.edit.tsx → /orders/:orderId/edit
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### ¿Cuándo usar `.` vs Carpetas?
|
|
218
|
+
|
|
219
|
+
| Escenario | Recomendación |
|
|
220
|
+
|-----------|---------------|
|
|
221
|
+
| Pocas rutas anidadas (2-3) | Flat con `.` |
|
|
222
|
+
| Muchas rutas anidadas (4+) | Carpetas |
|
|
223
|
+
| Rutas con componentes colocados | Carpetas con `-components/` |
|
|
224
|
+
|
|
225
|
+
### 3.3 Prefijo `-` (Guión) - Ignorar Archivos
|
|
226
|
+
|
|
227
|
+
**Propósito:** Excluir archivos/carpetas de la generación de rutas para colocación de código.
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
routes/
|
|
231
|
+
├── orders/
|
|
232
|
+
│ ├── $orderId.tsx → /orders/:orderId ✅
|
|
233
|
+
│ ├── -components/ → ❌ Ignorado por el router
|
|
234
|
+
│ │ └── OrderHeader.tsx
|
|
235
|
+
│ └── -hooks/ → ❌ Ignorado por el router
|
|
236
|
+
│ └── useOrderCalculations.ts
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 3.4 Prefijo `$` (Dólar) - Parámetros Dinámicos
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// routes/_app/orders/$orderId.tsx
|
|
243
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
244
|
+
|
|
245
|
+
export const Route = createFileRoute('/_app/orders/$orderId')({
|
|
246
|
+
component: OrderDetail,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
function OrderDetail() {
|
|
250
|
+
const { orderId } = Route.useParams(); // Tipado automático
|
|
251
|
+
return <div>Order: {orderId}</div>;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 3.5 Resumen de Prefijos
|
|
256
|
+
|
|
257
|
+
| Prefijo | Nombre | Efecto en URL | Uso Principal |
|
|
258
|
+
|---------|--------|---------------|---------------|
|
|
259
|
+
| `_` | Pathless | **No aparece** | Layouts sin path |
|
|
260
|
+
| `.` | Flat | Crea anidamiento | Evitar carpetas |
|
|
261
|
+
| `-` | Ignore | **No genera ruta** | Colocación de código |
|
|
262
|
+
| `$` | Dynamic | Captura valor | Parámetros de URL |
|
|
263
|
+
| `__` | Root | Raíz del árbol | Solo `__root.tsx` |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 4. Organización de Archivos
|
|
268
|
+
|
|
269
|
+
### 4.1 Estructura Enterprise (Module/Domain/Feature)
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
src/
|
|
273
|
+
├── main.tsx # React entry point
|
|
274
|
+
├── router.tsx # TanStack Router config
|
|
275
|
+
├── routeTree.gen.ts # Auto-generado (NO EDITAR)
|
|
276
|
+
├── globals.css # Estilos globales + Tailwind
|
|
277
|
+
│
|
|
278
|
+
├── routes/ # 🛣️ SOLO definición de rutas
|
|
279
|
+
│ ├── __root.tsx # Layout raíz (providers)
|
|
280
|
+
│ ├── index.tsx # Redirect inicial
|
|
281
|
+
│ ├── _auth.tsx # Layout público
|
|
282
|
+
│ ├── _auth/
|
|
283
|
+
│ │ └── login.tsx
|
|
284
|
+
│ ├── _app.tsx # Layout protegido
|
|
285
|
+
│ └── _app/
|
|
286
|
+
│ ├── dashboard.tsx
|
|
287
|
+
│ ├── sales/
|
|
288
|
+
│ │ ├── quotes.tsx
|
|
289
|
+
│ │ └── invoices.tsx
|
|
290
|
+
│ └── inventory/
|
|
291
|
+
│ └── products.tsx
|
|
292
|
+
│
|
|
293
|
+
├── modules/ # 🏢 Lógica de negocio por módulo
|
|
294
|
+
│ ├── sales/ # MODULE
|
|
295
|
+
│ │ ├── quotes/ # DOMAIN
|
|
296
|
+
│ │ │ ├── cart/ # FEATURE
|
|
297
|
+
│ │ │ │ ├── domain/
|
|
298
|
+
│ │ │ │ │ ├── entities/
|
|
299
|
+
│ │ │ │ │ │ └── CartItem.ts
|
|
300
|
+
│ │ │ │ │ ├── repositories/ # Interfaces
|
|
301
|
+
│ │ │ │ │ │ └── ICartRepository.ts
|
|
302
|
+
│ │ │ │ │ ├── services/ # Domain services
|
|
303
|
+
│ │ │ │ │ │ └── CartCalculator.ts
|
|
304
|
+
│ │ │ │ │ └── types/
|
|
305
|
+
│ │ │ │ │ └── cart.types.ts
|
|
306
|
+
│ │ │ │ ├── application/
|
|
307
|
+
│ │ │ │ │ ├── use-cases/
|
|
308
|
+
│ │ │ │ │ │ ├── AddToCart.ts
|
|
309
|
+
│ │ │ │ │ │ └── RemoveFromCart.ts
|
|
310
|
+
│ │ │ │ │ ├── hooks/
|
|
311
|
+
│ │ │ │ │ │ └── useCart.ts
|
|
312
|
+
│ │ │ │ │ └── store/
|
|
313
|
+
│ │ │ │ │ └── cart.store.ts
|
|
314
|
+
│ │ │ │ ├── infrastructure/
|
|
315
|
+
│ │ │ │ │ ├── repositories/ # Implementations
|
|
316
|
+
│ │ │ │ │ │ └── CartRepository.ts
|
|
317
|
+
│ │ │ │ │ ├── api/
|
|
318
|
+
│ │ │ │ │ │ └── cart.api.ts
|
|
319
|
+
│ │ │ │ │ └── adapters/
|
|
320
|
+
│ │ │ │ └── presentation/
|
|
321
|
+
│ │ │ │ ├── components/
|
|
322
|
+
│ │ │ │ │ ├── CartList.tsx
|
|
323
|
+
│ │ │ │ │ └── CartItem.tsx
|
|
324
|
+
│ │ │ │ └── pages/
|
|
325
|
+
│ │ │ │ └── CartPage.tsx
|
|
326
|
+
│ │ │ └── products/ # FEATURE
|
|
327
|
+
│ │ │ ├── domain/
|
|
328
|
+
│ │ │ ├── application/
|
|
329
|
+
│ │ │ ├── infrastructure/
|
|
330
|
+
│ │ │ └── presentation/
|
|
331
|
+
│ │ └── billing/ # DOMAIN
|
|
332
|
+
│ │ ├── invoices/ # FEATURE
|
|
333
|
+
│ │ └── reports/ # FEATURE
|
|
334
|
+
│ │
|
|
335
|
+
│ ├── inventory/ # MODULE
|
|
336
|
+
│ │ ├── products/ # DOMAIN
|
|
337
|
+
│ │ │ ├── catalog/ # FEATURE
|
|
338
|
+
│ │ │ └── stock/ # FEATURE
|
|
339
|
+
│ │ └── warehouses/ # DOMAIN
|
|
340
|
+
│ │
|
|
341
|
+
│ └── users/ # MODULE
|
|
342
|
+
│ └── authentication/ # DOMAIN
|
|
343
|
+
│ ├── login/ # FEATURE
|
|
344
|
+
│ └── registration/ # FEATURE
|
|
345
|
+
│
|
|
346
|
+
├── shared/ # 🔄 Código reutilizable
|
|
347
|
+
│ ├── components/
|
|
348
|
+
│ │ └── ui/ # shadcn/ui
|
|
349
|
+
│ ├── hooks/
|
|
350
|
+
│ │ ├── useDebounce.ts
|
|
351
|
+
│ │ └── useLocalStorage.ts
|
|
352
|
+
│ ├── lib/
|
|
353
|
+
│ │ ├── utils.ts # cn(), formatters
|
|
354
|
+
│ │ ├── api-client.ts # Axios/fetch config
|
|
355
|
+
│ │ └── env.ts # Variables de entorno tipadas
|
|
356
|
+
│ ├── types/
|
|
357
|
+
│ │ └── common.types.ts
|
|
358
|
+
│ └── constants/
|
|
359
|
+
│
|
|
360
|
+
├── app/ # 🎯 Configuración global
|
|
361
|
+
│ ├── store/ # Global store (si necesario)
|
|
362
|
+
│ ├── providers/ # Context providers
|
|
363
|
+
│ │ ├── ThemeProvider.tsx
|
|
364
|
+
│ │ └── QueryProvider.tsx
|
|
365
|
+
│ └── config/
|
|
366
|
+
│
|
|
367
|
+
├── infrastructure/ # 🔌 Servicios externos globales
|
|
368
|
+
│ ├── api/ # API configuration
|
|
369
|
+
│ ├── storage/ # IndexedDB, localStorage
|
|
370
|
+
│ └── pwa/ # PWA configuration
|
|
371
|
+
│
|
|
372
|
+
├── assets/ # 📁 Recursos estáticos
|
|
373
|
+
│ ├── fonts/
|
|
374
|
+
│ │ ├── Inter_18pt-Light.ttf
|
|
375
|
+
│ │ ├── Inter_18pt-Regular.ttf
|
|
376
|
+
│ │ └── Inter_18pt-Bold.ttf
|
|
377
|
+
│ └── images/
|
|
378
|
+
│ └── logos/
|
|
379
|
+
│
|
|
380
|
+
└── styles/
|
|
381
|
+
└── globals.css
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### 4.2 Jerarquía de Carpetas
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
MODULE (módulo de negocio)
|
|
388
|
+
└── DOMAIN (área funcional)
|
|
389
|
+
└── FEATURE (funcionalidad específica)
|
|
390
|
+
├── domain/ # Reglas de negocio
|
|
391
|
+
├── application/ # Casos de uso y estado
|
|
392
|
+
├── infrastructure/ # Implementaciones externas
|
|
393
|
+
└── presentation/ # UI
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 4.3 Organización de Imports
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// 1. Librerías externas
|
|
400
|
+
import React from 'react';
|
|
401
|
+
import { create } from 'zustand';
|
|
402
|
+
import { useQuery } from '@tanstack/react-query';
|
|
403
|
+
|
|
404
|
+
// 2. Módulos internos (por capa, de interno a externo)
|
|
405
|
+
import { CartItem } from '../domain/entities/CartItem';
|
|
406
|
+
import { addToCartUseCase } from '../application/use-cases/AddToCart';
|
|
407
|
+
import { cartRepository } from '../infrastructure/repositories/CartRepository';
|
|
408
|
+
|
|
409
|
+
// 3. Shared
|
|
410
|
+
import { cn } from '@/shared/lib/utils';
|
|
411
|
+
import { Button } from '@/shared/components/ui/button';
|
|
412
|
+
|
|
413
|
+
// 4. Types
|
|
414
|
+
import type { Cart } from '../domain/types/cart.types';
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## 5. Estándares de Componentes
|
|
420
|
+
|
|
421
|
+
### 5.1 Estrategia de Componentes
|
|
422
|
+
|
|
423
|
+
| Prioridad | Acción |
|
|
424
|
+
|-----------|--------|
|
|
425
|
+
| **1. Si no existe** | Preguntar al usuario: [1] Usar shadcn directamente, [2] Crear para componente |
|
|
426
|
+
| **3. Shadcn fallback** | Solo usar registro MCP Shadcn si el usuario elige opción [1] |
|
|
427
|
+
|
|
428
|
+
> **Beneficio:** 90% menos bugs usando componentes existentes vs creación manual.
|
|
429
|
+
|
|
430
|
+
### 5.2 Estructura de Componentes
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
interface ComponentProps {
|
|
434
|
+
// Required props
|
|
435
|
+
children: React.ReactNode;
|
|
436
|
+
// Optional props with defaults
|
|
437
|
+
className?: string;
|
|
438
|
+
variant?: 'default' | 'secondary';
|
|
439
|
+
'data-testid'?: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export const Component = memo<ComponentProps>(({
|
|
443
|
+
children,
|
|
444
|
+
className,
|
|
445
|
+
variant = 'default',
|
|
446
|
+
'data-testid': testId = 'component'
|
|
447
|
+
}) => {
|
|
448
|
+
return (
|
|
449
|
+
<div
|
|
450
|
+
className={cn(baseStyles, variantStyles[variant], className)}
|
|
451
|
+
data-testid={testId}
|
|
452
|
+
>
|
|
453
|
+
{children}
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
Component.displayName = 'Component';
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### 5.3 Convenciones de Nombrado
|
|
462
|
+
|
|
463
|
+
| Elemento | Convención | Ejemplo |
|
|
464
|
+
|----------|------------|---------|
|
|
465
|
+
| Componentes | PascalCase | `OrderCard.tsx` |
|
|
466
|
+
| Archivos | kebab-case | `order-card.tsx` |
|
|
467
|
+
| data-testid | kebab-case | `data-testid="order-card"` |
|
|
468
|
+
| Props/funciones | camelCase | `onSubmit`, `isLoading` |
|
|
469
|
+
| Constantes | SCREAMING_SNAKE | `MAX_ITEMS` |
|
|
470
|
+
|
|
471
|
+
### 5.4 Guidelines de Props
|
|
472
|
+
|
|
473
|
+
- Siempre definir interfaces TypeScript para props
|
|
474
|
+
- Usar props opcionales con defaults sensatos
|
|
475
|
+
- Incluir `className` para overrides de estilos
|
|
476
|
+
- Agregar `data-testid` para testing
|
|
477
|
+
- Documentar props complejas con JSDoc
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## 6. Patrones de Estado
|
|
482
|
+
|
|
483
|
+
### 6.1 Estructura de Zustand Store
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
// modules/sales/quotes/cart/application/store/cart.store.ts
|
|
487
|
+
import { create } from 'zustand';
|
|
488
|
+
import { devtools } from 'zustand/middleware';
|
|
489
|
+
import type { CartItem } from '../../domain/entities/CartItem';
|
|
490
|
+
import { addToCartUseCase } from '../use-cases/AddToCart';
|
|
491
|
+
import { removeFromCartUseCase } from '../use-cases/RemoveFromCart';
|
|
492
|
+
|
|
493
|
+
interface CartState {
|
|
494
|
+
// Domain entities
|
|
495
|
+
items: CartItem[];
|
|
496
|
+
|
|
497
|
+
// UI state
|
|
498
|
+
loading: boolean;
|
|
499
|
+
error: string | null;
|
|
500
|
+
|
|
501
|
+
// Actions (delegan a use cases)
|
|
502
|
+
addItem: (productId: string, quantity: number) => Promise<void>;
|
|
503
|
+
removeItem: (itemId: string) => Promise<void>;
|
|
504
|
+
clearCart: () => void;
|
|
505
|
+
clearError: () => void;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export const useCartStore = create<CartState>()(
|
|
509
|
+
devtools(
|
|
510
|
+
(set, get) => ({
|
|
511
|
+
// Initial state
|
|
512
|
+
items: [],
|
|
513
|
+
loading: false,
|
|
514
|
+
error: null,
|
|
515
|
+
|
|
516
|
+
// Actions
|
|
517
|
+
addItem: async (productId, quantity) => {
|
|
518
|
+
set({ loading: true, error: null });
|
|
519
|
+
try {
|
|
520
|
+
const newItem = await addToCartUseCase.execute({ productId, quantity });
|
|
521
|
+
set((state) => ({
|
|
522
|
+
items: [...state.items, newItem],
|
|
523
|
+
loading: false
|
|
524
|
+
}));
|
|
525
|
+
} catch (error) {
|
|
526
|
+
set({ error: (error as Error).message, loading: false });
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
removeItem: async (itemId) => {
|
|
531
|
+
set({ loading: true, error: null });
|
|
532
|
+
try {
|
|
533
|
+
await removeFromCartUseCase.execute(itemId);
|
|
534
|
+
set((state) => ({
|
|
535
|
+
items: state.items.filter(item => item.id !== itemId),
|
|
536
|
+
loading: false
|
|
537
|
+
}));
|
|
538
|
+
} catch (error) {
|
|
539
|
+
set({ error: (error as Error).message, loading: false });
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
clearCart: () => set({ items: [] }),
|
|
544
|
+
clearError: () => set({ error: null }),
|
|
545
|
+
}),
|
|
546
|
+
{ name: 'cartStore' }
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 6.2 Cuándo Usar Cada Tipo de Estado
|
|
552
|
+
|
|
553
|
+
| Tipo | Cuándo Usar | Herramienta |
|
|
554
|
+
|------|-------------|-------------|
|
|
555
|
+
| **Server State** | Datos del API, cache | TanStack Query |
|
|
556
|
+
| **Global Client State** | Auth, theme, cart | Zustand |
|
|
557
|
+
| **Local Component State** | Forms, UI toggles | useState/useReducer |
|
|
558
|
+
| **URL State** | Filtros, paginación | TanStack Router search params |
|
|
559
|
+
|
|
560
|
+
### 6.3 Integración con TanStack Query
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
// modules/sales/quotes/products/infrastructure/api/products.api.ts
|
|
564
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
565
|
+
import { productRepository } from '../repositories/ProductRepository';
|
|
566
|
+
|
|
567
|
+
export const productKeys = {
|
|
568
|
+
all: ['products'] as const,
|
|
569
|
+
lists: () => [...productKeys.all, 'list'] as const,
|
|
570
|
+
list: (filters: ProductFilters) => [...productKeys.lists(), filters] as const,
|
|
571
|
+
detail: (id: string) => [...productKeys.all, 'detail', id] as const,
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
export function useProducts(filters: ProductFilters) {
|
|
575
|
+
return useQuery({
|
|
576
|
+
queryKey: productKeys.list(filters),
|
|
577
|
+
queryFn: () => productRepository.getAll(filters),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function useProduct(id: string) {
|
|
582
|
+
return useQuery({
|
|
583
|
+
queryKey: productKeys.detail(id),
|
|
584
|
+
queryFn: () => productRepository.getById(id),
|
|
585
|
+
enabled: !!id,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## 7. Estándares de Testing
|
|
593
|
+
|
|
594
|
+
### 7.1 Estrategia de Testing
|
|
595
|
+
|
|
596
|
+
| Tipo | Cobertura | Herramienta | Qué Testear |
|
|
597
|
+
|------|-----------|-------------|-------------|
|
|
598
|
+
| **Unit** | Alta | Vitest | Entities, use cases, utilities |
|
|
599
|
+
| **Integration** | Media | Vitest + MSW | Feature workflows, API integration |
|
|
600
|
+
| **Component** | Media | React Testing Library | User interactions, accessibility |
|
|
601
|
+
| **E2E** | Baja | Playwright | Critical user journeys |
|
|
602
|
+
|
|
603
|
+
### 7.2 Estructura de Tests
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// modules/sales/quotes/cart/application/use-cases/__tests__/AddToCart.test.ts
|
|
607
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
608
|
+
import { addToCartUseCase } from '../AddToCart';
|
|
609
|
+
import { mockCartRepository } from '../../__mocks__/cartRepository.mock';
|
|
610
|
+
|
|
611
|
+
describe('AddToCart UseCase', () => {
|
|
612
|
+
beforeEach(() => {
|
|
613
|
+
vi.clearAllMocks();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('should add item to cart successfully', async () => {
|
|
617
|
+
const input = { productId: 'prod-1', quantity: 2 };
|
|
618
|
+
|
|
619
|
+
const result = await addToCartUseCase.execute(input);
|
|
620
|
+
|
|
621
|
+
expect(result).toMatchObject({
|
|
622
|
+
productId: 'prod-1',
|
|
623
|
+
quantity: 2,
|
|
624
|
+
});
|
|
625
|
+
expect(mockCartRepository.save).toHaveBeenCalledWith(expect.objectContaining(input));
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('should throw error when quantity is invalid', async () => {
|
|
629
|
+
const input = { productId: 'prod-1', quantity: -1 };
|
|
630
|
+
|
|
631
|
+
await expect(addToCartUseCase.execute(input)).rejects.toThrow(
|
|
632
|
+
'La cantidad debe ser mayor a 0'
|
|
633
|
+
);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### 7.3 Component Testing
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
// modules/sales/quotes/cart/presentation/components/__tests__/CartItem.test.tsx
|
|
642
|
+
import { describe, it, expect } from 'vitest';
|
|
643
|
+
import { render, screen } from '@testing-library/react';
|
|
644
|
+
import userEvent from '@testing-library/user-event';
|
|
645
|
+
import { axe } from 'vitest-axe';
|
|
646
|
+
import { CartItem } from '../CartItem';
|
|
647
|
+
|
|
648
|
+
describe('CartItem', () => {
|
|
649
|
+
const defaultProps = {
|
|
650
|
+
item: { id: '1', name: 'Producto Test', quantity: 2, price: 100 },
|
|
651
|
+
onRemove: vi.fn(),
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
it('should render correctly with default props', () => {
|
|
655
|
+
render(<CartItem {...defaultProps} />);
|
|
656
|
+
|
|
657
|
+
expect(screen.getByTestId('cart-item')).toBeInTheDocument();
|
|
658
|
+
expect(screen.getByText('Producto Test')).toBeInTheDocument();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('should handle remove action', async () => {
|
|
662
|
+
const user = userEvent.setup();
|
|
663
|
+
render(<CartItem {...defaultProps} />);
|
|
664
|
+
|
|
665
|
+
await user.click(screen.getByRole('button', { name: /eliminar/i }));
|
|
666
|
+
|
|
667
|
+
expect(defaultProps.onRemove).toHaveBeenCalledWith('1');
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('should be accessible', async () => {
|
|
671
|
+
const { container } = render(<CartItem {...defaultProps} />);
|
|
672
|
+
const results = await axe(container);
|
|
673
|
+
|
|
674
|
+
expect(results).toHaveNoViolations();
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## 8. Accesibilidad
|
|
682
|
+
|
|
683
|
+
### 8.1 Cumplimiento WCAG 2.1 AA
|
|
684
|
+
|
|
685
|
+
| Requisito | Implementación |
|
|
686
|
+
|-----------|----------------|
|
|
687
|
+
| Elementos semánticos | Usar HTML semántico (`<nav>`, `<main>`, `<article>`) |
|
|
688
|
+
| Jerarquía de headings | Un solo `<h1>`, headings en orden descendente |
|
|
689
|
+
| Navegación por teclado | Todos los elementos interactivos accesibles con Tab |
|
|
690
|
+
| Screen readers | Labels descriptivos, roles ARIA cuando necesario |
|
|
691
|
+
| Contraste de color | Mínimo 4.5:1 para texto normal |
|
|
692
|
+
|
|
693
|
+
### 8.2 Implementación ARIA
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
// ✅ Correcto - HTML semántico primero
|
|
697
|
+
<button onClick={handleSubmit}>Guardar</button>
|
|
698
|
+
|
|
699
|
+
// ✅ Correcto - ARIA cuando es necesario
|
|
700
|
+
<div
|
|
701
|
+
role="tabpanel"
|
|
702
|
+
aria-labelledby="tab-1"
|
|
703
|
+
aria-expanded={isOpen}
|
|
704
|
+
>
|
|
705
|
+
{content}
|
|
706
|
+
</div>
|
|
707
|
+
|
|
708
|
+
// ❌ Incorrecto - ARIA innecesario
|
|
709
|
+
<button role="button" aria-label="button">Guardar</button>
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## 9. Rendimiento
|
|
715
|
+
|
|
716
|
+
### 9.1 Optimización de Bundle
|
|
717
|
+
|
|
718
|
+
| Estrategia | Implementación |
|
|
719
|
+
|------------|----------------|
|
|
720
|
+
| Code splitting | Por ruta (automático con TanStack Router) |
|
|
721
|
+
| Lazy loading | `React.lazy()` para componentes no críticos |
|
|
722
|
+
| Tree shaking | Imports específicos, no barrel exports en shared |
|
|
723
|
+
| Bundle budget | Máximo 500KB total |
|
|
724
|
+
|
|
725
|
+
### 9.2 Rendimiento en Runtime
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
// ✅ React.memo para componentes costosos
|
|
729
|
+
export const ExpensiveList = memo<ListProps>(({ items }) => {
|
|
730
|
+
return items.map(item => <ExpensiveItem key={item.id} item={item} />);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// ✅ useMemo para cálculos costosos
|
|
734
|
+
const sortedItems = useMemo(() =>
|
|
735
|
+
items.sort((a, b) => a.name.localeCompare(b.name)),
|
|
736
|
+
[items]
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
// ✅ useCallback para handlers pasados a children
|
|
740
|
+
const handleClick = useCallback((id: string) => {
|
|
741
|
+
onSelect(id);
|
|
742
|
+
}, [onSelect]);
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### 9.3 Loading Performance
|
|
746
|
+
|
|
747
|
+
- Optimización de imágenes y lazy loading
|
|
748
|
+
- Skeleton screens para estados de carga
|
|
749
|
+
- Prefetch de recursos críticos
|
|
750
|
+
- Virtual scrolling para listas grandes
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
## 10. Seguridad
|
|
755
|
+
|
|
756
|
+
### 10.1 Seguridad Client-Side
|
|
757
|
+
|
|
758
|
+
| Riesgo | Mitigación |
|
|
759
|
+
|--------|------------|
|
|
760
|
+
| API keys expuestas | Nunca en código frontend, usar variables de entorno server-side |
|
|
761
|
+
| XSS | Sanitización de inputs, no usar `dangerouslySetInnerHTML` |
|
|
762
|
+
| Datos sensibles | No almacenar en localStorage sin encriptar |
|
|
763
|
+
|
|
764
|
+
### 10.2 Autenticación
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
// Manejo seguro de tokens
|
|
768
|
+
const useAuthStore = create<AuthState>((set) => ({
|
|
769
|
+
token: null,
|
|
770
|
+
|
|
771
|
+
setToken: (token: string) => {
|
|
772
|
+
// Almacenar en memoria, no localStorage
|
|
773
|
+
set({ token });
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
logout: () => {
|
|
777
|
+
set({ token: null });
|
|
778
|
+
// Limpiar cualquier dato sensible
|
|
779
|
+
},
|
|
780
|
+
}));
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## 11. Manejo de Errores
|
|
786
|
+
|
|
787
|
+
### 11.1 Error Boundaries por Feature
|
|
788
|
+
|
|
789
|
+
```typescript
|
|
790
|
+
// shared/components/feedback/ErrorBoundary.tsx
|
|
791
|
+
import { Component, ErrorInfo, ReactNode } from 'react';
|
|
792
|
+
|
|
793
|
+
interface Props {
|
|
794
|
+
children: ReactNode;
|
|
795
|
+
fallback?: ReactNode;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
interface State {
|
|
799
|
+
hasError: boolean;
|
|
800
|
+
error: Error | null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
804
|
+
state: State = { hasError: false, error: null };
|
|
805
|
+
|
|
806
|
+
static getDerivedStateFromError(error: Error): State {
|
|
807
|
+
return { hasError: true, error };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
811
|
+
console.error('Error capturado:', error, errorInfo);
|
|
812
|
+
// Enviar a servicio de logging
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
render() {
|
|
816
|
+
if (this.state.hasError) {
|
|
817
|
+
return this.props.fallback || (
|
|
818
|
+
<div className="p-4 text-center">
|
|
819
|
+
<h2 className="text-lg font-semibold text-red-600">
|
|
820
|
+
Algo salió mal
|
|
821
|
+
</h2>
|
|
822
|
+
<p className="text-gray-600">
|
|
823
|
+
Por favor, intenta recargar la página
|
|
824
|
+
</p>
|
|
825
|
+
</div>
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return this.props.children;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### 11.2 Manejo de Errores en API
|
|
835
|
+
|
|
836
|
+
```typescript
|
|
837
|
+
// shared/lib/api-client.ts
|
|
838
|
+
import axios, { AxiosError } from 'axios';
|
|
839
|
+
|
|
840
|
+
const apiClient = axios.create({
|
|
841
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
apiClient.interceptors.response.use(
|
|
845
|
+
(response) => response,
|
|
846
|
+
(error: AxiosError<{ message: string }>) => {
|
|
847
|
+
const message = error.response?.data?.message || 'Error de conexión';
|
|
848
|
+
|
|
849
|
+
// Mensaje amigable para el usuario
|
|
850
|
+
return Promise.reject(new Error(message));
|
|
851
|
+
}
|
|
852
|
+
);
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
---
|
|
856
|
+
|
|
857
|
+
## 12. Progressive Web App
|
|
858
|
+
|
|
859
|
+
### 12.1 Features PWA
|
|
860
|
+
|
|
861
|
+
| Feature | Implementación |
|
|
862
|
+
|---------|----------------|
|
|
863
|
+
| Service Worker | Caching y soporte offline |
|
|
864
|
+
| Web App Manifest | Instalabilidad |
|
|
865
|
+
| Push Notifications | Cuando sea necesario |
|
|
866
|
+
| Background Sync | Sincronización al restaurar conexión |
|
|
867
|
+
|
|
868
|
+
### 12.2 Estrategia Offline
|
|
869
|
+
|
|
870
|
+
| Tipo de Recurso | Estrategia |
|
|
871
|
+
|-----------------|------------|
|
|
872
|
+
| Assets estáticos | Cache-first |
|
|
873
|
+
| Datos dinámicos | Network-first con fallback |
|
|
874
|
+
| Páginas offline | Fallback pre-cacheado |
|
|
875
|
+
| Formularios | Queue y sync cuando online |
|
|
876
|
+
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
## 13. Estándares de Idioma
|
|
880
|
+
|
|
881
|
+
### 13.1 Español para Todo Contenido Visible al Usuario
|
|
882
|
+
|
|
883
|
+
**REGLA CRÍTICA: Todo texto visible al usuario final DEBE estar en español.**
|
|
884
|
+
|
|
885
|
+
#### ✅ Español Requerido
|
|
886
|
+
|
|
887
|
+
- Labels de UI, botones, formularios, mensajes, notificaciones
|
|
888
|
+
- Respuestas de API, errores de validación, templates de email
|
|
889
|
+
- Cualquier texto que el usuario vea (frontend o backend)
|
|
890
|
+
|
|
891
|
+
#### ✅ Inglés Requerido
|
|
892
|
+
|
|
893
|
+
- Código (variables, funciones, clases)
|
|
894
|
+
- Logs técnicos, comentarios, commits de git
|
|
895
|
+
- Documentación técnica para desarrolladores
|
|
896
|
+
|
|
897
|
+
#### ❌ Nunca Mezclar Idiomas en Texto Visible
|
|
898
|
+
|
|
899
|
+
```typescript
|
|
900
|
+
// ✅ CORRECTO
|
|
901
|
+
<Button>Guardar</Button>
|
|
902
|
+
toast.success("Datos guardados correctamente");
|
|
903
|
+
throw new BadRequestException('No se pudo crear el usuario');
|
|
904
|
+
|
|
905
|
+
// ❌ INCORRECTO
|
|
906
|
+
<Button>Save cambios</Button>
|
|
907
|
+
toast.error("Failed al guardar");
|
|
908
|
+
throw new BadRequestException('Invalid datos proporcionados');
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### 13.2 Implementación
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
// shared/constants/messages.ts
|
|
915
|
+
export const MESSAGES = {
|
|
916
|
+
SUCCESS: {
|
|
917
|
+
SAVED: 'Datos guardados correctamente',
|
|
918
|
+
DELETED: 'Elemento eliminado correctamente',
|
|
919
|
+
UPDATED: 'Información actualizada',
|
|
920
|
+
},
|
|
921
|
+
ERROR: {
|
|
922
|
+
GENERIC: 'Ha ocurrido un error. Por favor, intenta de nuevo',
|
|
923
|
+
NOT_FOUND: 'El recurso solicitado no fue encontrado',
|
|
924
|
+
UNAUTHORIZED: 'No tienes permisos para realizar esta acción',
|
|
925
|
+
VALIDATION: 'Por favor, verifica los datos ingresados',
|
|
926
|
+
},
|
|
927
|
+
LOADING: {
|
|
928
|
+
DEFAULT: 'Cargando...',
|
|
929
|
+
SAVING: 'Guardando...',
|
|
930
|
+
PROCESSING: 'Procesando...',
|
|
931
|
+
},
|
|
932
|
+
} as const;
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
---
|
|
936
|
+
|
|
937
|
+
## 14. Consideraciones Generales
|
|
938
|
+
|
|
939
|
+
### 14.1 Gestor de Paquetes
|
|
940
|
+
|
|
941
|
+
**Regla:** Usar `pnpm` como gestor de paquetes por defecto, pero respetar el gestor existente en proyectos ya iniciados.
|
|
942
|
+
|
|
943
|
+
| Escenario | Acción | Razón |
|
|
944
|
+
|-----------|--------|-------|
|
|
945
|
+
| Proyecto nuevo | Usar `pnpm` | Mejor rendimiento y manejo de dependencias |
|
|
946
|
+
| Proyecto con `package-lock.json` | Continuar con `npm` | Evitar conflictos de lockfiles |
|
|
947
|
+
| Proyecto con `yarn.lock` | Continuar con `yarn` | Evitar conflictos de lockfiles |
|
|
948
|
+
| Proyecto con `pnpm-lock.yaml` | Continuar con `pnpm` | Ya está configurado |
|
|
949
|
+
|
|
950
|
+
#### Cómo Identificar el Gestor Actual
|
|
951
|
+
|
|
952
|
+
```bash
|
|
953
|
+
ls -la | grep -E "package-lock|yarn.lock|pnpm-lock"
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
| Archivo encontrado | Gestor a usar |
|
|
957
|
+
|--------------------|---------------|
|
|
958
|
+
| `package-lock.json` | `npm` |
|
|
959
|
+
| `yarn.lock` | `yarn` |
|
|
960
|
+
| `pnpm-lock.yaml` | `pnpm` |
|
|
961
|
+
| Ninguno | `pnpm` (proyecto nuevo) |
|
|
962
|
+
|
|
963
|
+
#### Comandos Equivalentes
|
|
964
|
+
|
|
965
|
+
| Acción | pnpm | npm | yarn |
|
|
966
|
+
|--------|------|-----|------|
|
|
967
|
+
| Instalar | `pnpm install` | `npm install` | `yarn` |
|
|
968
|
+
| Agregar dep | `pnpm add <pkg>` | `npm install <pkg>` | `yarn add <pkg>` |
|
|
969
|
+
| Agregar dev | `pnpm add -D <pkg>` | `npm install -D <pkg>` | `yarn add -D <pkg>` |
|
|
970
|
+
| Ejecutar script | `pnpm <script>` | `npm run <script>` | `yarn <script>` |
|
|
971
|
+
| Remover | `pnpm remove <pkg>` | `npm uninstall <pkg>` | `yarn remove <pkg>` |
|
|
972
|
+
|
|
973
|
+
> **⚠️ Importante:** Nunca mezclar gestores de paquetes en un mismo proyecto.
|
|
974
|
+
|
|
975
|
+
---
|
|
976
|
+
|
|
977
|
+
## 15. Configuración Base
|
|
978
|
+
|
|
979
|
+
### 15.1 `vite.config.ts`
|
|
980
|
+
|
|
981
|
+
```typescript
|
|
982
|
+
import { defineConfig } from 'vite';
|
|
983
|
+
import react from '@vitejs/plugin-react';
|
|
984
|
+
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
|
|
985
|
+
import viteTsConfigPaths from 'vite-tsconfig-paths';
|
|
986
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
987
|
+
|
|
988
|
+
export default defineConfig({
|
|
989
|
+
plugins: [
|
|
990
|
+
// TanStack Router DEBE ir primero
|
|
991
|
+
TanStackRouterVite({
|
|
992
|
+
target: 'react',
|
|
993
|
+
autoCodeSplitting: true,
|
|
994
|
+
routesDirectory: './src/routes',
|
|
995
|
+
generatedRouteTree: './src/routeTree.gen.ts',
|
|
996
|
+
routeFileIgnorePrefix: '-',
|
|
997
|
+
}),
|
|
998
|
+
react(),
|
|
999
|
+
viteTsConfigPaths(),
|
|
1000
|
+
tailwindcss(),
|
|
1001
|
+
],
|
|
1002
|
+
resolve: {
|
|
1003
|
+
alias: {
|
|
1004
|
+
'@': '/src',
|
|
1005
|
+
'@modules': '/src/modules',
|
|
1006
|
+
'@shared': '/src/shared',
|
|
1007
|
+
'@app': '/src/app',
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
server: {
|
|
1011
|
+
port: 3000,
|
|
1012
|
+
},
|
|
1013
|
+
build: {
|
|
1014
|
+
outDir: 'dist',
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
### 15.2 `src/router.tsx`
|
|
1020
|
+
|
|
1021
|
+
```typescript
|
|
1022
|
+
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
|
|
1023
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
1024
|
+
import { routerWithQueryClient } from '@tanstack/react-router-with-query';
|
|
1025
|
+
import { routeTree } from './routeTree.gen';
|
|
1026
|
+
|
|
1027
|
+
function NotFoundPage() {
|
|
1028
|
+
return (
|
|
1029
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
1030
|
+
<div className="text-center">
|
|
1031
|
+
<h1 className="text-6xl font-bold">404</h1>
|
|
1032
|
+
<p className="mt-4">Página no encontrada</p>
|
|
1033
|
+
</div>
|
|
1034
|
+
</div>
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function ErrorPage({ error }: { error: Error }) {
|
|
1039
|
+
return (
|
|
1040
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
1041
|
+
<div className="text-center">
|
|
1042
|
+
<h1 className="text-2xl font-bold text-red-600">Error</h1>
|
|
1043
|
+
<p className="mt-4">{error.message}</p>
|
|
1044
|
+
</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export function createRouter() {
|
|
1050
|
+
const queryClient = new QueryClient({
|
|
1051
|
+
defaultOptions: {
|
|
1052
|
+
queries: {
|
|
1053
|
+
staleTime: 60 * 1000,
|
|
1054
|
+
refetchOnWindowFocus: false,
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
const router = createTanStackRouter({
|
|
1060
|
+
routeTree,
|
|
1061
|
+
context: { queryClient },
|
|
1062
|
+
defaultPreload: 'intent',
|
|
1063
|
+
scrollRestoration: true,
|
|
1064
|
+
defaultNotFoundComponent: NotFoundPage,
|
|
1065
|
+
defaultErrorComponent: ErrorPage,
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
return routerWithQueryClient(router, queryClient);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
declare module '@tanstack/react-router' {
|
|
1072
|
+
interface Register {
|
|
1073
|
+
router: ReturnType<typeof createRouter>;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
### 15.3 `src/main.tsx`
|
|
1079
|
+
|
|
1080
|
+
```typescript
|
|
1081
|
+
import { StrictMode } from 'react';
|
|
1082
|
+
import { createRoot } from 'react-dom/client';
|
|
1083
|
+
import { RouterProvider } from '@tanstack/react-router';
|
|
1084
|
+
import { createRouter } from './router';
|
|
1085
|
+
import './globals.css';
|
|
1086
|
+
|
|
1087
|
+
const router = createRouter();
|
|
1088
|
+
|
|
1089
|
+
createRoot(document.getElementById('root')!).render(
|
|
1090
|
+
<StrictMode>
|
|
1091
|
+
<RouterProvider router={router} />
|
|
1092
|
+
</StrictMode>
|
|
1093
|
+
);
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
### 15.4 `src/routes/__root.tsx`
|
|
1097
|
+
|
|
1098
|
+
```typescript
|
|
1099
|
+
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
|
1100
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
1101
|
+
import { Toaster } from 'sonner';
|
|
1102
|
+
|
|
1103
|
+
interface RouterContext {
|
|
1104
|
+
queryClient: QueryClient;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
1108
|
+
component: RootComponent,
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
function RootComponent() {
|
|
1112
|
+
const { queryClient } = Route.useRouteContext();
|
|
1113
|
+
|
|
1114
|
+
return (
|
|
1115
|
+
<QueryClientProvider client={queryClient}>
|
|
1116
|
+
<Outlet />
|
|
1117
|
+
<Toaster position="bottom-right" />
|
|
1118
|
+
</QueryClientProvider>
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
### 15.5 `vitest.config.ts`
|
|
1124
|
+
|
|
1125
|
+
```typescript
|
|
1126
|
+
import { defineConfig } from 'vitest/config';
|
|
1127
|
+
import react from '@vitejs/plugin-react';
|
|
1128
|
+
import viteTsConfigPaths from 'vite-tsconfig-paths';
|
|
1129
|
+
|
|
1130
|
+
export default defineConfig({
|
|
1131
|
+
plugins: [react(), viteTsConfigPaths()],
|
|
1132
|
+
test: {
|
|
1133
|
+
globals: true,
|
|
1134
|
+
environment: 'jsdom',
|
|
1135
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
1136
|
+
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
|
1137
|
+
coverage: {
|
|
1138
|
+
provider: 'v8',
|
|
1139
|
+
reporter: ['text', 'json', 'html'],
|
|
1140
|
+
exclude: [
|
|
1141
|
+
'node_modules/',
|
|
1142
|
+
'src/test/',
|
|
1143
|
+
'**/*.d.ts',
|
|
1144
|
+
'src/routeTree.gen.ts',
|
|
1145
|
+
],
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
});
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
---
|
|
1152
|
+
|
|
1153
|
+
## 16. Checklist de Implementación
|
|
1154
|
+
|
|
1155
|
+
### Configuración Inicial
|
|
1156
|
+
|
|
1157
|
+
- [ ] Instalar dependencias: `@tanstack/react-router`, `@tanstack/router-plugin`, `@tanstack/react-query`
|
|
1158
|
+
- [ ] Crear `vite.config.ts` con TanStackRouterVite plugin (PRIMERO)
|
|
1159
|
+
- [ ] Crear `src/router.tsx` con configuración del router
|
|
1160
|
+
- [ ] Crear `src/main.tsx` con RouterProvider
|
|
1161
|
+
- [ ] Crear `src/routes/__root.tsx`
|
|
1162
|
+
- [ ] Configurar path aliases en `tsconfig.json`
|
|
1163
|
+
- [ ] Agregar `routeTree.gen.ts` a `.prettierignore` y `.eslintignore`
|
|
1164
|
+
- [ ] Configurar Vitest
|
|
1165
|
+
|
|
1166
|
+
### Estructura de Rutas
|
|
1167
|
+
|
|
1168
|
+
- [ ] Crear layouts pathless (`_auth.tsx`, `_app.tsx`)
|
|
1169
|
+
- [ ] Implementar guards de autenticación en `beforeLoad`
|
|
1170
|
+
- [ ] Usar `$param` para rutas dinámicas
|
|
1171
|
+
- [ ] Usar `-` para carpetas de colocación
|
|
1172
|
+
- [ ] Verificar que las URLs sean correctas
|
|
1173
|
+
|
|
1174
|
+
### Arquitectura
|
|
1175
|
+
|
|
1176
|
+
- [ ] Organizar módulos siguiendo Module/Domain/Feature
|
|
1177
|
+
- [ ] Implementar capas de Clean Architecture por feature
|
|
1178
|
+
- [ ] Configurar stores Zustand por feature
|
|
1179
|
+
- [ ] Configurar TanStack Query para server state
|
|
1180
|
+
- [ ] Crear barrel exports (`index.ts`) por feature
|
|
1181
|
+
|
|
1182
|
+
### Calidad
|
|
1183
|
+
|
|
1184
|
+
- [ ] Configurar Vitest con coverage
|
|
1185
|
+
- [ ] Agregar tests unitarios para use cases
|
|
1186
|
+
- [ ] Agregar tests de componentes
|
|
1187
|
+
- [ ] Verificar accesibilidad (axe)
|
|
1188
|
+
- [ ] Validar textos en español
|
|
1189
|
+
|
|
1190
|
+
---
|
|
1191
|
+
|
|
1192
|
+
## Referencias
|
|
1193
|
+
|
|
1194
|
+
- [TanStack Router Documentation](https://tanstack.com/router/latest)
|
|
1195
|
+
- [TanStack Query Documentation](https://tanstack.com/query/latest)
|
|
1196
|
+
- [Zustand Documentation](https://zustand-demo.pmnd.rs/)
|
|
1197
|
+
- [Vite Documentation](https://vitejs.dev/)
|
|
1198
|
+
- [Vitest Documentation](https://vitest.dev/)
|
|
1199
|
+
- [Clean Architecture - Robert C. Martin](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|