¿Cómo escribir una regla de ESLint?
Crear una regla de ESLint personalizada para prohibir el uso de un componente. Cómo recorrer el AST, identificar el nodo correcto y empaquetar la regla en un plugin local.
Hace un tiempo, en Crehana tuvimos la necesidad de prohibir el uso del componente next/image. La razón era que ya teníamos un componente, llamado CustomNextImage, que envuelve a next/image y aplica ciertas optimizaciones extra a las imágenes.
El componente se ve más o menos así:
import * as React from 'react';
import Image from 'next/image';
const CustomNextImage = ({ src, ...rest }) => {
/**
* lógica para optimizaciones y features extra...
*/
return <Image src={src} {...rest} />;
};
export default CustomNextImage;En este artículo voy a describir cómo crear una regla de ESLint que muestre un error cuando alguien use next/image directamente, sugiriendo CustomNextImage como reemplazo.
Objetivo
Escribir una regla de ESLint que muestre un error cuando se use el componente Image de Next.js.
Setup del proyecto
Lo primero es entender que una regla de ESLint no se puede consumir directamente desde el archivo de configuración (.eslintrc.js). Para probar nuestra regla nueva tenemos que crear un eslint-plugin, y ese plugin se configurará con la regla que vamos a escribir.
Lo ideal sería publicar el plugin como un paquete en npm e instalarlo en el proyecto que lo va a usar, pero por simplicidad acá vamos a hacer una instalación local.
-
Creamos una carpeta para nuestro ESLint plugin:
mkdir eslint-plugin-custom cd eslint-plugin-custom yarn init --yes # o npm init -y -
Editamos el
package.jsony cambiamos los camposnameymain. Usaremoseslint-plugin-customcomo nombre del paquete:{ "name": "eslint-plugin-custom", "main": "src/index.js", "version": "1.0.0", "license": "MIT" } -
Dentro de
eslint-plugin-custom, creamos una carpetasrccon esta estructura:eslint-plugin-custom/ └── src/ ├── rules/ │ └── no-next-image-component.js └── index.js └── package.json -
Volvemos al proyecto web y hacemos una instalación local del plugin. Para eso editamos el
package.jsonagregando manualmente el nombre del plugin y la ruta donde vive.Suponiendo esta estructura de carpetas:
eslint-plugin-custom/ └── src └── package.json my-web-project/ └── package.json └── .eslintrc.jsEl
package.jsondel proyecto web debería quedar así:{ "name": "my-web-project", "version": "1.0.0", "main": "index.js", "license": "MIT", "dependencies": { "eslint-plugin-custom": "../eslint-plugin-custom" } }Después corremos
yarn installonpm installpara instalar la dependencia local. -
Usamos
eslint-plugin-customen la config de ESLint del proyecto web:// my-web-project/.eslintrc.js module.exports = { extends: ['plugin:eslint-plugin-custom/recommended'], };El
/recommendedes porque vamos a exponer una configuración "recomendada" con las reglas activadas por defecto.Si quisiéramos activar manualmente cada regla del plugin, sería así:
// my-web-project/.eslintrc.js module.exports = { plugins: ['eslint-plugin-custom'], rules: { 'eslint-plugin-custom/no-next-image-component': 2, }, };
Analizar el AST e identificar el nodo a usar
Antes de escribir la regla, hay que entender cómo funciona ESLint por dentro.
A grandes rasgos, ESLint analiza todo el código fuente y construye un objeto gigante llamado AST (Abstract Syntax Tree). Ese AST contiene información detallada sobre la estructura del código.
Vamos a usar astexplorer para inspeccionar el AST que genera el código de CustomNextImage.
Configuramos el parser con @typescript-eslint/parser (si no usas TypeScript, puedes usar @babel/parser).

Después pegamos el código de CustomNextImage:

Una vez que el AST está generado, ESLint ejecuta una función por cada type que existe en él.
Antes de escribir la regla hay que identificar el tipo de nodo que queremos analizar. En astexplorer podemos hacer click sobre el tag JSX Image y la herramienta nos lleva al objeto que lo representa en el AST.
Al clickear sobre el inicio de <Image .. vemos que apunta a un nodo de tipo JSXOpeningElement:

Quedándonos solo con la parte que nos interesa:
{
"openingElement": {
"type": "JSXOpeningElement",
"selfClosing": true,
"name": {
"type": "JSXIdentifier",
"name": "Image",
"range": [176, 181]
}
}
}Este nodo JSXOpeningElement tiene una propiedad name, y a su vez esa propiedad tiene otra name con el string "Image". Vamos a usar ese dato dentro de la regla para detectar el uso de <Image /> y avisar al desarrollador que use CustomNextImage.
Estructura de una regla de ESLint
Una regla de ESLint es un objeto con dos propiedades: meta y create.
metacontiene metadata sobre la regla (link a la documentación, tipo, etc.).createes una función que retorna un objeto con los métodos que se ejecutan en cada tipo de nodo "visitado". Acá es donde vive la lógica de nuestra regla.
// rule example
module.exports = {
meta: {
docs: {
description: '',
category: '',
recommended: true,
url: 'https://url-to-the-docs-of-the-rule.com/',
},
fixable: 'code',
},
create(context) {
return {
// se ejecuta en cada nodo de tipo JSXOpeningElement
JSXOpeningElement: node => {},
// se ejecuta en cada nodo de tipo ImportDeclaration
ImportDeclaration: node => {},
};
},
};Puedes ver todas las opciones disponibles aquí.
Escribir la regla
Finalmente, escribimos la regla siguiendo esa estructura y declaramos un método JSXOpeningElement en el objeto que retorna create. El parámetro node que recibe esa función es exactamente el mismo objeto que vimos antes en el AST.
// JSXOpeningElement node
{
"type": "JSXOpeningElement",
"selfClosing": true,
"name": {
"type": "JSXIdentifier",
"name": "Image",
"range": [176, 181]
}
}La regla termina viéndose así:
// eslint-plugin-custom/src/rules/no-next-image-component.js
module.exports = {
meta: {
docs: {
description: 'Prohibit usage of next/image <Image /> component',
category: 'HTML',
recommended: true,
url: 'https://url-to-the-docs-of-the-rule.com/',
},
fixable: 'code',
},
create(context) {
return {
JSXOpeningElement: node => {
// si el tag no es 'Image', salimos
if (node.name.name !== 'Image') {
return;
}
// reportamos el error
context.report({
node,
message:
'Do not use next <Image /> component, instead use the <CustomNextImage /> component because ...',
});
},
};
},
};La función JSXOpeningElement no tiene mucha ciencia: simplemente verifica que el name del tag sea Image y reporta un error indicando que se debe usar CustomNextImage.
Configurar el plugin con la regla nueva
El paso final es exponer la regla desde eslint-plugin-custom:
// eslint-plugin-custom/src/index.js
module.exports = {
rules: {
'no-next-image-component': require('./rules/no-next-image-component'),
},
/**
* configuración 'recomendada' para que se pueda usar como
* extends: ['plugin:eslint-plugin-custom/recommended'],
*/
configs: {
recommended: {
plugins: ['eslint-plugin-custom'],
rules: {
'eslint-plugin-custom/no-next-image-component': 2,
},
},
},
};Una vez hecho esto, el CLI de ESLint va a mostrar un error cada vez que se use un <Image /> en el código.
Si la regla no parece tener efecto en tu proyecto, te sugiero:
- Volver a correr
yarn installonpm installen el proyecto web. A veces los cambios quedan únicamente en el plugin y no se replican. - Hacer un full reload del editor o IDE. Algunas extensiones tardan en detectar cambios en archivos de configuración.
Próximos pasos
Como mejora podríamos escribir una segunda regla que valide los imports a next/image. La idea sería mostrar un error en cada:
import Image from 'next/image';Así el desarrollador se entera desde el principio de que ese import está prohibido. Para implementarla puedes inspeccionar en astexplorer qué tipo de nodo se genera para los imports.
Conclusión
Espero que te sirva como una introducción breve a las reglas custom de ESLint. La regla que escribimos acá es bastante simple, pero ESLint expone muchas más opciones — incluyendo reglas auto-fixable que pueden modificar el código por sí mismas.
¡Gracias por leer!