Ir al contenido

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';
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
}
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.

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>.

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.

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 en req.wuNonce y res.locals.nonce.
  • withSecurityHeaders(handler, options) envuelve handlers basados en Request/Response como 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.

function escapeHtml(value: unknown): string; // & < > " ' → entidades
function 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.

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[]);
}

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.

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.