API: wu-framework/server
Todo lo de esta página se exporta desde wu-framework/server. El punto de entrada es Node puro: no toca el DOM, no usa vm y no necesita flags experimentales.
import { renderApp, renderApps, renderStateScript, declarativeShadowDomPolyfill, escapeHtml, serializeState, SSR_MARKER_ATTR, SSR_ROOT_ATTR, renderReact, renderPreact, renderVue, renderSvelte, renderSolid, renderHtml, createNonce, securityHeaders, buildCsp, applySecurityHeaders, wuSecurityMiddleware, withSecurityHeaders,} from 'wu-framework/server';renderApp()
Sección titulada «renderApp()»function renderApp(appName: string, options: RenderAppOptions): Promise<RenderAppResult>;| Opción | Tipo | Por defecto | Qué hace |
|---|---|---|---|
module |
module | () => import() | Promise |
obligatorio | El módulo de servidor de la app (o un namespace con default) |
props |
object |
{} |
Props con las que renderizar; se publican para la hidratación |
styles |
string | string[] |
[] |
CSS adicional a encapsular, además de lo que devuelva render |
containerId |
string |
wu-app-<name> |
id del elemento contenedor |
className |
string |
'' |
Clases del contenedor |
shadowMode |
'open' | 'closed' |
'open' |
closed bloquea la hidratación |
declarative |
boolean |
true |
Con false emite marcado plano sin shadow root |
nonce |
string |
— | Nonce CSP base64/base64url para cada <style> en línea emitido |
Devuelve:
interface RenderAppResult { html: string; // el <div …>…</div> completo inner: string; // solo el <template> (o el marcado plano) containerAttrs: Record<string, string>; // { id, 'data-wu-app', 'data-wu-ssr', class? } state: unknown; // lo que publicó el render (las props, en los helpers integrados) appName: string; containerId: string;}inner y containerAttrs existen para integraciones que deben construir el contenedor con su propio motor — un React Server Component, un componente .astro — en vez de inyectar una cadena.
Validación. appName debe coincidir con /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,49}$/ (la misma regla que el manifiesto). shadowMode debe ser 'open' o 'closed'. El nonce, si existe, debe ser base64/base64url; un valor con comillas lanza antes de producir marcado. Si falta module, lanza un error con un mensaje que te dice qué pasar.
Fallos. Si el render de la app lanza, renderApp relanza:
try { await renderApp('cart', { module });} catch (err) { err.code; // 'WU_SSR_RENDER_FAILED' err.appName; // 'cart' err.cause; // el error original}renderApps()
Sección titulada «renderApps()»function renderApps( apps: Array<{ name: string } & RenderAppOptions>, options?: { failFast?: boolean; nonce?: string }): Promise<{ html: string; byApp: Record<string, string>; state: Record<string, unknown>; errors: Array<{ app: string; error: Error }>;}>;Renderiza en paralelo. html es la concatenación de todos los contenedores en orden; byApp te deja colocarlos por separado en una plantilla.
| Opción | Por defecto | Comportamiento |
|---|---|---|
failFast |
false |
Una app que lanza deja un contenedor vacío (sin data-wu-ssr) en byApp y una entrada en errors; la página se sigue renderizando. |
failFast: true |
— | El primer error se propaga. |
nonce |
— | Se reenvía a cada app para que todos los estilos usen el nonce de esta respuesta. |
Solo aparecen en state las apps cuyo render produjo un state !== undefined.
renderStateScript()
Sección titulada «renderStateScript()»function renderStateScript( state: Record<string, unknown>, options?: { varName?: string; merge?: boolean; nonce?: string }): string;| Opción | Por defecto | Comportamiento |
|---|---|---|
varName |
'__WU_SSR_STATE__' |
Variable global a asignar. Debe ser un identificador válido o lanza. |
merge |
false |
Con true emite Object.assign(window.X || {}, …), para componentes que emiten cada uno su propio script |
nonce |
— | Añade el mismo nonce CSP de la respuesta al <script> de estado. |
La serialización escapa <, >, U+2028 y U+2029, así que ningún valor puede escaparse del <script>.
declarativeShadowDomPolyfill()
Sección titulada «declarativeShadowDomPolyfill()»function declarativeShadowDomPolyfill(options?: { nonce?: string }): string;Devuelve una cadena <script>. Comprueba 'shadowRootMode' in HTMLTemplateElement.prototype y retorna de inmediato donde el soporte es nativo; en caso contrario recorre los template[shadowrootmode], adjunta el shadow root, mueve el contenido dentro y elimina el template. Colócalo al final del <body>:
res.end(template.replace('<!--polyfill-->', declarativeShadowDomPolyfill({ nonce })));Es opcional: mount() hace la misma materialización. El polyfill solo importa para que el contenido sea visible antes de que cargue el runtime de wu.
Cabeceras de seguridad y CSP
Sección titulada «Cabeceras de seguridad y CSP»Para SSR, genera un nonce nuevo por respuesta y usa el mismo valor en la cabecera y en todo el marcado en línea:
const nonce = createNonce();const { html, state } = await renderApps(apps, { nonce });applySecurityHeaders(res, { nonce });res.end(template .replace('<!--apps-->', html) .replace('<!--state-->', renderStateScript(state, { nonce })) .replace('<!--polyfill-->', declarativeShadowDomPolyfill({ nonce })));securityHeaders(options)devuelve el objeto completo de cabeceras.buildCsp(options)devuelve únicamente la CSP.applySecurityHeaders(res, options)trabaja con una respuesta Node.wuSecurityMiddleware(options)integra Express/Connect y deja el nonce enreq.wuNonceyres.locals.nonce.withSecurityHeaders(handler, options)envuelve handlers basados enRequest/Responsecomo Hono, Workers, Deno, Bun o rutas de Next.
No reutilices nonces entre respuestas y no caches HTML que contenga uno. Las
props se serializan escapando cierres de script, <, >, U+2028 y
U+2029; las pruebas generativas cubren estados y atributos hostiles.
Utilidades
Sección titulada «Utilidades»function escapeHtml(value: unknown): string; // & < > " ' → entidadesfunction serializeState(value: unknown): string; // JSON seguro para <script>const SSR_MARKER_ATTR: 'data-wu-ssr';const SSR_ROOT_ATTR: 'data-wu-root';data-wu-ssr lleva 'open', 'closed' o 'flat'. data-wu-root marca el elemento en el que la app se monta o se hidrata.
Helpers de framework
Sección titulada «Helpers de framework»function renderReact(Component: any, options?: RenderHelperOptions & { strictMode?: boolean }): WuServerModule;function renderPreact(Component: any, options?: RenderHelperOptions): WuServerModule;function renderVue(Component: any, options?: RenderHelperOptions & { setup?: (app: any, props: object) => void }): WuServerModule;function renderSvelte(Component: any, options?: RenderHelperOptions): WuServerModule;function renderSolid(Component: any, options?: RenderHelperOptions): WuServerModule;function renderHtml(fn: (props: object) => string | Promise<string>, options?: RenderHelperOptions): WuServerModule;
interface RenderHelperOptions { styles?: string | string[] | ((props: Record<string, any>) => string | string[]);}Inyectar el renderizador
Sección titulada «Inyectar el renderizador»Por defecto cada helper carga su framework con un import() dinámico. Eso está bien en un servidor Node normal, pero se resuelve en runtime, fuera del bundle. Si el proceso que renderiza ya tiene su propia copia del framework cargada, acabas con dos instancias distintas — y en React eso significa:
Cannot read properties of null (reading 'useState')Para esos casos puedes pasarle el renderizador ya resuelto, importado de forma estática, y el helper no busca ninguno:
import React from 'react';import { renderToString } from 'react-dom/server';
export default renderReact(Promo, { styles: css, react: React, renderToString });| Helper | Opciones inyectables |
|---|---|
renderReact |
react, renderToString |
renderPreact |
h, renderToString |
renderVue |
createSSRApp, renderToString |
Es la misma idea que createWuSlot(React) en el lado del cliente: quien monta decide qué instancia se usa, en vez de dejarlo a la resolución de módulos.
Hace falta sobre todo al empaquetar el módulo de servidor con un bundler — el ejemplo de Next.js lo usa por esto exactamente.
Todos producen { html, styles, state }, donde state son las props con las que se les llamó: eso es lo que acaba en renderStateScript y, al final, en el slot hydrate.
| Helper | Paquetes peer | Notas |
|---|---|---|
renderReact |
react, react-dom |
strictMode: true envuelve en React.StrictMode |
renderPreact |
preact, preact-render-to-string |
|
renderVue |
vue, @vue/server-renderer |
setup(app, props) se ejecuta antes de renderToString |
renderSvelte |
build de servidor de svelte |
Component.render() de Svelte 4 o svelte/server de Svelte 5; el css.code con scope de Svelte 4 se añade automáticamente a styles |
renderSolid |
solid-js |
|
renderHtml |
ninguno | Escapa tú mismo tus interpolaciones |
Si faltan paquetes peer, se lanza un mensaje que nombra exactamente qué instalar.
El contrato del módulo de servidor
Sección titulada «El contrato del módulo de servidor»interface WuServerModule { render(props: Record<string, any>): | string | { html: string; styles?: string | string[]; state?: unknown } | Promise<string | { html: string; styles?: string | string[]; state?: unknown }>;}Devolver null/undefined renderiza vacío. Devolver algo que no sea cadena ni objeto lanza. Cualquier cosa distinta de una cadena o de un objeto con la forma anterior es un error de programación, y wu lo dice con el nombre de la app en el mensaje.
