npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@numaris-microfront/contracts

v0.2.7

Published

Eventos de dominio, contrato de montaje y manifiesto de widget de la plataforma de micro-frontends Numaris.

Downloads

922

Readme

@numaris-microfront/contracts

El contrato entre el Shell (el host) y un widget (un remoto de Module Federation) de la plataforma de rastreo Numaris. Si estas construyendo un widget nuevo para esta plataforma, este es el paquete que necesitas leer entero antes de escribir codigo.

Trae tres cosas:

  1. Los eventos de dominio con los que tu widget se comunica con los demas, sin importarlos y sin conocerlos.
  2. El contrato de montaje: que te pasa el Shell cuando monta tu widget.
  3. El esquema del manifiesto: el archivo que declara, antes de escribir codigo, que eventos tu widget provee y cuales consume.

Instalacion

npm install @numaris-microfront/contracts

Depende de @numaris-microfront/event-bus (el bus subyacente) y zod (validacion de esquemas). No depende de React ni de ninguna libreria de UI.

El modelo mental: nunca importes otro widget

La regla de arquitectura de esta plataforma es una sola: un widget nunca importa codigo de otro widget, y nunca asume que otro widget esta presente. Toda comunicacion entre widgets pasa por el Event Bus, con eventos de dominio (trackable.selected), nunca de UI (tableRowClicked). Si tu widget necesita saber algo que otro widget podria proveer, te suscribis al evento correspondiente — y si el evento nunca llega (porque ese otro widget esta desactivado, o no existe en este despliegue), tu widget tiene que seguir funcionando con un fallback propio. Esa es la regla 1.3, y el esquema del manifiesto (mas abajo) la hace cumplir en validacion.

1. Los eventos de dominio (numarisBus)

import { numarisBus } from '@numaris-microfront/contracts';

// Suscribirse. Por default recibis el ultimo valor emitido, si existe (ver
// @numaris-microfront/event-bus para el detalle de la retencion).
const unsubscribe = numarisBus.on('trackable.selected', (payload, meta) => {
  console.log(payload.trackableId); // string | null
});

// Emitir.
numarisBus.emit('trackable.selected', { trackableId: 'trk-42' });

unsubscribe();

numarisBus es la MISMA instancia que expone @numaris-microfront/event-bus, solo que tipada con NumarisEventMap — el mapa completo de eventos de esta plataforma. Autocompletado y chequeo de tipos vienen gratis: emitir un evento con un payload mal formado no compila.

Catalogo de eventos (NumarisEventMap)

| Evento | Payload | Quien lo provee | Que significa | |---|---|---|---| | trackable.list.updated | { trackables: TrackableSummary[] } | trackable-list | El catalogo visible de la cuenta, completo. Se emite en CADA carga (inicial y cualquier refetch) — no uses esto para saber si algo cambio, solo para leer el estado actual. | | trackable.catalog.changed | { change: 'created' \| 'updated' \| 'deleted', trackableId: string } | trackable-list | SEÑAL de que una unidad se dio de alta, se edito o se dio de baja, confirmado por el servidor. Se emite UNA vez por mutacion, nunca en cada carga. Quien lo recibe relee su propia fuente de datos — el payload no trae el catalogo. | | trackable.selected | { trackableId: string \| null } | Cualquier widget que permita seleccionar una unidad | Que unidad esta seleccionada ahora. null es un valor legitimo: significa "ninguna". | | trackable.focus.requested | { trackableId: string } | Cualquier widget que pueda pedir mirar de cerca una unidad | SEÑAL: "acercate a esta unidad". El mapa centra la camara y cambia el zoom. NO es lo mismo que trackable.selected y no cambia la seleccion: si querés las dos cosas, emiti los dos eventos. Sin null —no existe pedir foco sobre nada— y sin nivel de zoom en el payload: el zoom lo decide quien tiene el mapa. Consumilo con replayLast: false, o un pedido viejo te mueve la camara sola al montar. | | poi.selected | { poiId: string \| null } | Cualquier widget que permita seleccionar un punto de interes | Que punto de interes esta seleccionado ahora. null significa "ninguno". Es HERMANO de trackable.selected y no el mismo evento: son dos espacios de ids distintos. | | poi.catalog.changed | { change: 'created' \| 'updated' \| 'deleted', poiId: string } | poi-list | SEÑAL de que un punto de interes se dio de alta, se edito o se dio de baja. Mismo patron que trackable.catalog.changed: quien lo recibe relee su propia fuente. | | organization.branding.updated | { organizationId: string } | El Shell | La marca de la organizacion (colores, tipografia) se guardo y ya esta aplicada. SEÑAL, no dato — los valores reales viajan por CSS variables en el documento, no en el payload. | | command.dispatched | { deviceId: string, commandId: string, dispatchedAt: number } | commands | Un comando se envio de verdad al equipo, confirmado por el servidor (no la intencion optimista con la que un widget puede pintar antes de llamar a la red). |

TrackableSummary (la forma de cada fila en trackable.list.updated):

interface TrackableSummary {
  id: string;
  name: string;
  icon: TrackableIcon; // union cerrada, ver TRACKABLE_ICONS mas abajo
  lastSeen: number | null; // epoch en SEGUNDOS de la hora de recepcion, o null si nunca reporto
  deviceId: string | null;
}

Reglas para consumir un evento

  • Todo consumo es opcional. Tu widget tiene que andar sin ese evento — otro widget que lo provee puede estar desactivado en este despliegue, o directamente no existir. Diseñá un fallback (normalmente: tu widget resuelve el dato con su propio fetch).
  • Tu widget puede no estar montado cuando el evento se emite. La plataforma anfitriona puede tener varias pantallas y montar sólo la que se está viendo, así que el widget que provee lo que consumís puede estar vivo, contratado, y simplemente en otra. No es el caso raro: es uno de los dos normales. Un evento retenido te llega igual al montar; una señal emitida mientras no estabas, no — por eso el fallback no es opcional.
  • El ultimo valor se retiene por tipo, asi que el orden en que se montan los widgets no cambia lo que cada uno ve. Si NO queres el replay (por ejemplo, para reaccionar solo a cambios que pasen DESPUES de montar, no al estado que ya tenias), suscribite con { replayLast: false }.
  • No inventes un evento nuevo sin agregarlo a este paquete. El mapa de eventos es el contrato compartido — un evento que solo vive en tu widget no lo puede escuchar nadie mas, y si le pones un nombre que choca con uno existente pero con otra forma de payload, rompe en runtime sin error de compilacion.

2. El contrato de montaje

Lo que el Shell le pasa a tu widget cuando lo monta, por props — la unica via de datos Shell-a-widget (nunca al reves, y nunca widget-a-widget):

import type { WidgetMountProps, PocSession } from '@numaris-microfront/contracts';

interface PocSession {
  getToken(): Promise<string>; // PEDILO en el momento de usarlo, no lo guardes en una variable:
                                // el token expira y una copia vieja capturada en un render queda muerta
  organizationId: string; // el tenant activo, ya resuelto — no lo derives de localStorage ni de la URL
  userLabel: string;
}

interface WidgetMountProps {
  session: PocSession;
  mountMode: 'shell' | 'standalone'; // tu widget tiene que comportarse igual en los dos casos
}

mountMode existe solo para diagnostico — si tu widget necesita ramificar comportamiento real segun si esta dentro del Shell o standalone (mas alla de, por ejemplo, mostrar un header propio en modo standalone), probablemente estas acoplando tu widget al host de una forma que no deberias.

Esas props no llegan a un componente: llegan a una funcion. Lo que tu bundle expone como ./Widget no es un componente de React, es un mount:

import type { WidgetMount, WidgetModule } from '@numaris-microfront/contracts';

type WidgetUnmount = () => void;
type WidgetMount = (container: HTMLElement, props: WidgetMountProps) => WidgetUnmount;

interface WidgetModule {
  mount: WidgetMount; // lo que el Shell busca en tu modulo expuesto
}

Por eso tu widget puede estar escrito con cualquier cosa que sepa pintar dentro de un HTMLElement: lo que cruza la frontera es un nodo del DOM y datos planos, no un arbol de React.

Tres cosas que hay que respetar, y que el tipo no puede exigir:

  • mount es sincrono. El Shell necesita tu desmontaje en el mismo tick en que monta. Todo lo asincrono —pedir tus datos— va adentro, despues de montar. Un mount que devuelva una promesa deja tu widget vivo sin nadie que lo apague la primera vez que el host lo desmonta rapido.
  • El desmontaje saca tu widget de la pantalla antes de volver. Lo que hagas despues con tu framework puede esperar un microtask, y en React tiene que esperarlo.
  • El error boundary es tuyo. Tu widget corre en su propia raiz, y el host no ve lo que pasa adentro: sin un boundary propio, un error de render deja tu hueco vacio y nadie se entera. Lo que el host todavia caza es el montaje —que tu bundle no cargue, que no exponga mount, que mount explote—.

Si tu widget es de React no escribas nada de esto a mano: npm create @numaris-microfront/widget te genera el src/mount.tsx con las tres cosas ya resueltas —el nodo propio, el desmontaje diferido y el boundary— y tu widget.tsx sigue siendo un componente normal con estas props. Ese archivo se genera y no se instala: no hay paquete publicado del adaptador, asi que el codigo es tuyo y lo podes leer entero.

3. El manifiesto (widgetManifestSchema)

Antes de escribir el codigo de tu widget, declarás un manifiesto — un JSON que dice qué eventos provee tu widget y cuáles consume, con su fallback. Se valida con este esquema:

import { validateWidgetManifest } from '@numaris-microfront/contracts';

const result = validateWidgetManifest({
  id: 'my-widget',
  title: 'Mi widget',
  version: '1.0.0',
  provides: [
    {
      event: 'my-widget.item.selected',
      description: 'Se eligio un item en la lista',
      payload: { itemId: 'id del item elegido, o null si se deselecciono' },
    },
  ],
  consumes: [
    {
      event: 'trackable.selected',
      description: 'Resaltar el item correspondiente si existe',
      required: false,
      fallback: 'No resalta nada hasta que el usuario elija manualmente',
    },
  ],
});

if (result.ok) {
  console.log(result.manifest);
} else {
  console.error(result.errors); // string[], uno por cada problema
}

Reglas que el esquema hace cumplir (no son solo convencion, son validacion real):

  • El nombre de un evento tiene que ser de dominio y punteado, en minuscula: sustantivo.accion (ej. trackable.selected). Un nombre de evento de UI como tableRowClicked no pasa la validacion.
  • Todo evento consumido con required: false (el default) tiene que declarar fallback: que hace tu widget cuando ese evento nunca llega.
  • required: true es la excepcion, no la norma, y el esquema te obliga a poner requiredApprovedByAdr — una referencia a la decision que aprobó depender obligatoriamente de otro widget. Sin esa referencia, la validación falla.

findUnprovidedConsumptions(manifests) compara una lista de manifiestos entre si y reporta (sin fallar) los eventos que alguien consume pero nadie de esa lista provee — útil para un panel de diagnóstico que muestre, en un despliegue dado, qué integraciones están "sueltas".

Otras piezas del paquete

  • layoutSchema / validateLayout / LAYOUT_SLOTS: si tu plataforma anfitriona organiza widgets en zonas (top, left, center, right, bottom), este es el esquema de esa estructura. Puede no aplicar a tu integración si el host no usa este layout.
  • validateWorkspaces / createWorkspaceSet / wrapLayout: un host puede tener varios acomodos por organización —espacios de trabajo, cada uno con nombre, icono y color— y esto es el esquema de esa colección. Dos cosas que conviene saber si vas a leer o escribir ese dato: un WorkspaceSet también valida como Layout, porque lleva un espejo del primer espacio para que un cliente que sólo entiende el modelo viejo lo siga leyendo; y validateWorkspaces acepta las dos formas, envolviendo un Layout suelto en un único espacio. Al escribir, armá siempre el conjunto con createWorkspaceSet: es lo único que mantiene ese espejo al día.
  • widgetCatalogSchema / validateWidgetCatalog: el esquema del catálogo que declara qué widgets existen y dónde vive el bundle de cada uno (usado por el host, no por el widget).
  • MANUFACTURERS / modelsFor(manufacturer): catálogo fijo de fabricantes y modelos de dispositivos GPS soportados. Vocabulario compartido entre widgets que necesitan ofrecer las mismas opciones (por ejemplo, un formulario de alta de unidad), no una fuente de verdad de catálogo real.
  • TRACKABLE_ICONS / TrackableIcon: la lista cerrada de íconos que puede tener una unidad. Es una unión de TypeScript, no un string, a propósito — un ícono que no está en la lista no compila.

Peer package

@numaris-microfront/contracts no importa React ni ninguna librería de UI. Si además querés componentes visuales consistentes con el resto de la plataforma, mirá @numaris-microfront/ui-kit.