eslint.config.mjs
Rama main · réplica de lectura del repositorio original.
/**
* 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' },
},
);