El manifiesto (wu.json)
wu.json es la autodeclaración de una micro-app. Vive en la raíz de la URL de la
app y se descarga una vez durante wu.init().
{ "name": "cart", "entry": "assets/index-a1b2c3.js", "version": "2.1.0", "styleMode": "fully-isolated", "wu": { "exports": { "CartWidget": "components/CartWidget.js" }, "imports": ["catalog.ProductCard"], "routes": ["/cart", "/checkout"], "permissions": ["storage"], "roles": ["customer", "admin"] }}| Campo | Tipo | Obligatorio | Límite |
|---|---|---|---|
name |
string | ✅ | máx. 50 caracteres |
entry |
string | ✅ | máx. 200 caracteres |
version |
string | se descarta si no es un string | |
styleMode |
enum | recurre a shared si es inválido |
|
folder |
string | se rechaza si coincide con un patrón peligroso | |
wu.exports |
object | máx. 100 entradas | |
wu.imports |
string[] | máx. 50 entradas | |
wu.routes |
string[] | máx. 100 entradas | |
wu.permissions |
string[] | — | |
wu.roles |
string[] | se descartan las entradas que no sean string |
Todo lo demás se descarta. normalize() reconstruye el objeto del
manifiesto desde cero y conserva solo los campos anteriores. Añadir metadatos
propios a wu.json esperando leerlos en tiempo de ejecución no va a funcionar.
El archivo entero debe pesar menos de 100 KB.
Debe coincidir con el nombre que la app pasa a wu.define() y con el nombre que
el shell usa en wu.init(). Los espacios sobrantes se recortan. Se rechaza si
contiene algún patrón peligroso (ver más abajo).
Ruta al módulo de JavaScript, relativa a la URL de la app. Si empieza por un
prefijo de carpeta reconocido — src/, dist/, public/, build/, assets/,
lib/ o es/ — Wu la usa directamente, sin sondear nada.
Cualquier otra cosa se resuelve en dos pasos. Primero se prueba la ruta tal
como la declara el manifiesto (<url>/<entry>) con un solo HEAD: si
responde, ahí acaba, y es lo que pasa cuando el manifiesto está bien puesto. Solo
si esa falla se abre un sondeo HEAD en paralelo de 7 candidatos por las
carpetas de arriba, y se toma el primero en orden de prioridad, no el más rápido.
El resultado se cachea por baseUrl + entry, así que los remontajes no repiten
el sondeo. Si no responde ninguno, el error nombra la ruta declarada — la que
tiene que arreglar quien publicó la app, no el último candidato probado.
Normalización: se elimina un ./ inicial y a un valor sin extensión se le añade
.js.
{ "entry": "src/main.tsx" } // → <url>/src/main.tsx{ "entry": "assets/index-a1b2.js" } // → <url>/assets/index-a1b2.js{ "entry": "main" } // → "main.js", y luego se sondeaPublica un entry con prefijo en producción. Mira Errores comunes.
styleMode
Sección titulada «styleMode»Uno de shared (por defecto), isolated, fully-isolated, o los alias none y
own-only. Este es el único sitio del que se lee styleMode: el campo en la
configuración de apps de wu.init() se ignora. Mira
Aislamiento de CSS.
Un valor no reconocido registra un aviso y se convierte en shared.
wu.roles
Sección titulada «wu.roles»Roles con permiso para montar esta app, para RBAC. Se leen de
wu.roles o de un roles de primer nivel; ambos se aceptan, y el manifiesto
normalizado los expone en wu.roles.
{ "name": "billing", "entry": "dist/main.js", "wu": { "roles": ["admin", "finance"] } }Las entradas vacías y las que no son string se filtran. Un valor que no sea un array registra un aviso y se ignora.
wu.exports / wu.imports
Sección titulada «wu.exports / wu.imports»exports asocia el nombre de un componente a una ruta dentro de la app, para
wu.use(). Las rutas se normalizan igual que entry (se quita el ./ inicial,
se añade .js si no tiene extensión) y cada una se comprueba contra los patrones
peligrosos.
imports declara dependencias con la forma "appName.componentName". Las
entradas sin un . se descartan con un aviso. validateDependencies() puede
contrastarlas con las apps registradas e informa de valid / invalid /
missing.
Ninguno de los dos campos es un enlazador. Describen intención para wu.use();
no dan a una app acceso al scope de módulos de otra.
wu.permissions y wu.routes
Sección titulada «wu.permissions y wu.routes»Arrays de formato libre, validados solo en tipo y longitud. Wu los registra y los deja disponibles para inspección; hacerlos cumplir es cosa de tu shell.
Validación
Sección titulada «Validación»Cada campo de tipo string se comprueba contra un conjunto de patrones peligrosos:
| Patrón | Bloquea |
|---|---|
.. |
Path traversal |
^/etc/, ^/proc/ |
Rutas del sistema |
^file:// |
Protocolo de archivos |
javascript: |
Inyección de URL JS |
data: |
URLs de datos |
<script |
Inyección de etiquetas |
on\w+\s*= |
Manejadores de eventos en línea |
entry y todas las rutas de export pasan además por una comprobación de URL que
también rechaza una pequeña lista de dominios bloqueados.
Ausente frente a inválido — una asimetría importante
Sección titulada «Ausente frente a inválido — una asimetría importante»GET <url>/wu.json │ ├─ 404 ──────────────────► manifiesto por defecto, sin error │ { name: <último segmento de la URL>, entry: 'index.js' } │ ├─ error de red / JSON ──► manifiesto por defecto, sin error │ (p. ej. un servidor de desarrollo devolviendo index.html para wu.json) │ ├─ 200, no valida ───────► LANZA ERROR (code: 'WU_MANIFEST_INVALID') │ └─ 200, válido ──────────► manifiesto normalizado, cacheadoLa rama del medio es la que importa. Un manifiesto que existe pero viola una
regla de seguridad es fatal: degradarlo al valor por defecto permisivo
dejaría que una app con un entry o una ruta de export peligrosos se registrase
igualmente, lo que convertiría toda la capa de validación en decorativa.
Un 404, en cambio, es una comodidad legítima para el desarrollo — pero también es
la causa del fallo en producción más habitual, porque el entry: 'index.js' del
manifiesto por defecto manda a Wu al sondeo de 8 rutas y los hosts estáticos
responden con index.html.
La descarga tiene un timeout de 10 segundos y se envía con cache: 'no-cache' y
Accept: application/json.
Los manifiestos validados se cachean en memoria, indexados por URL. Las llamadas
repetidas a init() los reutilizan.
wu.manifest.clearCache(); // todowu.manifest.clearCache('cart'); // las URLs que casen con esta regexwu.manifest.getStats(); // { cached, schemas, cacheKeys }Una regex inválida pasada a clearCache registra un aviso y no hace nada, en
lugar de lanzar un error.
Construir uno mediante código
Sección titulada «Construir uno mediante código»const manifest = wu.manifest.create('cart', { entry: 'dist/main.js', exports: { CartWidget: 'components/CartWidget.js' }, imports: ['catalog.ProductCard'],});create() ejecuta la misma normalización que un manifiesto descargado, así que
es una forma útil de comprobar en qué se va a convertir realmente una entrada
dada.
Generarlo durante el build
Sección titulada «Generarlo durante el build»Mantener a mano un entry con hash no sobrevive ni una semana. Emítelo desde el
build:
import { writeFileSync } from 'node:fs';
export default { plugins: [{ name: 'wu-manifest', writeBundle(_, bundle) { const entry = Object.values(bundle).find((c) => c.isEntry); writeFileSync('dist/wu.json', JSON.stringify({ name: 'cart', entry: `assets/${entry.fileName.split('/').pop()}`, version: process.env.npm_package_version, styleMode: 'shared', }, null, 2)); }, }],};Sirve dist/ y el manifiesto queda junto al bundle que describe.
