Ir al contenido

Aislamiento de CSS

Cada micro-app se renderiza dentro de un shadow root. Esa parte no es configurable: el CSS escrito por una app no puede seleccionar dentro de otra, y los selectores de la página anfitriona no pueden alcanzar su interior.

Lo que es configurable es cuáles de los estilos del documento anfitrión se copian dentro del shadow root para que la app pueda usarlos. Eso es styleMode.

En el wu.json de la app. En ningún otro sitio.

public/wu.json
{
"name": "cart",
"entry": "src/main.jsx",
"styleMode": "fully-isolated"
}

La razón: cómo se relaciona una app con el CSS del anfitrión es una propiedad de la app, no de la página que la aloja. Una app que trae un sistema de diseño completo debería declararlo una vez, no en cada shell que la incrusta.

Un valor inválido no es fatal: Wu avisa y recurre a shared.

Valor Alias Inyecta en el shadow root
shared Todos los <style> y <link rel=stylesheet> del documento anfitrión
isolated none Nada
fully-isolated own-only Solo los estilos que emitió la propia app

Los alias se añadieron en la v2.0 porque “isolated” y “fully-isolated” describen un nivel de aislamiento cuando en realidad ambos aíslan igual: la diferencia está en qué se inyecta. none y own-only lo dicen directamente. Ambas grafías funcionan.

<head> anfitrión shadow root
┌──────────────┐ ┌────────────────────┐
│ tailwind.css │──────►│ tailwind.css │
│ tokens.css │──────►│ tokens.css │
│ app-x.css │──────►│ app-x.css │
└──────────────┘ │ <div wu-app-root> │
│ └────────────────────┘
│ MutationObserver sobre <head>
└─► aparece una hoja nueva → se reinyecta (HMR de Vite)

Cada hoja de estilos del documento anfitrión se clona o se adopta dentro del shadow root. Un MutationObserver vigila <head> para que las hojas añadidas más tarde — que es exactamente lo que hace el HMR de Vite — también se inyecten.

Úsalo cuando las apps comparten un sistema de diseño. El shell carga Tailwind o tus tokens una vez y todas las micro-apps ven las mismas variables.

Coste: cada app lleva una copia del CSS completo del anfitrión. Con muchas apps y una hoja global grande esto es memoria real. También es el modo con más papeletas para dar sorpresas, porque una regla del anfitrión que habías olvidado ahora aplica dentro de una app que no escribiste tú.

Encapsulación pura de Shadow DOM. No se inyecta nada, no se monta ningún observador y Wu no vuelve a tocar el estilado de la app. La app es enteramente responsable de su propio CSS: estilos en línea, CSS-in-JS, módulos CSS incluidos en el JS, o etiquetas <style> que ella misma añade dentro de su shadow root al montarse.

Úsalo cuando la app es genuinamente autocontenida. Es el modo más barato y el más predecible.

El shadow root recibe solo los estilos que emitió la propia app, ninguno del anfitrión.

Es el modo más delicado, porque “los estilos propios de la app” hay que inferirlos a partir de etiquetas <style> que ya han aterrizado en el <head> del anfitrión. Wu las detecta de dos formas:

  1. data-wu-app="<name>" — la convención explícita, agnóstica al bundler.
  2. data-vite-dev-id que coincida con packages/<appName>/src/ — servidor de desarrollo de Vite en un monorepo organizado así.

No hay coincidencia genérica por URL. Si tu app no es una app de Vite organizada como packages/<name>/src/, tienes que etiquetar tus estilos tú mismo:

import { wu } from 'wu-framework';
const style = document.createElement('style');
style.textContent = '/* estilos del carrito */';
wu.tagStyleAsApp(style, 'cart'); // pone data-wu-app="cart"
document.head.appendChild(style);

tagStyleAsApp(el, appName) funciona igual con elementos <style> y <link>. Llámalo con webpack, esbuild, Rollup, Parcel o etiquetas escritas a mano.

El respaldo es laxo. Si no se encuentra ningún estilo propio en unos 3 segundos, Wu registra un aviso e inyecta todos los estilos cuyo data-vite-dev-id simplemente contenga el nombre de la app — una coincidencia por subcadena que puede arrastrar el CSS de una app hermana. Ver en la consola No own styles found for <app> after timeout, using FALLBACK significa que la detección falló y que deberías añadir tagStyleAsApp.

Un MutationObserver persistente sigue vigilando <head>, así que los estilos emitidos más tarde (chunks perezosos, HMR) se recogen. Se desconecta al desmontar.

Úsalo cuando la app necesita su propio CSS pero no debe heredar el del anfitrión — un widget con tema de Bootstrap dentro de un shell con Tailwind, por ejemplo.

shared isolated / none fully-isolated / own-only
El CSS del anfitrión llega a la app ✅ todo
El CSS propio de la app llega a la app cosa de la app ✅ autodetectado
Vigila <head> por estilos nuevos ✅ filtrado
Necesita una convención de etiquetado data-wu-app o estructura Vite
Memoria por app copia completa del CSS anfitrión ninguna solo el CSS de la app
Por defecto
  • Las propiedades personalizadas de CSS se heredan a través de la frontera del shadow. Un --brand definido en :root es visible dentro de todas las apps, en todos los modos. Esto es una ventaja: es la forma más limpia de tematizar apps que por lo demás están totalmente aisladas.
  • Un @font-face declarado dentro de un shadow root lo ignoran casi todos los navegadores. Declara las fuentes en el documento anfitrión; los archivos de fuente en sí se resuelven sin problema.
  • Las propiedades heredables siguen heredándose. font-family, color y line-height definidos en <body> cruzan al shadow root salvo que la app los sobrescriba. Los estilos base de Wu definen box-sizing y contención, pero no resetean la tipografía heredada.
  • La contención de layout cambia el posicionamiento. El contenedor del sandbox define contain: layout style paint e isolation: isolate, lo que crea un bloque contenedor. Los modales y desplegables que dan por hecho que pueden escapar a document.body pueden necesitar ajustes.

Sea cual sea el modo, cada shadow root recibe una hoja de estilos pequeña:

:host {
display: block;
width: 100%; height: 100%;
box-sizing: border-box;
contain: layout style paint;
}
.wu-app-root {
width: 100%; height: 100%;
box-sizing: border-box;
isolation: isolate;
position: relative;
overflow: hidden;
}
* { box-sizing: border-box; }

Fíjate en el overflow: hidden de la raíz de la app: el contenido que se desborda del contenedor se recorta. Si tu app necesita desbordar (un desplegable que escapa de su tarjeta), sobrescríbelo desde dentro de la app o dimensiona el contenedor anfitrión en consecuencia.

Ayuda para depurar: pon el atributo wu-debug en el contenedor anfitrión para obtener un contorno discontinuo y una etiqueta con el nombre de la app.

wu.tagStyleAsApp(el, 'cart'); // marca un style/link como propiedad de una app
wu.getSandboxInfo('cart'); // confirma que la app llegó a montarse

Si necesitas forzar una reinyección tras hacer algo inusual con las hojas de estilo del anfitrión, el sandbox expone reinjectStyles(appName), que respeta el modo de la app (no hace nada en isolated).