Ctrl K
ComunidadGuías…

eslint.config.mjs

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

Archivo original ↗Preguntar a KiBot
/**
 * One flat config for the whole workspace.
 *
 * The repo shipped a `lint` script in all 13 packages that was `echo "(eslint
 * not yet wired)"`, and CONTRIBUTING said that was deliberate: "a green check
 * that runs no rules is worse than an absent one". That reasoning was right,
 * and it is why this config exists rather than a token one. Every rule below
 * earns its place by catching something this codebase has actually shipped.
 *
 * Type-aware rules run only where a tsconfig covers the files. They are the
 * expensive half and the valuable half: `no-floating-promises` is the rule for
 * the `void recordSecurityEvent(...)` contract this codebase relies on
 * everywhere, and a missed `await` in a money path is the shape of bug that
 * costs real money.
 */

import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import nextPlugin from '@next/eslint-plugin-next';
import reactPlugin from 'eslint-plugin-react';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import noEmDash from './tools/eslint/no-em-dash.js';

const local = { rules: { 'no-em-dash': noEmDash } };

export default tseslint.config(
  {
    ignores: [
      '**/dist/**',
      '**/.next/**',
      '**/node_modules/**',
      '**/coverage/**',
      'packages/*/dist/**',
      // Static and generated assets in any app's `public/`, including a
      // published OpenAPI document. Written without naming an app: the
      // OSS-strip tripwire refuses any surviving reference to a stripped one.
      '**/public/**',
      // Generated by prisma; not ours to lint.
      'prisma/migrations/**',
      // Generated by Next on every build. Its triple-slash references are the
      // documented shape, and the file is overwritten anyway.
      '**/next-env.d.ts',
    ],
  },

  js.configs.recommended,
  ...tseslint.configs.recommended,

  {
    plugins: { local },
    languageOptions: {
      globals: { ...globals.node, ...globals.es2024 },
    },
    rules: {
      'local/no-em-dash': 'error',

      // Unused code is the cheapest real signal there is. `_`-prefixed is the
      // documented escape hatch, matching how the codebase already writes
      // deliberately-unused parameters.
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
          ignoreRestSiblings: true,
        },
      ],

      // `any` is a warning, not an error: the codebase has legitimate `as never`
      // and `as unknown as X` seams at provider boundaries, and turning those
      // into a wall of errors on day one would mean the whole thing gets
      // disabled. Warnings do not fail CI; `lint-staged` runs with
      // `--max-warnings=0` so a file you touch has to come back clean.
      '@typescript-eslint/no-explicit-any': 'warn',

      // `catch {}` that swallows silently is how a bookkeeping failure becomes
      // invisible. Deliberate ones in this codebase carry a comment saying so;
      // this makes the deliberate ones explicit rather than indistinguishable.
      'no-empty': ['error', { allowEmptyCatch: false }],

      'no-console': ['warn', { allow: ['warn', 'error'] }],

      // Enabled BECAUSE the codebase already carried `eslint-disable` comments
      // naming them. Those suppressions were written against a config that
      // never existed, so each was an assertion nobody could check. Turning the
      // rules on makes every one of them either true or a build error, which is
      // the point of issue #497 rather than deleting the intent someone wrote.
      'no-new': 'error',
      'no-constant-condition': ['error', { checkLoops: false }],
      // Not enabled globally: every hit in this codebase is DEFENSIVE, either a
      // denylist entry naming the scheme or a test fixture asserting it is
      // refused. See the tests override below, which turns it off there.
      'no-script-url': 'error',

      // Warn. Every current hit is a defensive initializer that a later branch
      // unconditionally overwrites (`let ttlSec = 0` before a try/catch that
      // sets it either way). The dead store is real, and rewriting eleven of
      // them across the brute-force limiter, the idempotency middleware and the
      // signature check to satisfy a style rule is risk without a return.
      // New ones are worth seeing; existing ones are not worth touching.
      'no-useless-assignment': 'warn',
      eqeqeq: ['error', 'always', { null: 'ignore' }],
      'no-var': 'error',
      'prefer-const': ['error', { destructuring: 'all' }],
    },
  },

  // Type-aware rules for the API, where the money paths live.
  {
    files: ['apps/api/src/**/*.ts', 'apps/api/test/**/*.ts', 'apps/api/scripts/**/*.ts'],
    languageOptions: {
      parserOptions: {
        project: ['./apps/api/tsconfig.test.json'],
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      // THE rule this codebase needs. `recordSecurityEvent` is fire-and-forget
      // by contract and written `void recordSecurityEvent(...)`; without this,
      // a dropped `void` or a missing `await` on a money write is invisible.
      '@typescript-eslint/no-floating-promises': 'error',
      '@typescript-eslint/await-thenable': 'error',
      // Reported, never auto-fixed. Its fixer removed `as object` from a
      // Prisma write where the assertion was load-bearing for
      // `InputJsonValue` assignability: the checker calls the assertion
      // redundant for the expression's own type while it still drives
      // overload resolution at the call site. Ten typecheck errors came out
      // of one `--fix` pass. Each of these needs a human to confirm the
      // assertion really is dead before it goes.
      '@typescript-eslint/no-unnecessary-type-assertion': 'off',
      // Off. 133 hits, and almost all are `async` for interface conformance:
      // a handler signature the framework awaits, or a service method whose
      // siblings are async. The rule cannot tell those from a genuine mistake,
      // and 133 warnings drown the ones that matter.
      '@typescript-eslint/require-await': 'off',
    },
  },

  // React surfaces.
  {
    // Every app EXCEPT the API, selected by shape rather than by name: the
    // OSS-strip tripwire refuses any surviving reference to a stripped app, so
    // this cannot list them. `apps/api` has no JSX, and the react/next rules
    // only fire on JSX or hooks, so including it costs nothing.
    files: ['apps/*/src/**/*.{ts,tsx}', 'apps/*/test/**/*.{ts,tsx}'],
    // The API is excluded by name, which is safe: it survives the strip, so
    // the tripwire that refuses references to a STRIPPED app does not apply.
    // It has no JSX, and `@next/next` rules like `no-assign-module-variable`
    // fire on plain server code that has nothing to do with Next.
    ignores: ['apps/api/**'],
    plugins: {
      'react-hooks': reactHooks,
      '@next/next': nextPlugin,
      react: reactPlugin,
      'jsx-a11y': jsxA11y,
    },
    settings: { react: { version: 'detect' } },
    languageOptions: {
      globals: { ...globals.browser, ...globals.node },
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      ...nextPlugin.configs.recommended.rules,
      // Next's own image guidance is advisory here: the marketing site
      // deliberately ships plain <img> in places for build-time simplicity.
      '@next/next/no-img-element': 'warn',

      // `react` and `jsx-a11y` are here because the codebase already carried
      // `eslint-disable` comments naming their rules, written against a config
      // that never existed. Those suppressions asserted something nobody could
      // check. Installing the plugins makes each one either true or a build
      // error, which is the whole point of the exercise.
      // Warn, not error. Every current use is JSON-LD (`JSON.stringify` of a
      // schema.org object into a <script type="application/ld+json">), which is
      // the documented way to do it and carries no user input. Erroring on 28
      // legitimate sites is how a rule gets switched off. Its job here is to
      // put a NEW `dangerouslySetInnerHTML` in front of a reviewer.
      'react/no-danger': 'warn',
      'react/jsx-key': 'error',
      'jsx-a11y/alt-text': 'error',
      'jsx-a11y/anchor-has-content': 'error',
      'jsx-a11y/media-has-caption': 'error',
      'jsx-a11y/aria-props': 'error',
      'jsx-a11y/role-has-required-aria-props': 'error',
    },
  },

  // Tests may reach for shapes production does not.
  {
    files: ['**/*.test.ts', '**/*.test.tsx', 'apps/api/test/**/*.ts', 'apps/api/test-providers/**/*.ts'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
      'no-console': 'off',
      // A security test's whole job is to contain the attack string it asserts
      // is refused. Flagging `javascript:alert(1)` in the test that proves it
      // is blocked would mean deleting the proof to satisfy the linter.
      'no-script-url': 'off',
    },
  },

  // Demo/capture scripts contain page.evaluate bodies, which run in the
  // BROWSER even though the file is Node. Without browser globals every
  // `document` and `window` in them reads as undefined.
  {
    files: ['**/scripts/**/*.{js,mjs}'],
    languageOptions: { globals: { ...globals.node, ...globals.browser } },
  },

  // Config and scripts are run by tooling, not shipped.
  {
    files: ['**/*.config.{js,mjs,ts}', '**/scripts/**/*.{js,mjs}', 'tools/**/*.js'],
    rules: { 'no-console': 'off', '@typescript-eslint/no-explicit-any': 'off' },
  },
);