Trovella Wiki

ESLint Configuration

The shared ESLint flat config -- TypeScript strict rules, naming conventions, complexity limits, import sorting, code smell detection, and SDK boundary enforcement.

Trovella uses ESLint 9 with the flat config format. All configuration lives in @repo/eslint-config, and each package in the monorepo spreads one of the shared presets into its own eslint.config.js.

Preset Architecture

The base config (packages/config-eslint/base.js) contains every rule. The library and Next.js presets extend it:

base.js          <- core rules, all plugins
  library.js     <- re-exports base (reserved for future library-specific rules)
  next.js        <- base + eslint-config-next

Every preset ends with eslint-config-prettier to disable formatting rules that conflict with Prettier.

Plugins

PluginPurpose
typescript-eslint (strict type-checked)Type-aware TypeScript rules
eslint-plugin-simple-import-sortDeterministic import/export ordering
eslint-plugin-sonarjsCode smell detection (cherry-picked rules)
eslint-plugin-jsdocRequire JSDoc on exported functions/classes/types
eslint-plugin-tsdocValidate TSDoc syntax in doc comments
eslint-plugin-unicornExpiring TODO enforcement

TypeScript Strict Rules

These rules target common AI-generated code quality issues:

RuleSettingWhy
no-unused-varsError (ignore _ prefix)Prevents dead parameters and variables
no-explicit-anyErrorForces proper typing; any defeats the type system
no-floating-promisesErrorCatches unhandled async operations
no-misused-promisesErrorPrevents passing promises where sync values are expected
consistent-type-importsError, inline-type-importsEnsures import { type Foo } syntax for type-only imports
only-throw-errorErrorOnly Error subclasses (TRPCError, AppError) may be thrown

The base config extends tseslint.configs.strictTypeChecked, which enables the full set of type-aware rules beyond the ones listed above.

Naming Conventions

The @typescript-eslint/naming-convention rule enforces consistent casing across the codebase:

SelectorAllowed formatsNotes
DefaultcamelCaseBaseline for all identifiers
VariablescamelCase, PascalCase, UPPER_CASEPascalCase for React components, UPPER_CASE for constants
Destructured variablesAnyExempt -- external APIs and env vars use varied casing
__ prefix variablesAnyNode.js shims like __dirname
FunctionscamelCase, PascalCasePascalCase for React components
Exported functions matching GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONSUPPER_CASENext.js HTTP route handlers
ParameterscamelCase, PascalCaseLeading _ allowed for unused params
Object literal propertiesAnyExempt -- Drizzle schemas, API payloads, config objects
Type propertiescamelCase, snake_casesnake_case for external API boundary types (Anthropic, Typesense)
Quoted propertiesAnyExempt -- required by the runtime
Type-like (types, interfaces, classes, enums)PascalCaseStandard TypeScript convention
Type parametersPascalCase with T prefixTResult, TItem, not Result or T
ImportscamelCase, PascalCasePascalCase for React components and classes

Complexity Limits

RuleLimitScope
max-lines400 (skip blanks/comments)File
max-lines-per-function100 (skip blanks/comments)Function
complexity (cyclomatic)20Function
sonarjs/cognitive-complexity20Function
max-depth4Nesting
max-params4Function signature

Relaxed Files

Complexity and size limits are turned off for files that are legitimately large:

  • **/seeds/** -- seed data files
  • **/migrations/** -- generated migration SQL
  • **/schema/** -- Drizzle schema definitions
  • **/__tests__/**, **/*.test.{ts,tsx}, **/*.spec.{ts,tsx} -- test files

The apps/web config also relaxes limits for src/components/ui/** (shadcn/ui components are generated, not hand-written).

Import Sorting

The simple-import-sort/imports rule enforces a fixed group order:

  1. Side-effect imports (import "./styles.css")
  2. React and Next.js (react, react-dom, next/*)
  3. External npm packages (@tanstack/*, zod, etc.)
  4. Internal monorepo packages (@repo/db, @repo/api, etc.)
  5. Relative imports (./, ../)
  6. CSS imports (*.css)

Both import and export sorting are set to error and are auto-fixable.

Code Smell Detection (SonarJS)

Rather than enabling the full sonarjs/recommended preset, the config cherry-picks specific rules:

RuleWhat it catches
cognitive-complexityFunctions that are hard to understand (redundant with the complexity limit, but measures differently)
no-identical-functionsCopy-pasted function bodies
no-collapsible-ifNested if statements that can be merged
no-duplicated-branchesif/else branches with identical code
no-all-duplicated-branchesAll branches of a conditional are identical
no-identical-expressionsa === a or x && x patterns
prefer-single-boolean-returnif (x) return true; return false; instead of return x;
no-redundant-jumpcontinue, return, or break that serve no purpose
no-nested-conditionalTernary inside ternary or deeply nested conditionals

TODO Expiration

The unicorn/expiring-todo-comments rule requires every TODO, FIXME, or HACK comment to include either a date or ticket reference:

// TODO [2026-05-01]: Migrate to streaming API
// FIXME [TRO-45]: Race condition in cache invalidation
// HACK [TRO-99]: Workaround for upstream bug in typesense client

Comments without a date or ticket reference cause a lint error. On pull requests, date-based expiry is ignored (the ignoreDatesOnPullRequests option) to avoid blocking PRs that happen to land on an expiry date.

Documentation Requirements

Two rules enforce documentation standards:

jsdoc/require-jsdoc (error) -- requires JSDoc on:

  • Exported function declarations (3+ lines)
  • Exported class declarations (3+ lines)
  • Exported type aliases and interfaces

This rule uses publicOnly: true, so internal (non-exported) code does not need JSDoc.

tsdoc/syntax (warn) -- validates that doc comments use valid TSDoc syntax. Set to warn rather than error to allow gradual adoption of advanced TSDoc tags.

General Rules

RuleSettingWhy
no-consoleErrorAll logging goes through @repo/logger; console.log is banned
prefer-constErrorImmutable by default
no-varErrorlet and const only
eqeqeqError (always)No loose equality (==); only strict (===)

SDK Boundary Enforcement

The restrictedImports() function in packages/config-eslint/restrictions.js generates no-restricted-imports rules that enforce the package boundary architecture. Each external SDK may only be imported from its designated wrapper package:

SDKAllowed inWrapper purpose
@anthropic-ai/sdk@repo/aiAI client, usage tracking, model selection
@google/genai@repo/aiGoogle AI models (Gemini)
better-auth@repo/auth, @repo/webAuthentication server/client
better-auth/client@repo/auth, @repo/webAuth client utilities
better-auth/react@repo/webReact hooks for auth state
drizzle-orm/pg-core@repo/dbSchema definitions
drizzle-orm/node-postgres@repo/dbDatabase driver
drizzle-orm/node-postgres/migrator@repo/dbMigration runner
@upstash/redis@repo/cacheRedis client
pino@repo/loggerStructured logging
typesense@repo/searchSearch engine client

How It Works

Each package's eslint.config.js calls restrictedImports("@repo/package-name"). The function returns a deny list for every SDK that the calling package is not in the allowed list for:

// packages/api/eslint.config.js
import library from "@repo/eslint-config/library";
import { restrictedImports } from "@repo/eslint-config/restrictions";

export default [
  ...library,
  {
    files: ["**/*.{ts,tsx}"],
    rules: {
      "no-restricted-imports": ["error", { paths: restrictedImports("@repo/api") }],
    },
  },
];

Because @repo/api is not in the allowed list for any SDK, it gets deny rules for all of them. If a developer tries to import { Anthropic } from "@anthropic-ai/sdk" inside @repo/api, ESLint reports:

Import @anthropic-ai/sdk only through @repo/ai. Direct usage is banned outside the wrapper package.

Router-Specific Restrictions

The @repo/api config adds extra restrictions for router files (src/routers/**/*.ts). These prevent routers from bypassing the middleware context:

Banned importWhyUse instead
db from @repo/dbBare db skips RLS tenant scopingctx.db (RLS-scoped)
Anything from @repo/loggerRouters should use the request-scoped loggerctx.logger
createAIHelper from @repo/aiRouters should use the pre-bound helperctx.ai

Web App Restrictions

The apps/web config bans barrel imports from features:

patterns: [
  {
    group: ["~/features/*/index", "~/features/*/index.*"],
    message: "Import directly from the file, not the feature barrel. See TRO-93.",
  },
];

This prevents circular dependency issues that arise from feature barrel re-exports.

Per-Package Config Pattern

Every package follows the same pattern in its eslint.config.js:

  1. Spread the appropriate preset (library or next)
  2. Add no-restricted-imports via restrictedImports("@repo/package-name")
  3. Optionally add package-specific rule overrides
// Minimal example: packages/db/eslint.config.js
import library from "@repo/eslint-config/library";
import { restrictedImports } from "@repo/eslint-config/restrictions";

export default [
  ...library,
  {
    files: ["**/*.{ts,tsx}"],
    rules: {
      "no-restricted-imports": ["error", { paths: restrictedImports("@repo/db") }],
    },
  },
];

Adding a New SDK Boundary

When introducing a new external service SDK:

  1. Create or identify the wrapper package (e.g., @repo/email for Resend)
  2. Add the SDK to the SDK_BOUNDARIES array in packages/config-eslint/restrictions.js
  3. Run pnpm lint to verify zero violations across the monorepo
  4. Update the layer hierarchy in .dependency-cruiserrc.mjs if needed (see Delivery -- Architecture Enforcement)

On this page