WU / DOCSUNIVERSAL MICROFRONTENDS
ES

Recetas de servidor y SSR

Renderiza HTML, React y Vue con WU en Node; comprueba escape, estado, CSP, errores y respuestas HTTP reales.

De un renderer a una respuesta HTTP

El servidor de WU recibe un módulo con render(props) y devuelve el marcado de una micro-app. El host HTTP conserva la responsabilidad de cargar datos, elegir módulos confiables y construir la respuesta. No necesitas inicializar el runtime del navegador para usar wu-framework/server.

El proyecto incluye una receta completa en examples/ssr/render-page.mjs. Compone tres renderers reales —HTML, React y Vue— y genera un documento con Declarative Shadow DOM, estado de hidratación y cabeceras CSP.

Puedes descargar las recetas ejecutables, extraer el ZIP y ejecutar npm install. Incluye el código, las pruebas y un manifest independiente para reproducirlas fuera del portal.

node examples/ssr/render-page.mjs Ada
node examples/ssr/server.mjs

La primera orden escribe HTML en stdout. La segunda abre http://127.0.0.1:4310/?name=Ada. Los comandos se ejecutan desde la raíz del proyecto con sus dependencias instaladas. El servidor acepta la variable PORT y se cierra con Ctrl+C.

Un renderer HTML pequeño

import { escapeHtml, renderApp } from 'wu-framework/server';

const greeting = {
  render({ name }) {
    return {
      html: `<p>Hola, ${escapeHtml(name)}.</p>`,
      styles: 'p { color: #b7d900; }',
      state: { name },
    };
  },
};

const result = await renderApp('greeting', {
  module: greeting,
  props: { name: 'Ada' },
});

El contenedor completo está en result.html. Su forma es:

<div id="wu-app-greeting" data-wu-app="greeting" data-wu-ssr="open">
  <template shadowrootmode="open">
    <style>p { color: #b7d900; }</style>
    <div data-wu-root><p>Hola, Ada.</p></div>
  </template>
</div>

result.inner contiene el interior del contenedor; result.containerAttrs permite que Astro u otro motor construyan el elemento exterior. state sólo contiene lo devuelto explícitamente por el renderer. Los renderers HTML personalizados deciden qué datos publicar; los helpers React y Vue devuelven las props como estado.

React y Vue sin compilar JSX ni SFC

Esta receta usa React.createElement y h para que Node pueda ejecutarla directamente. En un proyecto de componentes puedes importar el resultado del build de servidor equivalente.

import * as React from 'react';
import { renderToString as reactToString } from 'react-dom/server';
import { createSSRApp, h } from 'vue';
import { renderToString as vueToString } from '@vue/server-renderer';
import { renderReact, renderVue, renderApps } from 'wu-framework/server';

const reactApp = renderReact(
  ({ name }) => React.createElement('p', null, `Hola, ${name}`),
  { react: React, renderToString: reactToString },
);
const vueApp = renderVue(
  { props: ['name'], setup: props => () => h('p', `Hola, ${props.name}`) },
  { createSSRApp, renderToString: vueToString },
);

const result = await renderApps([
  { name: 'react-greeting', module: reactApp, props: { name: 'Ada' } },
  { name: 'vue-greeting', module: vueApp, props: { name: 'Ada' } },
]);

La inyección del renderer mantiene la misma instancia de React o Vue que utiliza el host. Los imports públicos de los helpers son wu-framework/server y wu-framework/server/adapters; no existen subpaths wu-framework/server/react o wu-framework/server/vue en el mapa de exports del paquete.

Estado y CSP por respuesta

import { createNonce, renderApps, renderStateScript, securityHeaders } from 'wu-framework/server';

const nonce = createNonce();
const rendered = await renderApps(apps, { nonce, failFast: true });
const headers = securityHeaders({ nonce });
const body = rendered.html + renderStateScript(rendered.state, { nonce });

Usa el mismo nonce en estilos, script de estado y cabecera CSP de esa respuesta; genera uno nuevo para la siguiente. renderStateScript publica por defecto window.__WU_SSR_STATE__. Su opción merge: true conserva entradas de otras apps cuando cada integración emite su propio script.

La receta HTTP local usa securityHeaders({ nonce, https: false, hsts: false }) porque escucha por HTTP en loopback. Para un servidor HTTPS configura la política de transporte correspondiente. El helper no inicia TLS ni administra certificados.

No publiques secretos en el estado: el HTML de la respuesta y las props de hidratación son visibles al cliente.

Escapar no es sanitizar un documento

escapeHtml convierte los caracteres especiales de un valor de texto. serializeState conserva JSON válido y neutraliza <, >, U+2028 y U+2029 para impedir que un dato cierre el script de estado. La prueba introduce </script><img ...> como nombre y comprueba que no aparece una etiqueta ejecutable.

El HTML devuelto por render() se inserta tal cual. Si recibes HTML externo, sanitízalo antes de entregarlo al renderer con una política adecuada al producto; renderApp no elimina etiquetas ni atributos peligrosos de ese HTML. El CSS del renderer también pertenece a código confiable.

El estado debe ser serializable como JSON. Referencias circulares y BigInt fallan en serializeState; funciones y propiedades undefined no constituyen estado de hidratación portable.

Fallos parciales y failFast

import { renderApps } from 'wu-framework/server';

const result = await renderApps([
  { name: 'healthy', module: { render: () => '<p>Disponible</p>' } },
  { name: 'optional', module: { render() { throw new Error('Sin datos'); } } },
]);

console.log(result.errors[0].app); // optional
console.log(result.byApp.optional);
// <div id="wu-app-optional" data-wu-app="optional"></div>

renderApps conserva el orden de las entradas aunque renderice en paralelo. Sin failFast, devuelve HTML de las apps sanas, contenedores vacíos para los fallos y una lista errors. El host decide si responde 200 con una degradación visible o cambia el estado HTTP.

Con failFast: true, la promesa rechaza. Eso no cancela automáticamente los otros renderers que ya empezaron; evita trabajo con efectos externos dentro de render.

Fallo Resultado
Nombre inválido Rechazo antes de renderizar; máximo 50 caracteres, inicio alfanumérico y resto alfanumérico, _ o -
Falta module Error que solicita el renderer
Módulo sin render Error de resolución
render lanza Error con code: WU_SSR_RENDER_FAILED, appName y cause
shadowMode distinto de open/closed Error de opciones
Nonce o varName inválidos Error antes de emitir el atributo o script correspondiente
Resultado no admitido de render Error de normalización; no tiene necesariamente el código de fallo del renderer

Qué falta para hidratar

La receta produce HTML real sin descargar JavaScript de las apps. Para hacerlo interactivo, registra las mismas apps en el cliente y móntalas en sus contenedores SSR. Un lifecycle con hydrate adopta el DOM existente; sin él, WU usa mount. Los adapters de React y Vue deben recibir el mismo árbol y las mismas props del servidor.

shadowMode: 'closed' sirve para marcado inerte: la hidratación normal no puede acceder a esa raíz. declarative: false produce marcado plano con data-wu-ssr="flat", útil cuando no necesitas encapsulación declarativa.

Consulta SSR e hidratación y el lifecycle para conectar ambos lados. Un módulo WASM/WASI puede aportar lógica portable al renderer a través de un runner del host; por sí solo no sustituye el servidor HTTP, la resolución de datos ni la política de respuesta. La guía Un servicio conectado con WU recorre solicitudes, contratos y cancelación entre aplicaciones desde el navegador.

Ejecutar las comprobaciones

node scripts/verify-server-examples.mjs

La suite comprueba los tres renderers, correspondencia entre nonce y CSP, estado, escape, opciones inválidas, código/contexto de fallos, degradación parcial y salida plana. Las pruebas usan los exports del paquete instalado, no copias de sus funciones.

WU SE CONSTRUYE CONTIGO

¿Encontraste un bug? ¿Tienes una idea?

Tu experiencia mejora WU. Cuéntanos qué falla o ayuda a sostener su desarrollo.