eslint-plugin-botmesh detecta en el código fuente los errores que ningún
linter genérico conoce: API v1 que ya no existe, un runtime.enter()
bloqueante, un install() de plugin async, triggers vacíos, esperas sin
timeout y estado por usuario en variables de módulo. Complementa a
bot.lint() / lintFlows() en tiempo de
ejecución, que revisan los flujos registrados (gotos rotos, pasos
inalcanzables, triggers duplicados).
Necesita ESLint 9 o superior (flat config) y Node >= 22. El src/bot.js
de cada plantilla de create-botmesh
pasa el preset strict sin avisos (lo comprueban los tests del plugin).
Instalación
Uso
typescript-eslint (su parser) y lista los
archivos .ts, porque ESLint solo revisa .js/.mjs/.cjs por defecto:
Presets
Reglas
botmesh/no-v1-api
Marca la API v1 / builderbot que no existe en v2 e indica el reemplazo en el
mensaje:
botmesh/no-await-runtime-enter
ConversationRuntime.enter() resuelve cuando la conversación termina.
Hacerle await en un handler bloquea la cola FIFO del chat, así que el mensaje
que espera la conversación nunca se despacha. La regla marca
await runtime.enter(...), return runtime.enter(...) (el dispatcher espera
la promesa devuelta), una arrow function cuyo cuerpo es runtime.enter(...)
y esas tres formas con .catch(...) / .then(...) / .finally(...)
encadenados o con llamadas opcionales (await runtime?.enter(...)): la promesa
encadenada también se resuelve solo cuando la conversación termina.
{ receiverPattern: string }, el código de una regex que debe cumplir
el nombre del receptor (por defecto runtime$, sin distinguir mayúsculas).
botmesh/no-async-plugin-install
Plugin.install() debe ser síncrono: bot.plug() no lo espera, así que el
bot arranca antes de que un install async termine y su rechazo solo llega al
evento 'error' (en versiones anteriores del core era un unhandled rejection
que tumbaba el proceso). TypeScript ya rechaza un install async al compilar
(PluginSpec.install devuelve void | SyncInstallError); la regla cubre
JavaScript y los escapes con as any. Revisa el objeto que se pasa a definePlugin(...), cualquier
objeto literal con name e install, y las clases con un método install
que declaran un campo name o implements Plugin. Para inicialización async
usa el patrón lazy (resolver en el primer uso y reportar los errores de
configuración desde un warmup no fatal), como LazyProvider en @botmesh/ai.
Sin opciones.
botmesh/no-empty-trigger
Un trigger vacío ('', ' ', un template literal vacío, [], [''],
{ pattern: '' }) no coincide con ningún mensaje real o, con contains,
coincide con todos. Se revisa en flow(...) / addKeyword(...),
bot.when(...) y la opción trigger de bot.conversation(name, fn, { trigger }).
El core además lanza un TypeError al registrar un patrón de texto vacío; la
regla lo detecta (igual que []) antes de ejecutar el código. flow([]) /
addKeyword([]) sí se permite: es la forma de declarar un subflujo sin trigger
propio (se ejecuta desde un then de choose, .use() o un goto); bot.lint()
avisa de los que nada alcanza.
Sin opciones.
botmesh/require-timeout-on-ask
Toda espera de respuesta debería fijar su timeout a propósito. Sin él, la
espera usa el conversation.timeout del bot o, si no hay, los 5 minutos
integrados, que rara vez sirven igual para un formulario largo que para un sí/no
rápido. Un timeout de 0 (o '0s') lo desactiva: si el usuario deja de
responder, la conversación sigue abierta, capturando sus mensajes, hasta un
comando como /reset. La regla marca los dos casos.
- DSL de flujos:
.ask(text, key, { timeout }),.choose(prompt, options, key, { timeout }),.form(definition, { timeout }), o un.timeout(...)en cualquier punto de la misma cadenaflow(...)(salvo conallowFlowTimeout: false). - Conversaciones:
c.ask(text, { timeout })y, si están enmethods,c.waitText({ timeout }),c.waitMedia(types, { timeout }),c.waitLocation({ timeout }),c.choose(prompt, options, { timeout })yc.waitAny({ timeoutMs }).waitAnyrecibe milisegundos entimeoutMse ignora una clavetimeout, así que la regla también marcac.waitAny({ timeout }). - Si el argumento de opciones no es un objeto literal (una variable, una llamada) o contiene un spread, la regla no puede ver su contenido y no avisa.
conversation.timeout del bot, desactiva la
regla en lugar de repetirlo en cada paso.
Opciones: { methods?: ('ask' | 'waitText' | 'waitMedia' | 'waitLocation' | 'choose' | 'waitAny' | 'form')[], allowFlowTimeout?: boolean }
(por defecto ['ask'] y true).
botmesh/prefer-ctx-user-over-global-state
Marca el globalState de v1 y los Map, Set u objetos de módulo indexados
por ctx.from, ctx.sender o ctx.senderId. Las escrituras
(users.set/delete(ctx.from), seen.add(ctx.from), scores[ctx.sender] = …,
scores[ctx.sender].n++) se marcan siempre; las lecturas
(users.get/has(ctx.from), scores[ctx.sender]), solo si la misma variable
también se escribe o se reasigna dentro de una función. Una lista constante de
permitidos o bloqueados (const ADMINS = new Set([...]),
if (blocked.has(ctx.from))), aunque se rellene a nivel de módulo, no guarda
estado por usuario y no se marca. El estado por usuario se pierde al reiniciar,
crece sin límite y no se comparte entre réplicas. Usa
ctx.user
(await ctx.user.ns('feature').set(key, value)) para los datos por usuario y
c.state para los datos de la conversación.
Los datos efímeros del proceso que no deben sobrevivir a un reinicio, como la
ventana de un rate limit, son una excepción deliberada: desactiva la regla en
esa línea (// eslint-disable-next-line botmesh/prefer-ctx-user-over-global-state).
Opción: { contextNames: string[] }, los nombres del parámetro de contexto
del handler (por defecto ['ctx']).
API
rules (rules['no-v1-api']); no
son exports con nombre. El meta.docs.url de cada regla apunta a su sección
de la versión en inglés de esta página, así que el editor enlaza cada aviso con
su explicación.