@bobtail.software/b-ssr 1.0.64 → 1.0.65
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 +593 -0
- package/bin/generate-rpc-types.mjs +91 -0
- package/dist/fastify-b-ssr-plugin.cjs +1 -1
- package/dist/fastify-b-ssr-plugin.d.cts +2 -0
- package/dist/fastify-b-ssr-plugin.d.ts +2 -0
- package/dist/fastify-b-ssr-plugin.js +1 -1
- package/dist/rpc-type-generator.cjs +9 -0
- package/dist/rpc-type-generator.d.cts +14 -0
- package/dist/rpc-type-generator.d.ts +14 -0
- package/dist/rpc-type-generator.js +9 -0
- package/dist/vite-rpc-plugin.cjs +35 -27
- package/dist/vite-rpc-plugin.d.cts +1 -0
- package/dist/vite-rpc-plugin.d.ts +1 -0
- package/dist/vite-rpc-plugin.js +36 -28
- package/package.json +27 -6
package/README.md
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# @bobtail.software/b-ssr
|
|
2
|
+
|
|
3
|
+
**Fastify + Vite + React SSR + TanStack Router + RPC**
|
|
4
|
+
|
|
5
|
+
`@bobtail.software/b-ssr` es una solución integral para construir aplicaciones "monolíticas" modernas con TypeScript. Combina la potencia de **Fastify** en el backend y **Vite** en el frontend, proporcionando Server-Side Rendering (SSR) y una capa de **RPC (Remote Procedure Call)** totalmente tipada sin necesidad de generar código manual ni mantener definiciones de API separadas.
|
|
6
|
+
|
|
7
|
+
## 🚀 Características Principales
|
|
8
|
+
|
|
9
|
+
- **Integración Profunda Fastify & Vite:** Manejo automático del servidor de desarrollo de Vite (HMR) y servicio de estáticos en producción.
|
|
10
|
+
- **End-to-End Type Safety:** Define tus rutas en el backend con esquemas **Zod**. La librería genera automáticamente los tipos para el cliente. Si cambias el backend, el frontend te avisa del error.
|
|
11
|
+
- **RPC Transparente:** Llama a tus funciones del backend directamente desde el frontend como si fueran funciones locales.
|
|
12
|
+
- `addRpcRoute`: Para mutaciones (POST).
|
|
13
|
+
- `addLoaderRoute`: Para fetching de datos (GET), ideal para loaders.
|
|
14
|
+
- **Soporte TanStack Router:** Helpers específicos (`createServerHandler`, `hydrateClient`) para integrar SSR con TanStack Router fácilmente.
|
|
15
|
+
- **Gestión de Archivos:** Soporte nativo para `multipart/form-data` validado con Zod.
|
|
16
|
+
- **Seguridad (Firewall):** El plugin de Vite impide que código sensible del backend (base de datos, secretos) se filtre al bundle del cliente.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📦 Instalación
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @bobtail.software/b-ssr fastify vite zod @tanstack/react-router react react-dom
|
|
24
|
+
pnpm add -D @types/node @types/react @types/react-dom
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ⚙️ Configuración
|
|
30
|
+
|
|
31
|
+
### 1. Configuración del Servidor Fastify (`server.ts`)
|
|
32
|
+
|
|
33
|
+
Registra el plugin principal. Esto habilitará el middleware de Vite y los decoradores de rutas.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import Fastify from 'fastify';
|
|
37
|
+
import bSsrPlugin from '@bobtail.software/b-ssr';
|
|
38
|
+
import path from 'path';
|
|
39
|
+
import { fileURLToPath } from 'url';
|
|
40
|
+
|
|
41
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
|
|
43
|
+
const fastify = Fastify();
|
|
44
|
+
|
|
45
|
+
await fastify.register(bSsrPlugin, {
|
|
46
|
+
root: process.cwd(),
|
|
47
|
+
// Archivo de entrada para SSR en desarrollo
|
|
48
|
+
devEntryFile: '/src/entry-server.tsx',
|
|
49
|
+
// Archivo compilado para producción
|
|
50
|
+
prodEntryFile: './dist/server/entry-server.mjs',
|
|
51
|
+
// Carpeta de assets del cliente en producción
|
|
52
|
+
clientDistDir: './dist/client',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Importa tus rutas de backend aquí
|
|
56
|
+
await fastify.register(import('./src/routers/my-router.js'));
|
|
57
|
+
|
|
58
|
+
await fastify.listen({ port: 3000 });
|
|
59
|
+
console.log('Server running on http://localhost:3000');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. Configuración de Vite (`vite.config.ts`)
|
|
63
|
+
|
|
64
|
+
Necesitas el plugin `rpcGeneratorPlugin` para habilitar la magia de los tipos y la separación cliente/servidor.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { defineConfig } from 'vite';
|
|
68
|
+
import react from '@vitejs/plugin-react';
|
|
69
|
+
import { rpcGeneratorPlugin } from '@bobtail.software/b-ssr/vite-plugin';
|
|
70
|
+
import { TanStackRouterVite } from '@tanstack/router-vite-plugin';
|
|
71
|
+
|
|
72
|
+
export default defineConfig({
|
|
73
|
+
plugins: [
|
|
74
|
+
react(),
|
|
75
|
+
TanStackRouterVite(),
|
|
76
|
+
rpcGeneratorPlugin({
|
|
77
|
+
// Patrón para encontrar tus archivos de rutas backend
|
|
78
|
+
routerPattern: 'src/routers/**/*.mts',
|
|
79
|
+
// Dónde se generarán los tipos
|
|
80
|
+
routerBaseDir: 'src/routers',
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 🔄 Generación de Tipos Standalone
|
|
89
|
+
|
|
90
|
+
Por defecto, el plugin de Vite genera tipos automáticamente cuando el servidor de desarrollo está activo. Sin embargo, hay casos donde necesitas generar tipos sin el servidor de desarrollo:
|
|
91
|
+
|
|
92
|
+
- Scripts de build de producción
|
|
93
|
+
- Pre-commit hooks (lint-staged)
|
|
94
|
+
- CI/CD pipelines
|
|
95
|
+
- Type-checking sin servidor dev
|
|
96
|
+
|
|
97
|
+
### Uso Programático
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { generateRpcTypes } from '@bobtail.software/b-ssr/type-generator';
|
|
101
|
+
|
|
102
|
+
await generateRpcTypes({
|
|
103
|
+
routerPattern: 'src/routers/**/*.mts',
|
|
104
|
+
tsConfigFilePath: 'tsconfig.json',
|
|
105
|
+
routerBaseDir: 'src/routers',
|
|
106
|
+
clean: true, // Opcional: elimina archivos .d.ts orphans
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Uso con CLI
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Generar tipos (lee configuración desde vite.config.ts)
|
|
114
|
+
pnpm generate:types
|
|
115
|
+
|
|
116
|
+
# Generar tipos y limpiar orphans
|
|
117
|
+
pnpm generate:types:clean
|
|
118
|
+
|
|
119
|
+
# Con npx (instalado globalmente)
|
|
120
|
+
generate-rpc-types
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Scripts de Build
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"scripts": {
|
|
128
|
+
"build": "pnpm generate:types && vite build",
|
|
129
|
+
"type-check": "pnpm generate:types && tsc --noEmit",
|
|
130
|
+
"pre-commit": "pnpm generate:types:clean && git add src/**/*.universal.d.ts"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Cuándo Usar Standalone vs Dev Server
|
|
136
|
+
|
|
137
|
+
| Escenario | Recomendación |
|
|
138
|
+
| ------------------------- | --------------------------------------------------------- |
|
|
139
|
+
| **Desarrollo activo** | Usa el plugin de Vite (generación automática en dev mode) |
|
|
140
|
+
| **Build de producción** | Usa `pnpm generate:types && vite build` |
|
|
141
|
+
| **Type-checking en CI** | Usa `pnpm generate:types && tsc --noEmit` |
|
|
142
|
+
| **Pre-commit hooks** | Usa `pnpm generate:types:clean` |
|
|
143
|
+
| **Script personalizados** | Usa `generateRpcTypes()` programáticamente |
|
|
144
|
+
|
|
145
|
+
**Por qué ambas opciones son necesarias:**
|
|
146
|
+
|
|
147
|
+
- El plugin de Vite genera automáticamente durante desarrollo (HMR, watch mode)
|
|
148
|
+
- El generador standalone es para escenarios donde NO hay servidor Vite (CI, scripts de build, etc.)
|
|
149
|
+
- Ambos usan la misma lógica interna, garantizando consistencia
|
|
150
|
+
|
|
151
|
+
### Ejemplo de Integración con CI/CD
|
|
152
|
+
|
|
153
|
+
**GitHub Actions:**
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
name: Build and Test
|
|
157
|
+
|
|
158
|
+
on: [push, pull_request]
|
|
159
|
+
|
|
160
|
+
jobs:
|
|
161
|
+
build:
|
|
162
|
+
runs-on: ubuntu-latest
|
|
163
|
+
steps:
|
|
164
|
+
- uses: actions/checkout@v4
|
|
165
|
+
- uses: pnpm/action-setup@v2
|
|
166
|
+
- uses: actions/setup-node@v4
|
|
167
|
+
with:
|
|
168
|
+
node-version: '18'
|
|
169
|
+
- run: pnpm install
|
|
170
|
+
- run: pnpm generate:types # Genera tipos para type-check
|
|
171
|
+
- run: pnpm type-check
|
|
172
|
+
- run: pnpm build
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**GitLab CI:**
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
build:
|
|
179
|
+
image: node:18
|
|
180
|
+
script:
|
|
181
|
+
- pnpm install
|
|
182
|
+
- pnpm generate:types # Genera tipos antes de build
|
|
183
|
+
- pnpm type-check
|
|
184
|
+
- pnpm build
|
|
185
|
+
artifacts:
|
|
186
|
+
paths:
|
|
187
|
+
- dist/
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
> **Nota de Compatibilidad:** El generador standalone usa la misma lógica interna que el plugin de Vite, garantizando que los tipos generados son idénticos en ambos casos. No necesitas elegir entre un enfoque u otro - ambos pueden coexistir perfectamente.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### Configuración del CLI
|
|
195
|
+
|
|
196
|
+
El CLI lee automáticamente la configuración del plugin desde tu `vite.config.ts`:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// vite.config.ts
|
|
200
|
+
export default defineConfig({
|
|
201
|
+
plugins: [
|
|
202
|
+
rpcGeneratorPlugin({
|
|
203
|
+
routerPattern: 'src/routers/**/*.mts', // ← Usado por CLI
|
|
204
|
+
routerBaseDir: 'src/routers', // ← Usado por CLI
|
|
205
|
+
tsConfigFilePath: 'tsconfig.json', // ← Usado por CLI
|
|
206
|
+
}),
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Si no hay `vite.config.ts`, el CLI usa valores por defecto:
|
|
212
|
+
|
|
213
|
+
- `routerPattern`: `src-ts/routers/**/*.mts`
|
|
214
|
+
- `tsConfigFilePath`: `tsconfig.json`
|
|
215
|
+
- `routerBaseDir`: `src-ts/routers`
|
|
216
|
+
- `clean`: `false`
|
|
217
|
+
|
|
218
|
+
### Modo `--clean`
|
|
219
|
+
|
|
220
|
+
El modo `clean` elimina archivos `.universal.d.ts` orphans:
|
|
221
|
+
|
|
222
|
+
1. Archivos `.universal.d.ts` que no tienen correspondiente `.mts`/`.ts`
|
|
223
|
+
2. Archivos `.universal.d.ts` de archivos `.mts`/`.ts` que ya no definen rutas RPC
|
|
224
|
+
|
|
225
|
+
Esto es útil cuando renombras o eliminas archivos de rutas backend.
|
|
226
|
+
|
|
227
|
+
**Advertencia:** Asegúrate de que el modo `clean` no elimine archivos necesarios antes de usarlo en scripts automáticos.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 🛠️ Uso y Definición de Rutas
|
|
232
|
+
|
|
233
|
+
La librería utiliza la extensión `.mts` (o `.ts`) para definir rutas de backend que exportan funciones RPC.
|
|
234
|
+
|
|
235
|
+
### 1. Crear un Router (Backend)
|
|
236
|
+
|
|
237
|
+
Crea un archivo, por ejemplo `src/routers/users.mts`.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import type { FastifyInstance } from 'fastify';
|
|
241
|
+
import { z } from 'zod';
|
|
242
|
+
|
|
243
|
+
export default async function userRouter(fastify: FastifyInstance) {
|
|
244
|
+
// 1. RPC (Mutation/Action) - Método POST
|
|
245
|
+
fastify.addRpcRoute('/create-user', {
|
|
246
|
+
schema: {
|
|
247
|
+
body: z.object({
|
|
248
|
+
name: z.string(),
|
|
249
|
+
email: z.string().email(),
|
|
250
|
+
}),
|
|
251
|
+
},
|
|
252
|
+
handler: async (req, reply) => {
|
|
253
|
+
// req.body está tipado automáticamente
|
|
254
|
+
const { name, email } = req.body;
|
|
255
|
+
return { success: true, id: 123, message: `User ${name} created` };
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 2. Loader (Query) - Método GET
|
|
260
|
+
fastify.addLoaderRoute('/get-user', {
|
|
261
|
+
schema: {
|
|
262
|
+
querystring: z.object({
|
|
263
|
+
id: z.string(),
|
|
264
|
+
}),
|
|
265
|
+
},
|
|
266
|
+
handler: async (req, reply) => {
|
|
267
|
+
const { id } = req.query;
|
|
268
|
+
return { id, name: 'Victor', role: 'admin' };
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 2. Consumir en el Cliente (Frontend)
|
|
275
|
+
|
|
276
|
+
Aquí ocurre la magia. Importas desde el archivo `.universal`. El plugin de Vite intercepta esta importación y te entrega un cliente ligero que hace `fetch`, manteniendo los tipos de retorno y argumentos.
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
// src/components/CreateUser.tsx
|
|
280
|
+
import React from 'react';
|
|
281
|
+
// NOTA: Importamos desde .universal, no desde .mts directamente
|
|
282
|
+
import { actionCreateUser, loaderGetUser } from '../routers/users.universal';
|
|
283
|
+
|
|
284
|
+
export function CreateUser() {
|
|
285
|
+
const handleSubmit = async () => {
|
|
286
|
+
try {
|
|
287
|
+
// TypeScript autocompleta 'body' y valida los tipos
|
|
288
|
+
const result = await actionCreateUser({
|
|
289
|
+
body: { name: 'Victor', email: 'test@example.com' },
|
|
290
|
+
});
|
|
291
|
+
console.log(result.message);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error(err);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
return <button onClick={handleSubmit}>Crear Usuario</button>;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
> **Nota:** El nombre de la función exportada se genera automáticamente basado en la URL.
|
|
302
|
+
>
|
|
303
|
+
> - `/create-user` (RPC) -> `actionCreateUser`
|
|
304
|
+
> - `/get-user` (Loader) -> `loaderGetUser`
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## 🌐 Integración con TanStack Router (SSR)
|
|
309
|
+
|
|
310
|
+
La librería exporta helpers específicos para hidratar y renderizar TanStack Router.
|
|
311
|
+
|
|
312
|
+
### Entry Server (`src/entry-server.tsx`)
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
import { createServerHandler } from '@bobtail.software/b-ssr/tanstack-server';
|
|
316
|
+
import { createRouter } from './router'; // Tu función que crea el router
|
|
317
|
+
|
|
318
|
+
// Exporta la función 'render' que Fastify llamará
|
|
319
|
+
export const render = createServerHandler(() => createRouter());
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Entry Client (`src/entry-client.tsx`)
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
import { hydrateClient } from '@bobtail.software/b-ssr/tanstack-client';
|
|
326
|
+
import { createRouter } from './router';
|
|
327
|
+
|
|
328
|
+
hydrateClient(() => createRouter());
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Render Route en Fastify
|
|
332
|
+
|
|
333
|
+
Para servir la aplicación HTML, usa `addRenderRoute` en tu servidor:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// server.ts
|
|
337
|
+
fastify.addRenderRoute('/*', {
|
|
338
|
+
handler: async (req, reply) => {
|
|
339
|
+
// Puedes pasar datos iniciales al SSR aquí si lo deseas
|
|
340
|
+
return { user: req.user };
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 📂 Estructura de Archivos Recomendada
|
|
348
|
+
|
|
349
|
+
```text
|
|
350
|
+
.
|
|
351
|
+
├── src/
|
|
352
|
+
│ ├── routers/ # Rutas Backend (RPCs)
|
|
353
|
+
│ │ ├── users.mts # Definición de rutas con Zod
|
|
354
|
+
│ │ └── posts.mts
|
|
355
|
+
│ ├── routes/ # Rutas de TanStack Router (Frontend)
|
|
356
|
+
│ ├── entry-server.tsx # Punto de entrada SSR
|
|
357
|
+
│ ├── entry-client.tsx # Punto de entrada Hidratación
|
|
358
|
+
│ └── router.tsx # Configuración de TanStack Router
|
|
359
|
+
├── server.ts # Servidor Fastify
|
|
360
|
+
├── vite.config.ts
|
|
361
|
+
└── package.json
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## 🧪 Testing
|
|
365
|
+
|
|
366
|
+
El proyecto utiliza **Vitest** para ejecutar una suite de tests comprehensiva que cubre:
|
|
367
|
+
|
|
368
|
+
- **Generación de tipos** - Prueba la generación automática de tipos `.d.ts` desde rutas backend (dev mode y standalone)
|
|
369
|
+
- **Validación de Zod** - Verifica edge cases complejos de esquemas Zod (refine, transform, pipe, union, etc.)
|
|
370
|
+
- **Error Handling** - Prueba validaciones de errores (400, 401, 403, 500) en escenarios reales
|
|
371
|
+
- **SSR Hydration** - Valida serialización correcta de datos complejos (Date, BigInt, Map, Set, etc.)
|
|
372
|
+
- **Firewall de Seguridad** - Verifica que código de backend no se filtre al bundle del cliente
|
|
373
|
+
- **Cliente RPC** - Prueba la lógica de llamadas RPC desde el frontend
|
|
374
|
+
|
|
375
|
+
### Scripts de Test
|
|
376
|
+
|
|
377
|
+
Ejecuta los tests con los siguientes comandos:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
# Ejecutar todos los tests una vez
|
|
381
|
+
pnpm test
|
|
382
|
+
|
|
383
|
+
# Ejecutar tests en modo watch (recomendado durante desarrollo)
|
|
384
|
+
pnpm test:watch
|
|
385
|
+
|
|
386
|
+
# Ejecutar tests con interfaz visual
|
|
387
|
+
pnpm test:ui
|
|
388
|
+
|
|
389
|
+
# Ejecutar tests con reporte de cobertura
|
|
390
|
+
pnpm test:coverage
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Cobertura de Tests
|
|
394
|
+
|
|
395
|
+
- **70/70 tests passing (100% de tests ejecutados)**
|
|
396
|
+
- **4 tests en skip** - Limitaciones conocidas del generador de tipos
|
|
397
|
+
|
|
398
|
+
**Distribución:**
|
|
399
|
+
|
|
400
|
+
- Unit Tests: 27 tests (vite-rpc-plugin, standalone type generator, virtual modules, firewall)
|
|
401
|
+
- Integration Tests: 43 tests (zod-validation, error-handling, ssr-hydration, rpc-client)
|
|
402
|
+
|
|
403
|
+
## 🔒 Seguridad
|
|
404
|
+
|
|
405
|
+
El plugin `vite-rpc-plugin` incluye un **Firewall**. Si intentas importar un archivo `.mts` de backend directamente en un archivo de cliente (sin usar la extensión `.universal`), el build fallará o lanzará un error en tiempo de ejecución, previniendo que código de servidor llegue al navegador.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## ⚠️ Limitaciones Conocidas
|
|
410
|
+
|
|
411
|
+
### Query Params y Path Params con Transforms
|
|
412
|
+
|
|
413
|
+
**Limitación:** Al convertir Zod schemas a JSON Schema para Fastify, se pierden las transforms (ej: `.transform(Number)`, `.pipe()`). Esto afecta principalmente a `querystring` y `params`.
|
|
414
|
+
|
|
415
|
+
**Por qué ocurre:**
|
|
416
|
+
|
|
417
|
+
- Fastify valida usando JSON Schema (no Zod directamente)
|
|
418
|
+
- `z.toJSONSchema()` con `{ io: 'input' }` genera el tipo de entrada (string), perdiendo las transforms
|
|
419
|
+
- Los handlers reciben strings en lugar de numbers/datos transformados
|
|
420
|
+
|
|
421
|
+
**Ejemplo del problema:**
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// Schema con transform
|
|
425
|
+
fastify.addRpcRoute('/search', {
|
|
426
|
+
schema: {
|
|
427
|
+
querystring: z.object({
|
|
428
|
+
page: z.string().transform(Number).optional(), // ← Transform perdido
|
|
429
|
+
limit: z.string().transform(Number).optional(), // ← Transform perdido
|
|
430
|
+
}),
|
|
431
|
+
},
|
|
432
|
+
handler: async (req) => {
|
|
433
|
+
// req.query.page es '2' (string) en lugar de 2 (number)
|
|
434
|
+
// ❌ Causa TypeError o comportamiento incorrecto
|
|
435
|
+
return { page: req.query.page + 1, limit: req.query.limit };
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Solución recomendada (Workaround):**
|
|
441
|
+
|
|
442
|
+
Opción 1 - Parsear manualmente en el handler:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
handler: async (req) => {
|
|
446
|
+
const { page: pageStr, limit: limitStr } = req.query;
|
|
447
|
+
const page = pageStr ? Number(pageStr) : 1;
|
|
448
|
+
const limit = limitStr ? Number(limitStr) : 10;
|
|
449
|
+
return { page, limit, results: [...] };
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Opción 2 - Usar strings en el schema y parsear en el handler:
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
fastify.addRpcRoute('/search', {
|
|
457
|
+
schema: {
|
|
458
|
+
querystring: z.object({
|
|
459
|
+
page: z.string().optional(),
|
|
460
|
+
limit: z.string().optional(),
|
|
461
|
+
}),
|
|
462
|
+
},
|
|
463
|
+
handler: async (req) => {
|
|
464
|
+
const { page, limit } = req.query;
|
|
465
|
+
return { page: Number(page || 1), limit: Number(limit || 10) };
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**Nota:** Este workaround NO es necesario para el `body`, ya que los transforms en body se mantienen correctamente después de la conversión a JSON Schema.
|
|
471
|
+
|
|
472
|
+
### Generación de Tipos en Edge Cases Complejos
|
|
473
|
+
|
|
474
|
+
**Limitación:** El generador de tipos tiene limitaciones conocidas en algunos edge cases complejos de Zod.
|
|
475
|
+
|
|
476
|
+
#### multipart/form-data con File
|
|
477
|
+
|
|
478
|
+
**Problema:** Al usar `multipart/form-data` con archivos, el plugin no genera correctamente el tipo `File` en el body.
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
fastify.addRpcRoute('/upload', {
|
|
482
|
+
schema: {
|
|
483
|
+
consumes: ['multipart/form-data'],
|
|
484
|
+
body: z.object({
|
|
485
|
+
file: z.instanceof(File), // No se genera correctamente en .d.ts
|
|
486
|
+
}),
|
|
487
|
+
},
|
|
488
|
+
handler: async (req) => {
|
|
489
|
+
// ...
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Workaround:** Usa el tipo `File` manualmente en el cliente:
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
await actionUpload({
|
|
498
|
+
body: {
|
|
499
|
+
file: fileObject as unknown as File,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
#### Imports de Tipos Externos
|
|
505
|
+
|
|
506
|
+
**Problema:** El plugin no extrae imports de tipos desde archivos externos en la generación de `.d.ts`.
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
// types.ts
|
|
510
|
+
export interface User {
|
|
511
|
+
id: string;
|
|
512
|
+
name: string;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// router.mts
|
|
516
|
+
import type { User } from './types'; // No se extrae en .d.ts
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Workaround:** Define los tipos inline en el router o usa Zod schemas directamente:
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
// router.mts
|
|
523
|
+
const UserSchema = z.object({
|
|
524
|
+
id: z.string(),
|
|
525
|
+
name: z.string(),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
fastify.addRpcRoute('/create-user', {
|
|
529
|
+
schema: {
|
|
530
|
+
body: UserSchema, // Funciona correctamente
|
|
531
|
+
},
|
|
532
|
+
handler: async (req) => {
|
|
533
|
+
// req.body está tipado correctamente
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
#### Parsing Incompleto en Schemas Complejos
|
|
539
|
+
|
|
540
|
+
**Problema:** En algunos edge cases con Zod muy complejos, el plugin puede generar `body: any` en lugar de inferir el tipo completo.
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
// Edge case con pipes, transforms y refinements anidados
|
|
544
|
+
fastify.addRpcRoute('/complex-route', {
|
|
545
|
+
schema: {
|
|
546
|
+
body: z.object({
|
|
547
|
+
data: z
|
|
548
|
+
.string()
|
|
549
|
+
.transform((val) => val.toUpperCase())
|
|
550
|
+
.pipe(z.string().min(5))
|
|
551
|
+
.refine((val) => val.includes('X')),
|
|
552
|
+
}),
|
|
553
|
+
},
|
|
554
|
+
handler: async (req) => {
|
|
555
|
+
// req.body puede ser 'any' en lugar del tipo inferido
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**Workaround:** Simplifica el schema o define el tipo manualmente:
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
interface ComplexBody {
|
|
564
|
+
data: string;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
fastify.addRpcRoute('/complex-route', {
|
|
568
|
+
schema: {
|
|
569
|
+
body: z.object({
|
|
570
|
+
data: z
|
|
571
|
+
.string()
|
|
572
|
+
.min(5)
|
|
573
|
+
.refine((val) => val.includes('X')),
|
|
574
|
+
}),
|
|
575
|
+
},
|
|
576
|
+
handler: async (req, reply) => {
|
|
577
|
+
const body = req.body as ComplexBody;
|
|
578
|
+
// ...
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Tests en Skip (Limitaciones del Generador)
|
|
584
|
+
|
|
585
|
+
Los siguientes tests están marcados como `.skip()` debido a limitaciones conocidas del generador de tipos:
|
|
586
|
+
|
|
587
|
+
1. **multipart/form-data con File** - El plugin no genera correctamente el tipo `File` en el body para uploads de archivos. Ver la sección "Generación de Tipos en Edge Cases Complejos" arriba.
|
|
588
|
+
2. **Import types externos** - El plugin no extrae imports de tipos desde archivos externos en la generación de `.d.ts`. Ver "Imports de Tipos Externos" arriba.
|
|
589
|
+
3. **File watching y caching** - El plugin no genera/actualiza archivos `.d.ts` correctamente en escenarios complejos de cache y watch. Estas son limitaciones internas de `ts-morph` y `fast-glob` cuando se crean archivos dinámicamente en tests.
|
|
590
|
+
|
|
591
|
+
**Impacto:** Estas limitaciones no afectan la funcionalidad crítica del sistema. Los tests en skip representan edge cases complejos del generador de tipos que tienen workarounds documentados en esta sección.
|
|
592
|
+
|
|
593
|
+
## 📄 Licencia
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, resolve } from 'path';
|
|
6
|
+
import { generateRpcTypes } from '../dist/rpc-type-generator.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const root = process.cwd();
|
|
10
|
+
|
|
11
|
+
function parseViteConfig(configPath) {
|
|
12
|
+
try {
|
|
13
|
+
const viteConfigContent = readFileSync(configPath, 'utf-8');
|
|
14
|
+
|
|
15
|
+
const options = {
|
|
16
|
+
routerPattern: undefined,
|
|
17
|
+
tsConfigFilePath: undefined,
|
|
18
|
+
routerBaseDir: undefined,
|
|
19
|
+
clean: process.argv.includes('--clean'),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const pluginMatch = viteConfigContent.match(
|
|
23
|
+
/rpcGeneratorPlugin\s*\(\s*{([^}]+)}\s*\)/s
|
|
24
|
+
);
|
|
25
|
+
if (pluginMatch) {
|
|
26
|
+
const pluginOptions = pluginMatch[1];
|
|
27
|
+
|
|
28
|
+
const routerPatternMatch = pluginOptions.match(
|
|
29
|
+
/routerPattern:\s*['"`]([^'"`]+)['"`]/
|
|
30
|
+
);
|
|
31
|
+
if (routerPatternMatch) {
|
|
32
|
+
options.routerPattern = routerPatternMatch[1];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tsConfigMatch = pluginOptions.match(
|
|
36
|
+
/tsConfigFilePath:\s*['"`]([^'"`]+)['"`]/
|
|
37
|
+
);
|
|
38
|
+
if (tsConfigMatch) {
|
|
39
|
+
options.tsConfigFilePath = tsConfigMatch[1];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const routerBaseDirMatch = pluginOptions.match(
|
|
43
|
+
/routerBaseDir:\s*['"`]([^'"`]+)['"`]/
|
|
44
|
+
);
|
|
45
|
+
if (routerBaseDirMatch) {
|
|
46
|
+
options.routerBaseDir = routerBaseDirMatch[1];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return options;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Warning: Could not parse vite.config.ts, using defaults');
|
|
53
|
+
return {
|
|
54
|
+
clean: process.argv.includes('--clean'),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
const viteConfigPath = resolve(root, 'vite.config.ts');
|
|
61
|
+
let options = { clean: process.argv.includes('--clean') };
|
|
62
|
+
|
|
63
|
+
if (existsSync(viteConfigPath)) {
|
|
64
|
+
console.log('📖 Reading configuration from vite.config.ts...');
|
|
65
|
+
const viteConfigOptions = parseViteConfig(viteConfigPath);
|
|
66
|
+
options = { ...options, ...viteConfigOptions };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = await generateRpcTypes(options);
|
|
70
|
+
|
|
71
|
+
if (result.generated.length > 0) {
|
|
72
|
+
console.log(`✅ Generated ${result.generated.length} type file(s)`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log('ℹ️ No type files generated (no RPC routes found)');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (result.cleaned.length > 0) {
|
|
78
|
+
console.log(`🧹 Cleaned ${result.cleaned.length} orphan file(s)`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (result.errors.length > 0) {
|
|
82
|
+
console.error(`\n❌ ${result.errors.length} error(s) occurred:`);
|
|
83
|
+
result.errors.forEach((err) => console.error(` - ${err}`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
main().catch((error) => {
|
|
89
|
+
console.error('Fatal error:', error);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|