Volver al blog

ESLint 9 y Flat Config: Guía Completa de Migración y Configuración

Hola HaWkers, ESLint 9 trajo un cambio significativo en la forma como configuramos el linter: el Flat Config. Si todavía estás usando .eslintrc.js o .eslintrc.json, es hora de entender el nuevo sistema y migrar tus proyectos.

En esta guía vamos a cubrir desde los conceptos básicos hasta configuraciones avanzadas para proyectos TypeScript y React.

Por Qué el Flat Config

El sistema antiguo de configuración de ESLint tenía algunas limitaciones:

Problemas del sistema antiguo:

  • Cascada confusa de configuraciones
  • Resolución compleja de plugins
  • Dificultad en entender cuál regla viene de dónde
  • Performance afectada por la búsqueda de archivos de configuración

Beneficios del Flat Config:

  • Archivo único de configuración (eslint.config.js)
  • Imports explícitos de plugins y configuraciones
  • Mejor performance
  • Más fácil de debuggear y entender
  • Soporte nativo a ESM

Estructura Básica del Flat Config

El Flat Config usa un archivo eslint.config.js (o .mjs, .cjs) en la raíz del proyecto:

// eslint.config.js
import js from '@eslint/js';

export default [
  js.configs.recommended,
  {
    rules: {
      'no-unused-vars': 'warn',
      'no-console': 'off',
    },
  },
];

Anatomía de una Configuración

// eslint.config.js
export default [
  // Configuración 1: Aplica a todos los archivos
  {
    rules: {
      'semi': ['error', 'always'],
    },
  },

  // Configuración 2: Aplica solo a archivos específicos
  {
    files: ['**/*.ts', '**/*.tsx'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'warn',
    },
  },

  // Configuración 3: Ignora archivos
  {
    ignores: ['dist/**', 'node_modules/**', '*.config.js'],
  },
];

Migración del Sistema Antiguo

Antes: .eslintrc.js

// .eslintrc.js (antiguo)
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'prettier',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint', 'react', 'react-hooks'],
  rules: {
    'react/react-in-jsx-scope': 'off',
    '@typescript-eslint/no-unused-vars': 'warn',
  },
};

Después: eslint.config.js

// eslint.config.js (nuevo)
import js from '@eslint/js';
import typescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import prettier from 'eslint-config-prettier';
import globals from 'globals';

export default [
  js.configs.recommended,

  // Configuración global
  {
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        ...globals.browser,
        ...globals.node,
        ...globals.es2021,
      },
    },
  },

  // TypeScript
  {
    files: ['**/*.ts', '**/*.tsx'],
    languageOptions: {
      parser: typescriptParser,
      parserOptions: {
        ecmaFeatures: { jsx: true },
      },
    },
    plugins: {
      '@typescript-eslint': typescript,
    },
    rules: {
      ...typescript.configs.recommended.rules,
      '@typescript-eslint/no-unused-vars': 'warn',
    },
  },

  // React
  {
    files: ['**/*.jsx', '**/*.tsx'],
    plugins: {
      react,
      'react-hooks': reactHooks,
    },
    rules: {
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      'react/react-in-jsx-scope': 'off',
    },
    settings: {
      react: {
        version: 'detect',
      },
    },
  },

  // Prettier (debe ser el último)
  prettier,

  // Ignores
  {
    ignores: ['dist/**', 'node_modules/**', '.next/**'],
  },
];

Configuraciones Comunes

Proyecto React + TypeScript

// eslint.config.js
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import globals from 'globals';

export default [
  js.configs.recommended,
  ...typescript.configs.recommended,

  {
    files: ['**/*.{js,jsx,ts,tsx}'],
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.es2021,
      },
    },
    plugins: {
      react,
      'react-hooks': reactHooks,
      'jsx-a11y': jsxA11y,
    },
    rules: {
      // React
      'react/prop-types': 'off',
      'react/react-in-jsx-scope': 'off',
      'react/jsx-uses-react': 'off',

      // Hooks
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',

      // Accesibilidad
      'jsx-a11y/alt-text': 'error',
      'jsx-a11y/anchor-is-valid': 'warn',

      // TypeScript
      '@typescript-eslint/no-unused-vars': ['warn', {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
      }],
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/no-explicit-any': 'warn',
    },
    settings: {
      react: { version: 'detect' },
    },
  },

  {
    ignores: ['dist/**', 'build/**', 'node_modules/**'],
  },
];

Proyecto Node.js + TypeScript

// eslint.config.js
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import node from 'eslint-plugin-n';
import globals from 'globals';

export default [
  js.configs.recommended,
  ...typescript.configs.recommended,

  {
    files: ['**/*.{js,ts}'],
    languageOptions: {
      globals: {
        ...globals.node,
        ...globals.es2021,
      },
    },
    plugins: {
      n: node,
    },
    rules: {
      // Node.js
      'n/no-missing-import': 'off', // TypeScript cuida de eso
      'n/no-unsupported-features/es-syntax': 'off',
      'n/no-process-exit': 'warn',

      // TypeScript
      '@typescript-eslint/no-unused-vars': ['error', {
        argsIgnorePattern: '^_',
      }],
      '@typescript-eslint/explicit-function-return-type': ['warn', {
        allowExpressions: true,
      }],

      // General
      'no-console': ['warn', { allow: ['warn', 'error'] }],
      'prefer-const': 'error',
    },
  },

  {
    ignores: ['dist/**', 'node_modules/**', '*.js'],
  },
];

Plugins y Sus Nuevas Configuraciones

@typescript-eslint

El typescript-eslint v8+ tiene soporte nativo al Flat Config:

import typescript from 'typescript-eslint';

export default [
  // Usa el helper de typescript-eslint
  ...typescript.configs.recommended,

  // O configuración más estricta
  ...typescript.configs.strict,

  // Customizaciones
  {
    files: ['**/*.ts', '**/*.tsx'],
    rules: {
      '@typescript-eslint/consistent-type-imports': 'error',
      '@typescript-eslint/no-floating-promises': 'error',
    },
  },
];

eslint-plugin-import

import importPlugin from 'eslint-plugin-import';

export default [
  {
    plugins: {
      import: importPlugin,
    },
    rules: {
      'import/order': ['error', {
        groups: [
          'builtin',
          'external',
          'internal',
          'parent',
          'sibling',
          'index',
        ],
        'newlines-between': 'always',
        alphabetize: { order: 'asc' },
      }],
      'import/no-duplicates': 'error',
      'import/no-unresolved': 'error',
    },
    settings: {
      'import/resolver': {
        typescript: true,
        node: true,
      },
    },
  },
];

Tips de Migración

1. Usa el Migration Tool

ESLint ofrece una herramienta de migración:

npx @eslint/migrate-config .eslintrc.js

2. Verifica Compatibilidad de Plugins

No todos los plugins fueron actualizados para Flat Config. Verifica la documentación de cada uno:

// Algunos plugins todavía usan el formato antiguo
// Usa el compatibility layer si es necesario
import { FlatCompat } from '@eslint/eslintrc';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

export default [
  // Plugins modernos
  js.configs.recommended,

  // Plugins legados via compat
  ...compat.extends('plugin:legacy-plugin/recommended'),

  // Tus reglas
  {
    rules: {
      // ...
    },
  },
];

3. Remueve Archivos Antiguos

Después de migrar, remueve los archivos de configuración antiguos:

rm .eslintrc.js .eslintrc.json .eslintrc.yaml .eslintignore

El Flat Config usa la propiedad ignores en el propio archivo de configuración.

4. Actualiza los Scripts

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}

Debugging de la Configuración

Ver Configuración Final

npx eslint --print-config src/index.ts

Verificar Cuáles Archivos Son Lintados

npx eslint . --debug 2>&1 | grep "Linting"

Testar Reglas Específicas

// eslint.config.js - temporariamente
export default [
  {
    files: ['**/*.ts'],
    rules: {
      // Testa solo una regla
      'no-console': 'error',
    },
  },
];

Conclusión

El Flat Config de ESLint 9 representa una evolución significativa en la forma como configuramos linting en proyectos JavaScript y TypeScript. Aunque la migración exige algún esfuerzo inicial, los beneficios de claridad, performance y mantenibilidad compensan.

Recomendaciones finales:

  • Migra proyectos nuevos directamente para Flat Config
  • Para proyectos existentes, usa la herramienta de migración como punto de partida
  • Verifica la compatibilidad de los plugins antes de migrar
  • Aprovecha para revisar y simplificar tus reglas durante la migración

Si estás interesado en más herramientas para mejorar la calidad de tu código, recomiendo conferir el artículo Debugging JavaScript: Técnicas Avanzadas con DevTools que complementa bien las prácticas de linting con técnicas de debugging.

¡Vamos a por ello! 🦅

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios