@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 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
+ });