Ctrl K
ComunidadGuías…

eslint.config.mjs

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

Archivo original ↗Preguntar a KiBot
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin';
import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
import reactHooks from 'eslint-plugin-react-hooks';
import tsParser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import';
import { plugin as shadcn } from '@shadcn/lint';
import prettier from 'eslint-plugin-prettier';
import { FlatCompat } from '@eslint/eslintrc';
import jsxA11Y from 'eslint-plugin-jsx-a11y';
import i18next from 'eslint-plugin-i18next';
import react from 'eslint-plugin-react';
import jest from 'eslint-plugin-jest';
import globals from 'globals';
import js from '@eslint/js';
import design from './scripts/eslint/design.mjs';
import chat from './scripts/eslint/chat.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
  baseDirectory: __dirname,
  recommendedConfig: js.configs.recommended,
  allConfig: js.configs.all,
});

const tenantModelRestrictions = [
  {
    selector: "CallExpression[callee.property.name='bulkSave']",
    message:
      'Avoid Model.bulkSave() — it derives writes and delegates to bulkWrite() after running save hooks, but without query middleware to scope the generated write filters. Use create()/insertMany() or tenantSafeBulkWrite() instead.',
  },
  {
    selector: "CallExpression[callee.property.name='watch']",
    message:
      "Avoid Model.watch() — a change stream opens outside query middleware, so the tenant isolation plugin cannot scope it and it emits every tenant's events. A change stream requires a justified inline exemption documenting its system context and explicit tenantId $match guard.",
  },
  {
    selector: "CallExpression[callee.property.name='estimatedDocumentCount']",
    message:
      'Avoid Model.estimatedDocumentCount() — it reads collection metadata and takes no filter, so it always returns the count across every tenant. Use countDocuments() for a tenant-scoped count.',
  },
];

/** Attributes a person reads or hears; a literal in one ships untranslated copy. */
const chatLiteralAttributeNames =
  '/^(title|alt|placeholder|label|aria-label|aria-description|aria-placeholder|aria-roledescription|aria-valuetext)$/';
const chatLiteralAttributes = {
  selector: [
    `JSXAttribute[name.name=${chatLiteralAttributeNames}][value.type='Literal'][value.value=/[A-Za-z]/]`,
    `JSXAttribute[name.name=${chatLiteralAttributeNames}] > JSXExpressionContainer > Literal[value=/[A-Za-z]/]`,
    `JSXAttribute[name.name=${chatLiteralAttributeNames}] > JSXExpressionContainer > TemplateLiteral[expressions.length=0][quasis.0.value.cooked=/[A-Za-z]/]`,
  ].join(', '),
  message: 'Localize user-facing attribute text with useLocalize().',
};

export default [
  {
    ignores: [
      'vendor/code/**/*',
      'client/dist/**/*',
      'client/public/**/*',
      'client/coverage/**/*',
      'e2e/playwright-report/**/*',
      'packages/api/types/**/*',
      'packages/api/dist/**/*',
      'packages/api/test_bundle/**/*',
      'api/demo/**/*',
      'packages/client/dist/**/*',
      'packages/chat/dist/**/*',
      'packages/data-provider/types/**/*',
      'packages/data-provider/dist/**/*',
      'packages/data-provider/test_bundle/**/*',
      'packages/data-schemas/dist/**/*',
      'data-node/**/*',
      'meili_data/**/*',
      '**/node_modules/**/*',
      'venv/**/*',
      '.devcontainer/**/*',
    ],
  },
  ...fixupConfigRules(
    compat.extends(
      'eslint:recommended',
      'plugin:react/recommended',
      'plugin:react-hooks/recommended',
      'plugin:jest/recommended',
      'prettier',
      'plugin:jsx-a11y/recommended',
    ),
  ),
  {
    plugins: {
      react: fixupPluginRules(react),
      'react-hooks': fixupPluginRules(reactHooks),
      '@typescript-eslint': typescriptEslintEslintPlugin,
      import: importPlugin,
      'jsx-a11y': fixupPluginRules(jsxA11Y),
      'import/parsers': tsParser,
      i18next,
      prettier: fixupPluginRules(prettier),
    },

    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.node,
        ...globals.commonjs,
      },
      parser: tsParser,
      ecmaVersion: 'latest',
      sourceType: 'module',
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
    },

    settings: {
      react: {
        createClass: 'createReactClass',
        pragma: 'React',
        fragment: 'Fragment',
        version: 'detect',
      },
      'import/parsers': {
        '@typescript-eslint/parser': ['.ts', '.tsx'],
      },
      'import/resolver': {
        typescript: {
          project: ['./client/tsconfig.json'],
        },
        node: {
          project: ['./client/tsconfig.json'],
        },
      },
    },

    rules: {
      'prettier/prettier': 'error',
      'react/react-in-jsx-scope': 'off',

      '@typescript-eslint/ban-ts-comment': [
        'error',
        {
          'ts-ignore': false,
        },
      ],
      // Disable a11y features to be enabled later on.
      'jsx-a11y/no-static-element-interactions': 'off',
      'jsx-a11y/click-events-have-key-events': 'off',
      'jsx-a11y/alt-text': 'off',
      'jsx-a11y/img-redundant-alt': 'off',
      'jsx-a11y/no-noninteractive-tabindex': 'off',
      // common rules
      'no-nested-ternary': 'error',
      'no-constant-binary-expression': 'warn',
      'no-unused-vars': [
        'warn',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
        },
      ],
      'no-console': 'off',
      // Import cycles are checked by config/circular-deps.mjs over the bundler graph;
      // `import/no-cycle` re-walked that graph from every file (80% of a full-tree lint).
      'import/no-self-import': 'error',
      'import/extensions': 'off',
      'no-promise-executor-return': 'off',
      'no-param-reassign': 'off',
      'no-continue': 'off',
      'no-restricted-syntax': 'off',
      'react/prop-types': 'off',
      'react/display-name': 'off',
    },
  },
  {
    files: ['api/**/*.js', 'config/**/*.js'],
    rules: {
      // API
      'no-async-promise-executor': 'off',
    },
  },
  {
    files: [
      'client/src/**/*.tsx',
      'client/src/**/*.ts',
      'client/src/**/*.jsx',
      'client/src/**/*.js',
    ],
    rules: {
      // Client a11y
      // TODO: maybe later to error.
      'jsx-a11y/no-noninteractive-element-interactions': 'off',
      'jsx-a11y/label-has-associated-control': 'off',
      'jsx-a11y/no-static-element-interactions': 'off',
      'jsx-a11y/click-events-have-key-events': 'off',
      'jsx-a11y/interactive-supports-focus': 'off',
      'jsx-a11y/no-noninteractive-tabindex': 'off',
      'jsx-a11y/img-redundant-alt': 'off',
    },
  },
  // @shadcn/lint: design-system enforcement for the two client surfaces. The linter treats
  // `@librechat/client` — plus the app-local `~/components/ui` re-exports — as the design
  // system, reads each primitive's cva variants, and reports a className that overrides what
  // the primitive owns, naming the variants and sizes to use instead. The tree's existing
  // violations are recorded in eslint-suppressions.json, so these rules gate new and edited
  // code without a tree-wide migration; see CLAUDE.md, "Theming and styling".
  //
  // The client's entry points and helpers are `.jsx`/`.js` — App.jsx among them — so the globs
  // name those extensions too: the rules have to see them.
  {
    files: [
      'client/src/**/*.{ts,tsx,js,jsx}',
      'packages/client/src/**/*.{ts,tsx,js,jsx}',
      'packages/chat/src/**/*.{ts,tsx,js,jsx}',
    ],
    plugins: { shadcn, design },
    settings: {
      shadcn: {
        ui: '@librechat/client',
        componentImports: ['^~/components/ui(/|$)'],
        note: 'See CLAUDE.md, "Theming and styling".',
      },
    },
    rules: {
      'shadcn/no-restyle': [
        'error',
        {
          // `icon-*` is a sizing utility from client/src/style.css (height, width, stroke-width),
          // so it belongs with layout rather than with a primitive's own appearance.
          allow: ['layout', 'icon-*'],
          // The lint names a component after its declaration in `packages/client/dist`, where the
          // bundler suffixes a colliding name (`DialogContent$1`, from the two dialog families), so
          // every pattern accepts that suffix.
          // A contract replaces `allow` rather than extending it, so each one restates the
          // baseline. These record policy, not debt: the categories below are the caller's to
          // set, which is why they are not in eslint-suppressions.json.
          contracts: [
            // A text primitive renders the caller's text, so the caller owns its size, weight
            // and leading. Color is still the theme's: it stays reported here.
            {
              pattern: '^(Label|Description|DialogTitle|DialogDescription|SeriesLabel)(\\$\\d+)?$',
              allow: ['layout', 'icon-*', 'typography'],
            },
            // A skeleton stands in for the caller's content, so it takes that content's
            // silhouette and footprint.
            { pattern: '^Skeleton(\\$\\d+)?$', allow: ['layout', 'icon-*', 'shape', 'spacing'] },
            // A dialog or panel container lays out whatever the caller puts in it, so the padding
            // and gaps between those parts are the caller's. Color and shape stay the theme's.
            {
              pattern: '^(DialogContent|DialogHeader|DialogFooter|PanelContent)(\\$\\d+)?$',
              allow: ['layout', 'icon-*', 'spacing'],
            },
          ],
        },
      ],
      'shadcn/no-raw-colors': 'error',
      'shadcn/no-arbitrary-values': ['error', { allow: ['layout'] }],
      'shadcn/no-inline-styles': [
        'error',
        {
          // Geometry that carries a measured or animated number — a virtual row's height, a
          // floating panel's offset, a drag transform — has no class form. Everything else
          // (color, display, transition, spacing) does, and stays reported.
          allow: [
            'width',
            'height',
            'minWidth',
            'minHeight',
            'maxWidth',
            'maxHeight',
            'top',
            'right',
            'bottom',
            'left',
            'transform',
            'transformOrigin',
            'zIndex',
          ],
        },
      ],
      'shadcn/require-static-classes': 'error',
      // A disabled control that fades through `opacity-*` also has to compose a shared disabled
      // recipe, or a theme with `disabledStyle: fill` cannot paint it (scripts/eslint/design.mjs).
      'design/disabled-recipe': [
        'error',
        {
          primitives: ['Button', 'IconButton', 'Input', 'Textarea', 'Checkbox', 'Switch'],
          sources: ['@librechat/client'],
        },
      ],
      // Now answerable: the rule asks the installed Tailwind whether a class generates CSS, and
      // the app is on v4. Classes declared in a stylesheet Tailwind reads are recognized on their
      // own; these are the ones it cannot see, plain selectors in files loaded separately
      // (style.css families, the library's component CSS) and classes a third party puts in the
      // DOM. Everything outside this list that generates no CSS is reported: the `prose` variants
      // that quietly render nothing today, and the `token-`-prefixed names, which were painting
      // through plain rules in `client/src/style.css` that this change removes in favour of the
      // tokens themselves rather than adding six more names here.
      'shadcn/no-unknown-classes': [
        'error',
        {
          allow: [
            // client/src/style.css and the library's component CSS
            'icon-*',
            'hover-button',
            'toast-root',
            'alert-root',
            'tooltip',
            'spinner',
            'popover-ui',
            'select-item',
            'reset-rc-number-input',
            'reset-rc-number-input-text-right',
            'assistant-item',
            'animated-tab',
            'animated-tab-list',
            'animated-tab-panel',
            'animated-panels',
            'animate-popover',
            'animate-popover-bottom',
            'animate-pulse-slow',
            'animate-gradient-x',
            'animate-fadeIn',
            'slow-pulse',
            'hide-scrollbar',
            'scrollbar-gutter-spacer',
            'progress-text-wrapper',
            'progress-text-content',
            'active',
            // put in the DOM by a dependency, not by Tailwind
            'lucide',
            'lucide-*',
            'language-*',
            'i-heroicons-*',
            'form-check-label',
            // Markers a selector reads rather than Tailwind styling: each one is queried by a
            // stylesheet, a component, or an e2e spec, so it carries no CSS of its own.
            'popover',
            'user-turn',
            'agent-turn',
            'final-completion',
            'sibling-content-group',
            'scroll-animation',
            'hover-button-active',
            'open',
            'split-parent',
            // Message-text markers the app's stylesheet paints; a @librechat/chat source cannot see
            // that stylesheet, so its lint has no Tailwind entry that declares them.
            'markdown',
            'prose',
            'prose-sm',
            'message-content',
            'submitting',
            'result-streaming',
            'result-thinking',
            'shimmer',
            'tool-status-text',
            'message-render',
            'message-timestamp',
            'popover-animate',
            // The thinking-level slider's spring curves, which have no theme token.
            'composer-effort-glide',
            'composer-effort-thumb',
            'composer-effort-fill',
            // The thinking button's label roll and the composer popover clock it resizes on.
            'composer-slot-resize',
            'composer-label-in-up',
            'composer-label-in-down',
            'composer-label-out-up',
            'composer-label-out-down',
            'animate-composer-icon',
            'animate-composer-popover',
          ],
        },
      ],
    },
  },
  {
    // A spec asserts the class strings a control renders; it composes nothing a theme paints.
    files: [
      'client/src/**/*.{spec,test}.{ts,tsx,js,jsx}',
      'client/src/**/__tests__/**/*.{ts,tsx,js,jsx}',
      'packages/client/src/**/*.{spec,test}.{ts,tsx,js,jsx}',
      'packages/client/src/**/__tests__/**/*.{ts,tsx,js,jsx}',
      'packages/chat/src/**/*.{spec,test}.{ts,tsx,js,jsx}',
      'packages/chat/src/**/__tests__/**/*.{ts,tsx,js,jsx}',
    ],
    rules: {
      'design/disabled-recipe': 'off',
    },
  },
  {
    // A primitive owns its own internals, so the rules that police callers are off inside the
    // component library. `no-raw-colors` and `no-inline-styles` stay on: the primitives are
    // where theme tokens matter most. `client/src/components/ui` is deliberately not here:
    // `componentImports` marks it as a place primitives are imported from, but what it holds
    // are app composites — a dialog, a collapse, a date-range picker — and their overrides of a
    // shared primitive are exactly what `no-restyle` exists to report.
    files: ['packages/client/src/**/*.{ts,tsx,js,jsx}'],
    rules: {
      'shadcn/no-restyle': 'off',
      'shadcn/no-arbitrary-values': 'off',
      'shadcn/require-static-classes': 'off',
    },
  },
  {
    // Entries 2 and 4 of packages/client/src/theme/allowlist.md: the third-party brand marks and
    // the opt-in multicolour illustration carry fixed `fill` literals that must not follow the
    // theme. The scope is these six files by name, so a new SVG or any other file still reports
    // its colours; the allowlist stays the record of why each one is here.
    files: [
      'packages/client/src/svgs/GoogleIcon.tsx',
      'packages/client/src/svgs/FacebookIcon.tsx',
      'packages/client/src/svgs/DiscordIcon.tsx',
      'packages/client/src/svgs/GeminiIcon.tsx',
      'packages/client/src/svgs/PaLMIcon.tsx',
      'packages/client/src/svgs/BirthdayIcon.tsx',
    ],
    rules: {
      'shadcn/no-raw-colors': 'off',
    },
  },
  {
    files: ['**/.eslintrc.js', '**/jest.config.js', 'client/vite.config.ts'],
    languageOptions: {
      globals: {
        ...globals.node,
      },
    },
  },
  {
    files: [
      '**/*.test.js',
      '**/*.test.jsx',
      '**/*.test.ts',
      '**/*.test.tsx',
      '**/*.spec.js',
      '**/*.spec.jsx',
      '**/*.spec.ts',
      '**/*.spec.tsx',
      '**/setupTests.js',
    ],
    settings: {
      jest: {
        globalAliases: {
          describe: ['describeIfFerretDB', 'describeLive'],
          it: ['itIfFerretDB'],
        },
      },
    },
    languageOptions: {
      globals: {
        ...globals.jest,
        ...globals.node,
      },
    },
    rules: {
      // TEST
      'jest/no-standalone-expect': ['error', { additionalTestBlockFunctions: ['itIfFerretDB'] }],
      'react/display-name': 'off',
      'react/prop-types': 'off',
      'jest/no-commented-out-tests': 'off',
      'react/no-unescaped-entities': 'off',
      'jest/no-conditional-expect': 'off',
      'jest/no-disabled-tests': 'off',
      '@typescript-eslint/no-unused-vars': 'off',
      // A spec's fixture markup is an assertion, not a design surface.
      'shadcn/no-restyle': 'off',
      'shadcn/no-raw-colors': 'off',
      'shadcn/no-arbitrary-values': 'off',
      'shadcn/no-inline-styles': 'off',
      'shadcn/require-static-classes': 'off',
      'shadcn/no-unknown-classes': 'off',
    },
  },
  ...compat
    .extends(
      'plugin:@typescript-eslint/eslint-recommended',
      'plugin:@typescript-eslint/recommended',
    )
    .map((config) => ({
      ...config,
      files: ['**/*.ts', '**/*.tsx'],
    })),
  {
    files: ['**/*.ts', '**/*.tsx'],
    // Package, E2E, and root integration tests are not part of the client
    // TypeScript project. They still get the recommended rules above.
    ignores: ['packages/**/*', 'client/vite.config.ts', 'e2e/**/*'],
    plugins: {
      '@typescript-eslint': typescriptEslintEslintPlugin,
      jest: fixupPluginRules(jest),
    },
    languageOptions: {
      parser: tsParser,
      ecmaVersion: 5,
      sourceType: 'script',
    },
    rules: {
      // i18n
      'i18next/no-literal-string': [
        'error',
        {
          mode: 'jsx-text-only',
          'should-validate-template': true,
        },
      ],
      //
      'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
      '@typescript-eslint/no-unused-expressions': 'off',
      '@typescript-eslint/no-unused-vars': [
        'warn',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
        },
      ],
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/ban-ts-comment': 'off',
      // React
      'react/no-unknown-property': 'warn',
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',
      // General
      'no-constant-binary-expression': 'off',
    },
  },
  {
    // **Data-provider specific configuration block**
    files: ['./packages/data-provider/**/*.ts'],
    languageOptions: {
      parser: tsParser,
      ecmaVersion: 'latest',
      sourceType: 'module',
    },
    rules: {
      '@typescript-eslint/no-unused-vars': [
        'warn',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
        },
      ],
    },
  },
  {
    files: ['./api/demo/**/*.ts'],
  },
  {
    files: ['./packages/api/**/*.ts'],
    rules: {
      'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
      '@typescript-eslint/no-unused-vars': [
        'warn',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
          destructuredArrayIgnorePattern: '^_',
        },
      ],
    },
  },
  {
    files: ['./packages/data-provider/specs/**/*.ts'],
    languageOptions: {
      ecmaVersion: 5,
      sourceType: 'script',
    },
  },
  {
    files: ['./api/demo/specs/**/*.ts'],
    languageOptions: {
      ecmaVersion: 5,
      sourceType: 'script',
    },
  },
  {
    files: ['./packages/api/specs/**/*.ts'],
    languageOptions: {
      ecmaVersion: 5,
      sourceType: 'script',
    },
  },
  {
    // **Data-schemas — shared rules for all TS files**
    files: ['./packages/data-schemas/**/*.ts'],
    languageOptions: {
      parser: tsParser,
      ecmaVersion: 'latest',
      sourceType: 'module',
      parserOptions: {
        project: [
          './packages/data-schemas/tsconfig.json',
          './packages/data-schemas/misc/ferretdb/tsconfig.json',
          './packages/data-schemas/misc/documentdb/tsconfig.json',
        ],
      },
    },
    rules: {
      '@typescript-eslint/no-unused-vars': [
        'warn',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
          destructuredArrayIgnorePattern: '^_',
        },
      ],
    },
  },
  {
    files: ['packages/data-schemas/**/*.ts', 'packages/api/**/*.{ts,js}', 'api/**/*.{ts,js}'],
    ignores: ['**/*.spec.{ts,js}', '**/*.test.{ts,js}'],
    rules: {
      'no-restricted-syntax': ['error', ...tenantModelRestrictions],
    },
  },
  {
    // **Data-schemas — ban model APIs that bypass tenant isolation in production code**
    // Raw driver calls bypass the plugin; bulkSave also bypasses query filter scoping.
    // Tests and the tenantSafeBulkWrite wrapper itself are excluded.
    files: ['./packages/data-schemas/**/*.ts'],
    ignores: ['**/*.spec.ts', '**/*.test.ts', '**/utils/tenantBulkWrite.ts'],
    rules: {
      'no-restricted-syntax': [
        'error',
        {
          selector: "CallExpression[callee.property.name='bulkWrite']",
          message:
            'Use tenantSafeBulkWrite() instead of Model.bulkWrite() — Mongoose middleware does not fire for bulkWrite, so the tenant isolation plugin cannot intercept it.',
        },
        {
          selector: "MemberExpression[property.name='collection'][parent.type='MemberExpression']",
          message:
            'Avoid Model.collection.* — raw driver calls bypass all Mongoose middleware including tenant isolation. Use Mongoose model methods or tenantSafeBulkWrite() instead.',
        },
        ...tenantModelRestrictions,
      ],
    },
  },
  {
    // What a chat source may load, judged on the resolved target however the specifier is
    // spelled (scripts/eslint/chat.mjs): nothing from the app, and no UI code in the core.
    files: ['packages/chat/src/**/*.{ts,tsx,js,jsx}'],
    plugins: { chat },
    rules: {
      'chat/boundary': [
        'error',
        { sourceRoot: path.join(__dirname, 'packages/chat/src'), packageName: '@librechat/chat' },
      ],
      // The code moving in from `client/src` keeps the app's convention for intentionally unused names.
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
        },
      ],
    },
  },
  {
    // The app's literal-copy rule skips `packages/**`; chat components render visible text, so it
    // reaches them. Specs assert rendered strings and stay exempt, as they are in the app.
    files: ['packages/chat/src/**/*.{tsx,jsx}'],
    ignores: ['**/*.{spec,test}.{tsx,jsx}', '**/__tests__/**'],
    rules: {
      'i18next/no-literal-string': [
        'error',
        {
          mode: 'jsx-text-only',
          'should-validate-template': true,
        },
      ],
      'no-restricted-syntax': ['error', chatLiteralAttributes],
    },
  },
  {
    files: ['packages/chat/src/index.ts', 'packages/chat/src/core/**/*.{ts,tsx,js,jsx}'],
    ignores: ['**/*.{spec,test}.{ts,tsx,js,jsx}'],
    rules: {
      'no-restricted-syntax': [
        'error',
        {
          selector: 'JSXElement, JSXFragment',
          message:
            'The @librechat/chat core has no UI dependency; JSX compiles to react/jsx-runtime.',
        },
      ],
    },
  },
];