Ctrl K
ComunidadGuías…

eslint.config.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 reactCompiler from 'eslint-plugin-react-compiler';
import integrationBoundaries from './eslint-rules/integration-boundaries.mjs';
import moduleSdkBoundary from './eslint-rules/module-sdk-boundary.mjs';
import { MAX_LINES_TODO } from './eslint-rules/max-lines-todo.mjs';
import {
  ELSE_RETURN,
  NESTED_TERNARY,
  NON_NULL_ASSERTION,
  OBJECT_LITERAL_ASSERTION,
  PREFER_CONST,
  TYPE_IMPORTS,
} from './eslint-rules/cleanliness-todo.mjs';
import {
  NULLISH_COALESCING,
  OPTIONAL_CHAIN,
  SWITCH_EXHAUSTIVE,
  UNNECESSARY_ASSERTION,
  UNNECESSARY_CONDITION,
} from './eslint-rules/typed-lint-todo.mjs';

export default [
  js.configs.recommended,

  // Node build config (esbuild)
  {
    files: ['api/esbuild.config.js'],
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'script',
      globals: {
        require: 'readonly',
        module: 'readonly',
        process: 'readonly',
        __dirname: 'readonly',
      },
    },
  },

  // Backend TypeScript files (api/src/**/*.ts)
  {
    files: ['api/src/**/*.ts'],
    languageOptions: {
      parser: typescriptParser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        project: './api/tsconfig.json',
      },
      globals: {
        console: 'readonly',
        process: 'readonly',
        Buffer: 'readonly',
        URL: 'readonly',
        __dirname: 'readonly',
        __filename: 'readonly',
        module: 'readonly',
        require: 'readonly',
        exports: 'readonly',
        global: 'readonly',
        globalThis: 'readonly',
        setTimeout: 'readonly',
        clearTimeout: 'readonly',
        setInterval: 'readonly',
        clearInterval: 'readonly',
        setImmediate: 'readonly',
        clearImmediate: 'readonly',
        Promise: 'readonly',
        NodeJS: 'readonly',
      },
    },
    plugins: {
      '@typescript-eslint': typescript,
    },
    rules: {
      ...typescript.configs.recommended.rules,
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/explicit-function-return-type': 'off',
      // Use `log` from utils/logger. Stray console.log/debug dumps reached
      // production logs unfiltered; console.warn/error remain in catch blocks.
      'no-console': ['error', { allow: ['warn', 'error'] }],
    },
  },

  // Scripts TypeScript files (scripts/**/*.ts)
  {
    files: ['scripts/**/*.ts'],
    languageOptions: {
      parser: typescriptParser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        // No project requirement - scripts don't need strict type checking
      },
      globals: {
        console: 'readonly',
        process: 'readonly',
        Buffer: 'readonly',
        URL: 'readonly',
        __dirname: 'readonly',
        __filename: 'readonly',
        module: 'readonly',
        require: 'readonly',
        exports: 'readonly',
        global: 'readonly',
        globalThis: 'readonly',
        setTimeout: 'readonly',
        clearTimeout: 'readonly',
        setInterval: 'readonly',
        clearInterval: 'readonly',
        setImmediate: 'readonly',
        clearImmediate: 'readonly',
        Promise: 'readonly',
        NodeJS: 'readonly',
        // Some scripts evaluate browser-side code snippets (e.g. Playwright helpers)
        window: 'readonly',
      },
    },
    plugins: {
      '@typescript-eslint': typescript,
    },
    rules: {
      ...typescript.configs.recommended.rules,
      // Scripts are allowed to have helpers and any-typed data structures
      '@typescript-eslint/no-unused-vars': 'off',
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/explicit-function-return-type': 'off',
      'no-console': 'off',
    },
  },

  // Repo tooling scripts (scripts/**/*.mjs, tests/*.mjs) — plain Node ESM.
  {
    files: ['scripts/**/*.mjs', 'tests/*.mjs'],
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        console: 'readonly',
        process: 'readonly',
        setTimeout: 'readonly',
        clearTimeout: 'readonly',
        URL: 'readonly',
      },
    },
    rules: {
      'no-console': 'off',
    },
  },

  // Frontend tooling scripts (client/scripts/**/*.mjs)
  // These run in Node, but may use modern Web APIs available in Node (fetch, AbortController, etc).
  {
    files: ['client/scripts/**/*.mjs'],
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        console: 'readonly',
        process: 'readonly',
        setTimeout: 'readonly',
        clearTimeout: 'readonly',
        setInterval: 'readonly',
        clearInterval: 'readonly',
        fetch: 'readonly',
        AbortController: 'readonly',
        structuredClone: 'readonly',
        Promise: 'readonly',
      },
    },
    rules: {
      'no-console': 'off',
    },
  },

  // Tests TypeScript files (tests/**/*.ts)
  {
    files: ['tests/**/*.ts'],
    languageOptions: {
      parser: typescriptParser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        // No project requirement - tests don't need strict type checking
      },
      globals: {
        console: 'readonly',
        process: 'readonly',
        Buffer: 'readonly',
        URL: 'readonly',
        __dirname: 'readonly',
        __filename: 'readonly',
        module: 'readonly',
        require: 'readonly',
        exports: 'readonly',
        global: 'readonly',
        globalThis: 'readonly',
        setTimeout: 'readonly',
        clearTimeout: 'readonly',
        setInterval: 'readonly',
        clearInterval: 'readonly',
        setImmediate: 'readonly',
        clearImmediate: 'readonly',
        Promise: 'readonly',
        NodeJS: 'readonly',
        // Playwright / test globals
        test: 'readonly',
        expect: 'readonly',
        describe: 'readonly',
        it: 'readonly',
        beforeAll: 'readonly',
        afterAll: 'readonly',
        beforeEach: 'readonly',
        afterEach: 'readonly',
        page: 'readonly',
        browser: 'readonly',
        context: 'readonly',
        // Browser-like globals used in tests & helpers
        window: 'readonly',
        document: 'readonly',
        navigator: 'readonly',
        localStorage: 'readonly',
        sessionStorage: 'readonly',
        URLSearchParams: 'readonly',
        CustomEvent: 'readonly',
      },
    },
    plugins: {
      '@typescript-eslint': typescript,
    },
    rules: {
      ...typescript.configs.recommended.rules,
      // Tests are allowed to have helpers and any-typed fixtures
      '@typescript-eslint/no-unused-vars': 'off',
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/explicit-function-return-type': 'off',
      'no-console': 'off',
    },
  },

  // Frontend React files (client/**/*.tsx, client/**/*.ts)
  {
    files: ['client/**/*.{ts,tsx}'],
    languageOptions: {
      parser: typescriptParser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        ecmaFeatures: {
          jsx: true,
        },
      },
      globals: {
        console: 'readonly',
        document: 'readonly',
        window: 'readonly',
        navigator: 'readonly',
        localStorage: 'readonly',
        sessionStorage: 'readonly',
        fetch: 'readonly',
        FormData: 'readonly',
        HTMLElement: 'readonly',
        HTMLDivElement: 'readonly',
        HTMLAudioElement: 'readonly',
        HTMLInputElement: 'readonly',
        HTMLSelectElement: 'readonly',
        HTMLTextAreaElement: 'readonly',
        HTMLImageElement: 'readonly',
        SVGSVGElement: 'readonly',
        DOMParser: 'readonly',
        Element: 'readonly',
        Audio: 'readonly',
        URLSearchParams: 'readonly',
        URL: 'readonly',
        CustomEvent: 'readonly',
        MouseEvent: 'readonly',
        Event: 'readonly',
        DOMException: 'readonly',
        DocumentFragment: 'readonly',
        File: 'readonly',
        FileReader: 'readonly',
        alert: 'readonly',
        requestAnimationFrame: 'readonly',
        cancelAnimationFrame: 'readonly',
        ResizeObserver: 'readonly',
        globalThis: 'readonly',
        RequestInit: 'readonly',
        Response: 'readonly',
        setTimeout: 'readonly',
        clearTimeout: 'readonly',
        setInterval: 'readonly',
        clearInterval: 'readonly',
        Promise: 'readonly',
        __APP_VERSION__: 'readonly', // Defined by Vite
      },
    },
    plugins: {
      '@typescript-eslint': typescript,
      react,
      'react-hooks': reactHooks,
      'react-compiler': reactCompiler,
    },
    rules: {
      ...typescript.configs.recommended.rules,
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      'react/react-in-jsx-scope': 'off', // Not needed in React 17+
      'react/prop-types': 'off', // Using TypeScript
      'react/no-unescaped-entities': 'off', // Allow quotes in text content
      '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/no-unused-expressions': [
        'error',
        { allowShortCircuit: true, allowTernary: true },
      ],
      'no-console': 'off',
      // Disable React Compiler warnings – we still keep core React/TS rules
      'react-compiler/react-compiler': 'off',
    },
    settings: {
      react: {
        version: 'detect',
      },
    },
  },

  // Game integration boundaries (3.0 module split). Core code reaches a game
  // only through integrations/registry and integrations/types; an integration
  // never imports another one or the registry; api/src/core never imports
  // rconService or the MatchZy Enhanced event types by any path. An error everywhere,
  // with no allowlist. See eslint-rules/integration-boundaries.mjs.
  {
    files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
    plugins: {
      local: integrationBoundaries,
    },
    rules: {
      'local/integration-boundaries': 'error',
    },
  },

  // Module SDK boundary (item 8c). A client game module reaches platform code
  // only through client/src/module-sdk and integrations/types, and uses the
  // host's copy of the shared packages. An error, so no new bypass creeps in.
  // See eslint-rules/module-sdk-boundary.mjs.
  {
    files: ['client/src/integrations/**/*.{ts,tsx}'],
    plugins: {
      'module-sdk': moduleSdkBoundary,
    },
    rules: {
      'module-sdk/module-sdk-boundary': 'error',
    },
  },

  // File length. A file over 450 lines of code (blank lines and comments not
  // counted) is too long to keep in your head: split it. Files from before the
  // rule are listed; a PR that splits one takes it off the list.
  {
    files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
    ignores: MAX_LINES_TODO,
    rules: {
      'max-lines': ['error', { max: 450, skipBlankLines: true, skipComments: true }],
    },
  },

  // Typed internals. Values our own code makes or stores carry their real
  // type, so mistakes show up here instead of as runtime checks. Data from
  // outside (a request body, a fetch, a server message, a feed) comes in as
  // `Untrusted` (utils/untrusted) and is checked once where it arrives.
  // A caught error keeps `unknown` (anything can be thrown): in `catch`, in
  // `.catch()` callbacks and in a parameter named error/err/e/reason/cause.
  {
    files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
    rules: {
      'no-restricted-syntax': [
        'error',
        {
          selector:
            'TSUnknownKeyword:not(CatchClause TSUnknownKeyword):not(CallExpression[callee.property.name="catch"] TSUnknownKeyword):not(Identifier[name=/^(e|err|error|reason|cause)$/] > TSTypeAnnotation > TSUnknownKeyword)',
          message:
            'Give it its real type. Outside data is `Untrusted` (utils/untrusted), checked once where it arrives.',
        },
        {
          selector:
            'TSAsExpression[expression.type="TSAsExpression"][expression.typeAnnotation.type="TSUnknownKeyword"]',
          message: '`as unknown as` switches type checking off. Fix the types instead.',
        },
      ],
    },
  },

  // Cleanliness. One rule per object so each can skip its own list of files
  // from before the rule (eslint-rules/cleanliness-todo.mjs); new files follow
  // all of them. max-depth is a warning until we agree a limit.
  ...[
    ['no-nested-ternary', 'error', NESTED_TERNARY],
    ['no-else-return', 'error', ELSE_RETURN],
    ['prefer-const', 'error', PREFER_CONST],
    ['@typescript-eslint/no-non-null-assertion', 'error', NON_NULL_ASSERTION],
    [
      '@typescript-eslint/consistent-type-assertions',
      ['error', { assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }],
      OBJECT_LITERAL_ASSERTION,
    ],
    ['@typescript-eslint/consistent-type-imports', 'error', TYPE_IMPORTS],
  ].map(([rule, setting, ignores]) => ({
    files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
    ignores,
    rules: { [rule]: setting },
  })),
  {
    files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
    rules: { eqeqeq: ['error', 'smart'], 'max-depth': ['warn', 4] },
  },

  // Type-aware (api/src only: the client has no typed linting yet). No
  // floating promises (a fire-and-forget call says so with `void`), no promise
  // where a void callback is expected (Express handlers excepted), and five
  // more that skip their own list of files from before the rule
  // (eslint-rules/typed-lint-todo.mjs).
  {
    files: ['api/src/**/*.ts'],
    rules: {
      '@typescript-eslint/no-floating-promises': 'error',
      '@typescript-eslint/no-misused-promises': [
        'error',
        { checksVoidReturn: { arguments: false } },
      ],
    },
  },
  ...[
    ['@typescript-eslint/switch-exhaustiveness-check', SWITCH_EXHAUSTIVE],
    ['@typescript-eslint/no-unnecessary-type-assertion', UNNECESSARY_ASSERTION],
    ['@typescript-eslint/no-unnecessary-condition', UNNECESSARY_CONDITION],
    ['@typescript-eslint/prefer-nullish-coalescing', NULLISH_COALESCING],
    ['@typescript-eslint/prefer-optional-chain', OPTIONAL_CHAIN],
  ].map(([rule, ignores]) => ({ files: ['api/src/**/*.ts'], ignores, rules: { [rule]: 'error' } })),

  // Ignore patterns
  {
    ignores: [
      'node_modules/**',
      'dist/**',
      'public/**',
      'api/dist/**',
      'api/public/**',
      'client/dist/**',
      'playwright-report/**',
      'logs/**',
      'site/**', // Generated documentation site
      'data/**',
      '.venv/**', // (legacy) Python virtual environment at repo root
      'docs/.venv/**', // Python virtual environment for docs
      '*.js',
      '*.mjs',
      'vite.config.ts',
      'client/vite.config.ts',
      'eslint.config.mjs',
      'client/src/brackets-viewer/**', // Vendored code - ignore linting errors
      // Standalone example projects: their own package.json, lockfile and
      // tsconfig, installed separately and typechecked on their own in CI.
      // Linting them from here would apply this project's type-aware rules to
      // a tsconfig they are not part of.
      'examples/**',
      // The MCP server: its own package, typechecked on its own in CI.
      'mcp/**',
      '.pnp.cjs', // Yarn PnP file
    ],
  },
];