Ir al contenido

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 sondea

Publica un entry con prefijo en producción. Mira Errores comunes.

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.

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.

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.

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.

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, cacheado

La 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(); // todo
wu.manifest.clearCache('cart'); // las URLs que casen con esta regex
wu.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.

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.

Mantener a mano un entry con hash no sobrevive ni una semana. Emítelo desde el build:

vite.config.js
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.