@bobtail.software/b-ssr 1.1.1 → 1.1.2

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 CHANGED
@@ -59,6 +59,71 @@ await fastify.listen({ port: 3000 });
59
59
  console.log('Server running on http://localhost:3000');
60
60
  ```
61
61
 
62
+ ### Multi-entry SSR (opcional)
63
+
64
+ Puedes definir múltiples entry points de SSR y escogerlos por ruta o con un resolver.
65
+
66
+ ```typescript
67
+ await fastify.register(bSsrPlugin, {
68
+ root: process.cwd(),
69
+ entries: [
70
+ {
71
+ name: 'app',
72
+ match: '/app',
73
+ devEntryFile: '/src/entry-app-server.tsx',
74
+ prodEntryFile: './dist/app/server/entry-server.mjs',
75
+ },
76
+ {
77
+ name: 'admin',
78
+ match: /^\\/admin/,
79
+ devEntryFile: '/src/entry-admin-server.tsx',
80
+ prodEntryFile: './dist/admin/server/entry-server.mjs',
81
+ },
82
+ {
83
+ name: 'fallback',
84
+ devEntryFile: '/src/entry-server.tsx',
85
+ prodEntryFile: './dist/server/entry-server.mjs',
86
+ },
87
+ ],
88
+ // Solo se sirve un clientDistDir por defecto.
89
+ // Si necesitas varios, registra static manualmente.
90
+ clientDistDir: './dist/client',
91
+ });
92
+ ```
93
+
94
+ Selección de entry (orden de prioridad):
95
+
96
+ 1. `entry` en `addRenderRoute` (override explícito)
97
+ 2. `resolveEntry(req)` en opciones del plugin
98
+ 3. `entries[].match` (string, RegExp, o función)
99
+ 4. Fallback sin `match`
100
+ 5. `devEntryFile` / `prodEntryFile` (single-entry legacy)
101
+
102
+ Resolver global:
103
+
104
+ ```typescript
105
+ const entries = [
106
+ {
107
+ name: 'app',
108
+ devEntryFile: '/src/entry-app-server.tsx',
109
+ prodEntryFile: './dist/app/server/entry-server.mjs',
110
+ },
111
+ {
112
+ name: 'admin',
113
+ devEntryFile: '/src/entry-admin-server.tsx',
114
+ prodEntryFile: './dist/admin/server/entry-server.mjs',
115
+ },
116
+ ];
117
+
118
+ await fastify.register(bSsrPlugin, {
119
+ root: process.cwd(),
120
+ entries,
121
+ resolveEntry: (req) => (req.headers['x-entry'] === 'admin' ? entries[1] : null),
122
+ });
123
+ ```
124
+
125
+ Si `entries` no existe, el comportamiento es el mismo que antes (single-entry).
126
+
62
127
  ### 2. Configuración de Vite (`vite.config.ts`)
63
128
 
64
129
  Necesitas el plugin `rpcGeneratorPlugin` para habilitar la magia de los tipos y la separación cliente/servidor.
@@ -342,6 +407,14 @@ fastify.addRenderRoute('/*', {
342
407
  });
343
408
  ```
344
409
 
410
+ Puedes forzar un entry específico por ruta usando `entry` (aplica también a sub-rutas por el wildcard):
411
+
412
+ ```typescript
413
+ fastify.addRenderRoute('/admin', { entry: 'admin' });
414
+ ```
415
+
416
+ `entry` debe coincidir con `entries[].name`.
417
+
345
418
  ---
346
419
 
347
420
  ## 📂 Estructura de Archivos Recomendada
@@ -392,13 +465,12 @@ pnpm test:coverage
392
465
 
393
466
  ### Cobertura de Tests
394
467
 
395
- - **70/70 tests passing (100% de tests ejecutados)**
396
- - **4 tests en skip** - Limitaciones conocidas del generador de tipos
468
+ La suite cubre unit tests e integration tests. Algunos tests están en `skip` por limitaciones conocidas del generador de tipos.
397
469
 
398
- **Distribución:**
470
+ **Distribución (general):**
399
471
 
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)
472
+ - Unit Tests: vite-rpc-plugin, type generator standalone, virtual modules, firewall
473
+ - Integration Tests: zod-validation, error-handling, ssr-hydration, rpc-client
402
474
 
403
475
  ## 🔒 Seguridad
404
476
 
@@ -1 +1 @@
1
- "use strict";var D=Object.create;var R=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var I=Object.getPrototypeOf,G=Object.prototype.hasOwnProperty;var P=(t,r)=>{for(var a in r)R(t,a,{get:r[a],enumerable:!0})},T=(t,r,a,l)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of b(r))!G.call(t,o)&&o!==a&&R(t,o,{get:()=>r[o],enumerable:!(l=O(r,o))||l.enumerable});return t};var S=(t,r,a)=>(a=t!=null?D(I(t)):{},T(r||!t||!t.__esModule?R(a,"default",{value:t,enumerable:!0}):a,t)),k=t=>T(R({},"__esModule",{value:!0}),t);var H={};P(H,{default:()=>_});module.exports=k(H);var $=require("@fastify/multipart"),w=S(require("fastify-plugin"),1),v=S(require("path"),1);async function E(t=24678){let r=await import("net");return new Promise((a,l)=>{let o=r.createServer();o.listen(t,"127.0.0.1",()=>{let e=o.address(),n=typeof e=="object"&&e!==null?e.port:t;o.close(()=>a(n))}),o.on("error",e=>{e.code==="EADDRINUSE"?o.close(()=>a(E(t+1))):l(e)})})}function g(t){if(t)return t}var A=async(t,r)=>{if(t.body&&typeof t.body=="object"){let a=t.body,l={};for(let o of Object.keys(a)){let e=a[o];e&&typeof e=="object"?e.type==="field"?l[o]=e.value:e.type==="file"&&(l[o]={filename:e.filename,mimetype:e.mimetype,encoding:e.encoding,fieldname:e.fieldname,file:e.file}):l[o]=e}t.body=l}},C=async(t,r)=>{let a=e=>{if(r.errorHandler){let n=r.errorHandler(e);if(n)return n}return{message:"Internal Server Error",statusCode:500}};if(t.hasDecorator("viteInitDone")&&t.viteInitDone)return;if(!t.hasDecorator("multipartErrors"))try{let e=await import("@fastify/multipart");await t.register(e.default)}catch(e){if(e.code!=="FST_ERR_DEC_ALREADY_PRESENT")throw e}let l=process.env.NODE_ENV==="production";if(l){if(r.clientDistDir)try{let e=await import("@fastify/static"),n=v.default.resolve(r.root,r.clientDistDir);await t.register(e.default,{root:n,wildcard:!1}),console.log(`\u{1F4C2} [Fastify-SSR] Serving static assets from: ${n}`)}catch(e){console.error("\u274C [Fastify-SSR] Error registering @fastify/static. Did you install it?",e)}}else{if(!t.hasDecorator("use"))try{let c=await import("@fastify/middie");await t.register(c.default,{hook:"onRequest"})}catch(c){if(c.code!=="FST_ERR_DEC_ALREADY_PRESENT")throw c}let e=await import("vite"),n=r.viteConfig||{},f;n.server?.hmr&&typeof n.server.hmr=="object"&&"port"in n.server.hmr?f=n.server.hmr.port:f=r.hmrPort??await E();let u=await e.createServer({root:r.root,appType:"custom",...n,server:{hmr:{port:f},...n.server,middlewareMode:!0}});t.use(u.middlewares),t.hasDecorator("viteServer")||t.decorate("viteServer",u),console.log("\u{1F680} [Fastify-SSR] Vite Dev Server Ready")}t.decorate("viteInitDone",!0);let o=async(e,n,f)=>{try{let u=e.raw.url,c=r.getGlobalSettings?await r.getGlobalSettings(e):void 0;if(v.default.extname(u)!==""){n.status(404).send(`File not found: ${u}`);return}let s="";!l&&t.viteServer&&(s=await t.viteServer.transformIndexHtml(u,"<html><head></head><body></body></html>"),s=s.substring(s.indexOf("<head>")+6,s.indexOf("</head>"))),s=(c?`<script>window.__GLOBAL_SETTINGS__ = ${JSON.stringify(c)}</script>`:"")+s;let m=l?await import(v.default.resolve(r.root,r.prodEntryFile)):await t.viteServer.ssrLoadModule(r.devEntryFile),i=m.render||m.default;if(typeof i!="function")throw new Error(`Entry file ${r.devEntryFile} must export a 'render' function.`);await i({req:e,reply:n,head:s,data:f,globalSettings:c})}catch(u){t.viteServer?.ssrFixStacktrace(u),console.error("[SSR Error]:",u),n.sent||n.status(500).send("Internal Server Error")}};t.decorate("addRpcRoute",function(e,n){let{handler:f,schema:u,...c}=n,s=`/rpc${e}`,p=g(u),m={...c,schema:p};n?.schema?.consumes?.includes("multipart/form-data")&&(m.preValidation=A),this.route({method:"POST",url:s,...m,handler:async(h,d)=>{try{let y=await f.call(this,h,d);return d.sent?void 0:y}catch(y){if(console.error(`[RPC Error] ${s}:`,y),!d.sent){let{statusCode:F,message:x}=a(y);d.status(F).send({error:{message:x}})}}}})}),t.decorate("addRenderRoute",function(e,n){let{handler:f,schema:u,...c}=n||{},s=g(u),p=async(m,i,h)=>{try{let d=await f?.call(this,m,i);return h?d:o(m,i,d)}catch(d){if(console.error(`[Render Error] ${e}:`,d),!i.sent){let{statusCode:y,message:F}=a(d);if(h)i.status(y).send({error:{message:F}});else return o(m,i,{ssrError:{statusCode:500,message:"Internal Error"}})}}};if(this.route({method:"GET",url:e,schema:s,...c,handler:(m,i)=>p(m,i,!1)}),e!=="*"&&e!=="/*"){this.route({method:"GET",url:`/loader${e}`,schema:s,...c,handler:(h,d)=>p(h,d,!0)});let m=e.endsWith("/")?"*":"/*",i=`${e}${m}`;this.route({method:"GET",url:i,schema:s,...c,handler:(h,d)=>p(h,d,!1)})}}),t.decorate("addLoaderRoute",function(e,n){let{handler:f,schema:u,...c}=n,s=`/api${e}`,p=g(u);this.route({method:"GET",url:s,schema:p,...c,handler:async(m,i)=>{try{let h=await f.call(this,m,i);return i.sent?void 0:h}catch(h){if(console.error(`[Loader API Error] ${s}:`,h),!i.sent){let{statusCode:d,message:y}=a(h);i.status(d).send({error:{message:y}})}}}})})},_=(0,w.default)(C,{name:"fastify-b-ssr"});
1
+ "use strict";var b=Object.create;var v=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var G=Object.getPrototypeOf,P=Object.prototype.hasOwnProperty;var k=(e,r)=>{for(var n in r)v(e,n,{get:r[n],enumerable:!0})},w=(e,r,n,f)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of I(r))!P.call(e,o)&&o!==n&&v(e,o,{get:()=>r[o],enumerable:!(f=O(r,o))||f.enumerable});return e};var R=(e,r,n)=>(n=e!=null?b(G(e)):{},w(r||!e||!e.__esModule?v(n,"default",{value:e,enumerable:!0}):n,e)),q=e=>w(v({},"__esModule",{value:!0}),e);var Z={};k(Z,{default:()=>H});module.exports=q(Z);var N=require("@fastify/multipart"),T=R(require("fastify-plugin"),1),E=R(require("path"),1);async function D(e=24678){let r=await import("net");return new Promise((n,f)=>{let o=r.createServer();o.listen(e,"127.0.0.1",()=>{let t=o.address(),s=typeof t=="object"&&t!==null?t.port:e;o.close(()=>n(s))}),o.on("error",t=>{t.code==="EADDRINUSE"?o.close(()=>n(D(e+1))):f(t)})})}function F(e){if(e)return e}function x(e){let r=e.raw?.url||e.url||"",n=r.indexOf("?");return n===-1?r:r.slice(0,n)}function A(e,r,n){return e.match?typeof e.match=="string"?n.startsWith(e.match):e.match instanceof RegExp?e.match.test(n):e.match(r):!1}function C(e,r){if(r.resolveEntry){let t=r.resolveEntry(e);if(t)return t}let n=r.entries||[];if(n.length===0)return;let f=x(e),o;for(let t of n){if(!t.match){o||(o=t);continue}if(A(t,e,f))return t}return o}function _(e,r){if(!(!r.entries||r.entries.length===0))return r.entries.find(n=>n.name===e)}var $=async(e,r)=>{if(e.body&&typeof e.body=="object"){let n=e.body,f={};for(let o of Object.keys(n)){let t=n[o];t&&typeof t=="object"?t.type==="field"?f[o]=t.value:t.type==="file"&&(f[o]={filename:t.filename,mimetype:t.mimetype,encoding:t.encoding,fieldname:t.fieldname,file:t.file}):f[o]=t}e.body=f}},M=async(e,r)=>{let n=t=>{if(r.errorHandler){let s=r.errorHandler(t);if(s)return s}return{message:"Internal Server Error",statusCode:500}};if(e.hasDecorator("viteInitDone")&&e.viteInitDone)return;if(!e.hasDecorator("multipartErrors"))try{let t=await import("@fastify/multipart");await e.register(t.default)}catch(t){if(t.code!=="FST_ERR_DEC_ALREADY_PRESENT")throw t}let f=process.env.NODE_ENV==="production";if(f){let t=new Set;r.clientDistDir&&t.add(r.clientDistDir);for(let d of r.entries||[])d.clientDistDir&&t.add(d.clientDistDir);t.size>1&&console.warn("\u26A0\uFE0F [Fastify-SSR] Multiple clientDistDir detected. Only the first will be served. Register additional static dirs manually if needed.");let[s]=t;if(s)try{let d=await import("@fastify/static"),h=E.default.resolve(r.root,s);await e.register(d.default,{root:h,wildcard:!1}),console.log(`\u{1F4C2} [Fastify-SSR] Serving static assets from: ${h}`)}catch(d){console.error("\u274C [Fastify-SSR] Error registering @fastify/static. Did you install it?",d)}}else{if(!e.hasDecorator("use"))try{let u=await import("@fastify/middie");await e.register(u.default,{hook:"onRequest"})}catch(u){if(u.code!=="FST_ERR_DEC_ALREADY_PRESENT")throw u}let t=await import("vite"),s=r.viteConfig||{},d;s.server?.hmr&&typeof s.server.hmr=="object"&&"port"in s.server.hmr?d=s.server.hmr.port:d=r.hmrPort??await D();let h=await t.createServer({root:r.root,appType:"custom",...s,server:{hmr:{port:d},...s.server,middlewareMode:!0}});e.use(h.middlewares),e.hasDecorator("viteServer")||e.decorate("viteServer",h),console.log("\u{1F680} [Fastify-SSR] Vite Dev Server Ready")}e.decorate("viteInitDone",!0);let o=async(t,s,d,h)=>{try{let u=t.raw.url,y=r.getGlobalSettings?await r.getGlobalSettings(t):void 0;if(E.default.extname(u)!==""){s.status(404).send(`File not found: ${u}`);return}let m="";!f&&e.viteServer&&(m=await e.viteServer.transformIndexHtml(u,"<html><head></head><body></body></html>"),m=m.substring(m.indexOf("<head>")+6,m.indexOf("</head>"))),m=(y?`<script>window.__GLOBAL_SETTINGS__ = ${JSON.stringify(y)}</script>`:"")+m;let a;if(h){if(a=_(h,r),!a)throw new Error(`Entry "${h}" not found. Make sure entries[] includes a matching name.`)}else a=C(t,r);let i=a?.devEntryFile??r.devEntryFile,l=a?.prodEntryFile??r.prodEntryFile;if(!i||!l)throw new Error(`No SSR entry resolved for request "${x(t)}". Provide devEntryFile/prodEntryFile or configure entries/resolveEntry.`);let c=f?await import(E.default.resolve(r.root,l)):await e.viteServer.ssrLoadModule(i),p=c.render||c.default;if(typeof p!="function")throw new Error(`Entry file ${f?l:i} must export a 'render' function.`);await p({req:t,reply:s,head:m,data:d,globalSettings:y})}catch(u){e.viteServer?.ssrFixStacktrace(u),console.error("[SSR Error]:",u),s.sent||s.status(500).send("Internal Server Error")}};e.decorate("addRpcRoute",function(t,s){let{handler:d,schema:h,...u}=s,y=`/rpc${t}`,m=F(h),S={...u,schema:m};s?.schema?.consumes?.includes("multipart/form-data")&&(S.preValidation=$),this.route({method:"POST",url:y,...S,handler:async(i,l)=>{try{let c=await d.call(this,i,l);return l.sent?void 0:c}catch(c){if(console.error(`[RPC Error] ${y}:`,c),!l.sent){let{statusCode:p,message:g}=n(c);l.status(p).send({error:{message:g}})}}}})}),e.decorate("addRenderRoute",function(t,s){let{handler:d,schema:h,entry:u,...y}=s||{},m=F(h),S=async(a,i,l)=>{try{let c=await d?.call(this,a,i);return l?c:o(a,i,c,u)}catch(c){if(console.error(`[Render Error] ${t}:`,c),!i.sent){let{statusCode:p,message:g}=n(c);if(l)i.status(p).send({error:{message:g}});else return o(a,i,{ssrError:{statusCode:500,message:"Internal Error"}})}}};if(this.route({method:"GET",url:t,schema:m,...y,handler:(a,i)=>S(a,i,!1)}),t!=="*"&&t!=="/*"){this.route({method:"GET",url:`/loader${t}`,schema:m,...y,handler:(l,c)=>S(l,c,!0)});let a=t.endsWith("/")?"*":"/*",i=`${t}${a}`;this.route({method:"GET",url:i,schema:m,...y,handler:(l,c)=>S(l,c,!1)})}}),e.decorate("addLoaderRoute",function(t,s){let{handler:d,schema:h,...u}=s,y=`/api${t}`,m=F(h);this.route({method:"GET",url:y,schema:m,...u,handler:async(S,a)=>{try{let i=await d.call(this,S,a);return a.sent?void 0:i}catch(i){if(console.error(`[Loader API Error] ${y}:`,i),!a.sent){let{statusCode:l,message:c}=n(i);a.status(l).send({error:{message:c}})}}}})})},H=(0,T.default)(M,{name:"fastify-b-ssr"});
@@ -13,6 +13,11 @@ type AddRpcRouteOptions<TRpcSchema extends FastifySchema> = BaseRouteOptions<Rou
13
13
  type AddRenderRouteOptions<TSchema extends FastifySchema, TRpcSchema extends FastifySchema> = BaseRouteOptions<RouteGenericForSchema<TSchema>> & {
14
14
  handler?: (this: FastifyInstance, req: FastifyRequest<RouteGenericForSchema<TSchema>>, reply: FastifyReply<RouteGenericForSchema<TSchema>>) => Promise<unknown> | unknown;
15
15
  prefix?: string;
16
+ /**
17
+ * Nombre del entry point a usar para esta ruta (y sus sub-rutas).
18
+ * Debe coincidir con `entries[].name` en las opciones del plugin.
19
+ */
20
+ entry?: string;
16
21
  };
17
22
  type AddLoaderRouteOptions<TSchema extends FastifySchema> = BaseRouteOptions<RouteGenericForSchema<TSchema>> & {
18
23
  handler: (this: FastifyInstance, req: FastifyRequest<RouteGenericForSchema<TSchema>>, reply: FastifyReply<RouteGenericForSchema<TSchema>>) => Promise<unknown> | unknown;
@@ -25,18 +30,39 @@ interface RouteGenericForSchema<TSchema extends FastifySchema> extends RouteGene
25
30
  Params: InferZod<TSchema['params']>;
26
31
  Headers: InferZod<TSchema['headers']>;
27
32
  }
33
+ type SsrEntryMatch = string | RegExp | ((req: FastifyRequest) => boolean);
34
+ interface SsrEntry {
35
+ /** Nombre opcional para debugging */
36
+ name?: string;
37
+ /**
38
+ * Regla para escoger el entry. Si no se provee, actúa como fallback
39
+ * cuando ningún match coincide.
40
+ */
41
+ match?: SsrEntryMatch;
42
+ /** Ruta al archivo de entrada SSR en desarrollo (ej: '/src/entry-ssr-server.tsx') */
43
+ devEntryFile: string;
44
+ /** Ruta relativa al archivo compilado del servidor SSR en producción (ej: './dist-react/server/entry-server.mjs') */
45
+ prodEntryFile: string;
46
+ /** Ruta relativa a la carpeta de salida del cliente en producción */
47
+ clientDistDir?: string;
48
+ }
49
+ type SsrEntryResolver = (req: FastifyRequest) => SsrEntry | null | undefined;
28
50
  interface FastifyReactSsrOptions {
29
51
  /** Raíz del proyecto (donde está vite.config.ts). Generalmente `process.cwd()` o `__dirname` */
30
52
  root: string;
31
53
  /** Ruta al archivo de entrada SSR en desarrollo (ej: '/src/entry-ssr-server.tsx') */
32
- devEntryFile: string;
54
+ devEntryFile?: string;
33
55
  /** Ruta relativa al archivo compilado del servidor SSR en producción (ej: './dist-react/server/entry-server.mjs') */
34
- prodEntryFile: string;
56
+ prodEntryFile?: string;
35
57
  /**
36
58
  * Ruta relativa a la carpeta de salida del cliente en producción (ej: './dist-react/client').
37
59
  * Requerido para servir assets (CSS, JS) en producción.
38
60
  */
39
61
  clientDistDir?: string;
62
+ /** Múltiples entry points para SSR */
63
+ entries?: SsrEntry[];
64
+ /** Resolver custom para elegir entry point en base al request */
65
+ resolveEntry?: SsrEntryResolver;
40
66
  /** Configuración extra para Vite (opcional) */
41
67
  viteConfig?: InlineConfig;
42
68
  /** Manejador de errores personalizado */
@@ -62,4 +88,4 @@ declare module 'fastify' {
62
88
  }
63
89
  declare const _default: FastifyPluginAsyncZod<FastifyReactSsrOptions>;
64
90
 
65
- export { type AddLoaderRouteOptions, type AddRenderRouteOptions, type AddRpcRouteOptions, type BaseRouteOptions, type FastifyReactSsrOptions, type RouteGenericForSchema, _default as default };
91
+ export { type AddLoaderRouteOptions, type AddRenderRouteOptions, type AddRpcRouteOptions, type BaseRouteOptions, type FastifyReactSsrOptions, type RouteGenericForSchema, type SsrEntry, type SsrEntryMatch, type SsrEntryResolver, _default as default };
@@ -13,6 +13,11 @@ type AddRpcRouteOptions<TRpcSchema extends FastifySchema> = BaseRouteOptions<Rou
13
13
  type AddRenderRouteOptions<TSchema extends FastifySchema, TRpcSchema extends FastifySchema> = BaseRouteOptions<RouteGenericForSchema<TSchema>> & {
14
14
  handler?: (this: FastifyInstance, req: FastifyRequest<RouteGenericForSchema<TSchema>>, reply: FastifyReply<RouteGenericForSchema<TSchema>>) => Promise<unknown> | unknown;
15
15
  prefix?: string;
16
+ /**
17
+ * Nombre del entry point a usar para esta ruta (y sus sub-rutas).
18
+ * Debe coincidir con `entries[].name` en las opciones del plugin.
19
+ */
20
+ entry?: string;
16
21
  };
17
22
  type AddLoaderRouteOptions<TSchema extends FastifySchema> = BaseRouteOptions<RouteGenericForSchema<TSchema>> & {
18
23
  handler: (this: FastifyInstance, req: FastifyRequest<RouteGenericForSchema<TSchema>>, reply: FastifyReply<RouteGenericForSchema<TSchema>>) => Promise<unknown> | unknown;
@@ -25,18 +30,39 @@ interface RouteGenericForSchema<TSchema extends FastifySchema> extends RouteGene
25
30
  Params: InferZod<TSchema['params']>;
26
31
  Headers: InferZod<TSchema['headers']>;
27
32
  }
33
+ type SsrEntryMatch = string | RegExp | ((req: FastifyRequest) => boolean);
34
+ interface SsrEntry {
35
+ /** Nombre opcional para debugging */
36
+ name?: string;
37
+ /**
38
+ * Regla para escoger el entry. Si no se provee, actúa como fallback
39
+ * cuando ningún match coincide.
40
+ */
41
+ match?: SsrEntryMatch;
42
+ /** Ruta al archivo de entrada SSR en desarrollo (ej: '/src/entry-ssr-server.tsx') */
43
+ devEntryFile: string;
44
+ /** Ruta relativa al archivo compilado del servidor SSR en producción (ej: './dist-react/server/entry-server.mjs') */
45
+ prodEntryFile: string;
46
+ /** Ruta relativa a la carpeta de salida del cliente en producción */
47
+ clientDistDir?: string;
48
+ }
49
+ type SsrEntryResolver = (req: FastifyRequest) => SsrEntry | null | undefined;
28
50
  interface FastifyReactSsrOptions {
29
51
  /** Raíz del proyecto (donde está vite.config.ts). Generalmente `process.cwd()` o `__dirname` */
30
52
  root: string;
31
53
  /** Ruta al archivo de entrada SSR en desarrollo (ej: '/src/entry-ssr-server.tsx') */
32
- devEntryFile: string;
54
+ devEntryFile?: string;
33
55
  /** Ruta relativa al archivo compilado del servidor SSR en producción (ej: './dist-react/server/entry-server.mjs') */
34
- prodEntryFile: string;
56
+ prodEntryFile?: string;
35
57
  /**
36
58
  * Ruta relativa a la carpeta de salida del cliente en producción (ej: './dist-react/client').
37
59
  * Requerido para servir assets (CSS, JS) en producción.
38
60
  */
39
61
  clientDistDir?: string;
62
+ /** Múltiples entry points para SSR */
63
+ entries?: SsrEntry[];
64
+ /** Resolver custom para elegir entry point en base al request */
65
+ resolveEntry?: SsrEntryResolver;
40
66
  /** Configuración extra para Vite (opcional) */
41
67
  viteConfig?: InlineConfig;
42
68
  /** Manejador de errores personalizado */
@@ -62,4 +88,4 @@ declare module 'fastify' {
62
88
  }
63
89
  declare const _default: FastifyPluginAsyncZod<FastifyReactSsrOptions>;
64
90
 
65
- export { type AddLoaderRouteOptions, type AddRenderRouteOptions, type AddRpcRouteOptions, type BaseRouteOptions, type FastifyReactSsrOptions, type RouteGenericForSchema, _default as default };
91
+ export { type AddLoaderRouteOptions, type AddRenderRouteOptions, type AddRpcRouteOptions, type BaseRouteOptions, type FastifyReactSsrOptions, type RouteGenericForSchema, type SsrEntry, type SsrEntryMatch, type SsrEntryResolver, _default as default };
@@ -1 +1 @@
1
- import"@fastify/multipart";import T from"fastify-plugin";import R from"path";async function F(t=24678){let a=await import("net");return new Promise((f,h)=>{let u=a.createServer();u.listen(t,"127.0.0.1",()=>{let e=u.address(),r=typeof e=="object"&&e!==null?e.port:t;u.close(()=>f(r))}),u.on("error",e=>{e.code==="EADDRINUSE"?u.close(()=>f(F(t+1))):h(e)})})}function v(t){if(t)return t}var w=async(t,a)=>{if(t.body&&typeof t.body=="object"){let f=t.body,h={};for(let u of Object.keys(f)){let e=f[u];e&&typeof e=="object"?e.type==="field"?h[u]=e.value:e.type==="file"&&(h[u]={filename:e.filename,mimetype:e.mimetype,encoding:e.encoding,fieldname:e.fieldname,file:e.file}):h[u]=e}t.body=h}},E=async(t,a)=>{let f=e=>{if(a.errorHandler){let r=a.errorHandler(e);if(r)return r}return{message:"Internal Server Error",statusCode:500}};if(t.hasDecorator("viteInitDone")&&t.viteInitDone)return;if(!t.hasDecorator("multipartErrors"))try{let e=await import("@fastify/multipart");await t.register(e.default)}catch(e){if(e.code!=="FST_ERR_DEC_ALREADY_PRESENT")throw e}let h=process.env.NODE_ENV==="production";if(h){if(a.clientDistDir)try{let e=await import("@fastify/static"),r=R.resolve(a.root,a.clientDistDir);await t.register(e.default,{root:r,wildcard:!1}),console.log(`\u{1F4C2} [Fastify-SSR] Serving static assets from: ${r}`)}catch(e){console.error("\u274C [Fastify-SSR] Error registering @fastify/static. Did you install it?",e)}}else{if(!t.hasDecorator("use"))try{let s=await import("@fastify/middie");await t.register(s.default,{hook:"onRequest"})}catch(s){if(s.code!=="FST_ERR_DEC_ALREADY_PRESENT")throw s}let e=await import("vite"),r=a.viteConfig||{},l;r.server?.hmr&&typeof r.server.hmr=="object"&&"port"in r.server.hmr?l=r.server.hmr.port:l=a.hmrPort??await F();let c=await e.createServer({root:a.root,appType:"custom",...r,server:{hmr:{port:l},...r.server,middlewareMode:!0}});t.use(c.middlewares),t.hasDecorator("viteServer")||t.decorate("viteServer",c),console.log("\u{1F680} [Fastify-SSR] Vite Dev Server Ready")}t.decorate("viteInitDone",!0);let u=async(e,r,l)=>{try{let c=e.raw.url,s=a.getGlobalSettings?await a.getGlobalSettings(e):void 0;if(R.extname(c)!==""){r.status(404).send(`File not found: ${c}`);return}let n="";!h&&t.viteServer&&(n=await t.viteServer.transformIndexHtml(c,"<html><head></head><body></body></html>"),n=n.substring(n.indexOf("<head>")+6,n.indexOf("</head>"))),n=(s?`<script>window.__GLOBAL_SETTINGS__ = ${JSON.stringify(s)}</script>`:"")+n;let d=h?await import(R.resolve(a.root,a.prodEntryFile)):await t.viteServer.ssrLoadModule(a.devEntryFile),o=d.render||d.default;if(typeof o!="function")throw new Error(`Entry file ${a.devEntryFile} must export a 'render' function.`);await o({req:e,reply:r,head:n,data:l,globalSettings:s})}catch(c){t.viteServer?.ssrFixStacktrace(c),console.error("[SSR Error]:",c),r.sent||r.status(500).send("Internal Server Error")}};t.decorate("addRpcRoute",function(e,r){let{handler:l,schema:c,...s}=r,n=`/rpc${e}`,p=v(c),d={...s,schema:p};r?.schema?.consumes?.includes("multipart/form-data")&&(d.preValidation=w),this.route({method:"POST",url:n,...d,handler:async(m,i)=>{try{let y=await l.call(this,m,i);return i.sent?void 0:y}catch(y){if(console.error(`[RPC Error] ${n}:`,y),!i.sent){let{statusCode:S,message:g}=f(y);i.status(S).send({error:{message:g}})}}}})}),t.decorate("addRenderRoute",function(e,r){let{handler:l,schema:c,...s}=r||{},n=v(c),p=async(d,o,m)=>{try{let i=await l?.call(this,d,o);return m?i:u(d,o,i)}catch(i){if(console.error(`[Render Error] ${e}:`,i),!o.sent){let{statusCode:y,message:S}=f(i);if(m)o.status(y).send({error:{message:S}});else return u(d,o,{ssrError:{statusCode:500,message:"Internal Error"}})}}};if(this.route({method:"GET",url:e,schema:n,...s,handler:(d,o)=>p(d,o,!1)}),e!=="*"&&e!=="/*"){this.route({method:"GET",url:`/loader${e}`,schema:n,...s,handler:(m,i)=>p(m,i,!0)});let d=e.endsWith("/")?"*":"/*",o=`${e}${d}`;this.route({method:"GET",url:o,schema:n,...s,handler:(m,i)=>p(m,i,!1)})}}),t.decorate("addLoaderRoute",function(e,r){let{handler:l,schema:c,...s}=r,n=`/api${e}`,p=v(c);this.route({method:"GET",url:n,schema:p,...s,handler:async(d,o)=>{try{let m=await l.call(this,d,o);return o.sent?void 0:m}catch(m){if(console.error(`[Loader API Error] ${n}:`,m),!o.sent){let{statusCode:i,message:y}=f(m);o.status(i).send({error:{message:y}})}}}})})},b=T(E,{name:"fastify-b-ssr"});export{b as default};
1
+ import"@fastify/multipart";import w from"fastify-plugin";import v from"path";async function g(t=24678){let r=await import("net");return new Promise((s,h)=>{let d=r.createServer();d.listen(t,"127.0.0.1",()=>{let e=d.address(),n=typeof e=="object"&&e!==null?e.port:t;d.close(()=>s(n))}),d.on("error",e=>{e.code==="EADDRINUSE"?d.close(()=>s(g(t+1))):h(e)})})}function E(t){if(t)return t}function F(t){let r=t.raw?.url||t.url||"",s=r.indexOf("?");return s===-1?r:r.slice(0,s)}function T(t,r,s){return t.match?typeof t.match=="string"?s.startsWith(t.match):t.match instanceof RegExp?t.match.test(s):t.match(r):!1}function D(t,r){if(r.resolveEntry){let e=r.resolveEntry(t);if(e)return e}let s=r.entries||[];if(s.length===0)return;let h=F(t),d;for(let e of s){if(!e.match){d||(d=e);continue}if(T(e,t,h))return e}return d}function x(t,r){if(!(!r.entries||r.entries.length===0))return r.entries.find(s=>s.name===t)}var b=async(t,r)=>{if(t.body&&typeof t.body=="object"){let s=t.body,h={};for(let d of Object.keys(s)){let e=s[d];e&&typeof e=="object"?e.type==="field"?h[d]=e.value:e.type==="file"&&(h[d]={filename:e.filename,mimetype:e.mimetype,encoding:e.encoding,fieldname:e.fieldname,file:e.file}):h[d]=e}t.body=h}},O=async(t,r)=>{let s=e=>{if(r.errorHandler){let n=r.errorHandler(e);if(n)return n}return{message:"Internal Server Error",statusCode:500}};if(t.hasDecorator("viteInitDone")&&t.viteInitDone)return;if(!t.hasDecorator("multipartErrors"))try{let e=await import("@fastify/multipart");await t.register(e.default)}catch(e){if(e.code!=="FST_ERR_DEC_ALREADY_PRESENT")throw e}let h=process.env.NODE_ENV==="production";if(h){let e=new Set;r.clientDistDir&&e.add(r.clientDistDir);for(let c of r.entries||[])c.clientDistDir&&e.add(c.clientDistDir);e.size>1&&console.warn("\u26A0\uFE0F [Fastify-SSR] Multiple clientDistDir detected. Only the first will be served. Register additional static dirs manually if needed.");let[n]=e;if(n)try{let c=await import("@fastify/static"),m=v.resolve(r.root,n);await t.register(c.default,{root:m,wildcard:!1}),console.log(`\u{1F4C2} [Fastify-SSR] Serving static assets from: ${m}`)}catch(c){console.error("\u274C [Fastify-SSR] Error registering @fastify/static. Did you install it?",c)}}else{if(!t.hasDecorator("use"))try{let u=await import("@fastify/middie");await t.register(u.default,{hook:"onRequest"})}catch(u){if(u.code!=="FST_ERR_DEC_ALREADY_PRESENT")throw u}let e=await import("vite"),n=r.viteConfig||{},c;n.server?.hmr&&typeof n.server.hmr=="object"&&"port"in n.server.hmr?c=n.server.hmr.port:c=r.hmrPort??await g();let m=await e.createServer({root:r.root,appType:"custom",...n,server:{hmr:{port:c},...n.server,middlewareMode:!0}});t.use(m.middlewares),t.hasDecorator("viteServer")||t.decorate("viteServer",m),console.log("\u{1F680} [Fastify-SSR] Vite Dev Server Ready")}t.decorate("viteInitDone",!0);let d=async(e,n,c,m)=>{try{let u=e.raw.url,y=r.getGlobalSettings?await r.getGlobalSettings(e):void 0;if(v.extname(u)!==""){n.status(404).send(`File not found: ${u}`);return}let f="";!h&&t.viteServer&&(f=await t.viteServer.transformIndexHtml(u,"<html><head></head><body></body></html>"),f=f.substring(f.indexOf("<head>")+6,f.indexOf("</head>"))),f=(y?`<script>window.__GLOBAL_SETTINGS__ = ${JSON.stringify(y)}</script>`:"")+f;let i;if(m){if(i=x(m,r),!i)throw new Error(`Entry "${m}" not found. Make sure entries[] includes a matching name.`)}else i=D(e,r);let o=i?.devEntryFile??r.devEntryFile,l=i?.prodEntryFile??r.prodEntryFile;if(!o||!l)throw new Error(`No SSR entry resolved for request "${F(e)}". Provide devEntryFile/prodEntryFile or configure entries/resolveEntry.`);let a=h?await import(v.resolve(r.root,l)):await t.viteServer.ssrLoadModule(o),R=a.render||a.default;if(typeof R!="function")throw new Error(`Entry file ${h?l:o} must export a 'render' function.`);await R({req:e,reply:n,head:f,data:c,globalSettings:y})}catch(u){t.viteServer?.ssrFixStacktrace(u),console.error("[SSR Error]:",u),n.sent||n.status(500).send("Internal Server Error")}};t.decorate("addRpcRoute",function(e,n){let{handler:c,schema:m,...u}=n,y=`/rpc${e}`,f=E(m),S={...u,schema:f};n?.schema?.consumes?.includes("multipart/form-data")&&(S.preValidation=b),this.route({method:"POST",url:y,...S,handler:async(o,l)=>{try{let a=await c.call(this,o,l);return l.sent?void 0:a}catch(a){if(console.error(`[RPC Error] ${y}:`,a),!l.sent){let{statusCode:R,message:p}=s(a);l.status(R).send({error:{message:p}})}}}})}),t.decorate("addRenderRoute",function(e,n){let{handler:c,schema:m,entry:u,...y}=n||{},f=E(m),S=async(i,o,l)=>{try{let a=await c?.call(this,i,o);return l?a:d(i,o,a,u)}catch(a){if(console.error(`[Render Error] ${e}:`,a),!o.sent){let{statusCode:R,message:p}=s(a);if(l)o.status(R).send({error:{message:p}});else return d(i,o,{ssrError:{statusCode:500,message:"Internal Error"}})}}};if(this.route({method:"GET",url:e,schema:f,...y,handler:(i,o)=>S(i,o,!1)}),e!=="*"&&e!=="/*"){this.route({method:"GET",url:`/loader${e}`,schema:f,...y,handler:(l,a)=>S(l,a,!0)});let i=e.endsWith("/")?"*":"/*",o=`${e}${i}`;this.route({method:"GET",url:o,schema:f,...y,handler:(l,a)=>S(l,a,!1)})}}),t.decorate("addLoaderRoute",function(e,n){let{handler:c,schema:m,...u}=n,y=`/api${e}`,f=E(m);this.route({method:"GET",url:y,schema:f,...u,handler:async(S,i)=>{try{let o=await c.call(this,S,i);return i.sent?void 0:o}catch(o){if(console.error(`[Loader API Error] ${y}:`,o),!i.sent){let{statusCode:l,message:a}=s(o);i.status(l).send({error:{message:a}})}}}})})},k=w(O,{name:"fastify-b-ssr"});export{k as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobtail.software/b-ssr",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Fastify + Vite SSR Plugin wrapper with RPC",
5
5
  "author": "Victor Moreno <info@bobtail.software> (https://bobtail.software)",
6
6
  "license": "GPL-3.0",