> For the complete documentation index, see [llms.txt](https://gotts.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gotts.gitbook.io/docs/monorepo-infrastructure/monorepo/05-lint-format.md).

# Lint and Format

> **Part of**: [Monorepo Infrastructure PRD](/docs/monorepo-infrastructure/monorepo.md) | **Last Updated**: 2026-02-19

***

## ESLint 10 — Flat Config

ESLint 10 removes support for the legacy `eslintrc` format. All configuration uses the flat config format (`eslint.config.ts`).

### Shared Config Package

`@gotts.ai/eslint-config` provides composable presets for different package types.

#### `packages/eslint-config/package.json` (normative)

```jsonc
{
  "name": "@gotts.ai/eslint-config",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "default": "./src/index.ts",
    },
    "./base": {
      "types": "./src/base.ts",
      "default": "./src/base.ts",
    },
    "./library": {
      "types": "./src/library.ts",
      "default": "./src/library.ts",
    },
    "./nextjs": {
      "types": "./src/nextjs.ts",
      "default": "./src/nextjs.ts",
    },
  },
  "dependencies": {
    "@eslint/js": "^10.0.0",
    "eslint-plugin-perfectionist": "^4.0.0",
    "eslint-plugin-turbo": "^2.0.0",
    "typescript-eslint": "^8.0.0",
  },
  "peerDependencies": {
    "eslint": "^10.0.0",
    "typescript": "^5.8.0",
  },
}
```

> **Internal package pattern.** This package uses `exports` pointing to TypeScript source. No build step. ESLint resolves the config via the bundler-compatible `default` condition.

#### `src/base.ts` (normative)

```typescript
import eslint from "@eslint/js";
import perfectionist from "eslint-plugin-perfectionist";
import turboPlugin from "eslint-plugin-turbo";
import tseslint from "typescript-eslint";
import type { Linter } from "eslint";

export const base: Linter.Config[] = [
  eslint.configs.recommended,
  ...tseslint.configs.strictTypeChecked,
  ...tseslint.configs.stylisticTypeChecked,
  {
    plugins: {
      perfectionist,
      turbo: turboPlugin,
    },
    rules: {
      // Import sorting via perfectionist
      "perfectionist/sort-imports": [
        "error",
        {
          type: "natural",
          groups: [
            "builtin",
            "external",
            "internal",
            "parent",
            "sibling",
            "index",
            "type",
          ],
          newlinesBetween: "always",
        },
      ],
      "perfectionist/sort-named-exports": ["error", { type: "natural" }],
      "perfectionist/sort-named-imports": ["error", { type: "natural" }],

      // Turbo env vars
      "turbo/no-undeclared-env-vars": "warn",

      // Strict type safety
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/no-unused-vars": [
        "error",
        { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
      ],
      "@typescript-eslint/consistent-type-imports": [
        "error",
        { prefer: "type-imports", fixStyle: "separate-type-imports" },
      ],

      // Prefer modern patterns
      "prefer-const": "error",
      "no-var": "error",
    },
  },
];
```

#### `src/library.ts` (normative)

```typescript
import { base } from "./base.js";
import type { Linter } from "eslint";

export const library: Linter.Config[] = [
  ...base,
  {
    rules: {
      // Libraries should have explicit return types on exports
      "@typescript-eslint/explicit-function-return-type": [
        "warn",
        { allowExpressions: true },
      ],
    },
  },
];
```

#### `src/nextjs.ts` (normative)

```typescript
import { base } from "./base.js";
import type { Linter } from "eslint";

// Next.js ESLint config
// Note: @next/eslint-plugin-next handles Next.js-specific rules
// This config extends base with React-specific adjustments
export const nextjs: Linter.Config[] = [
  ...base,
  {
    settings: {
      react: {
        version: "detect",
      },
    },
    rules: {
      // Relax strict type checking for React component patterns
      "@typescript-eslint/no-misused-promises": [
        "error",
        { checksVoidReturn: { attributes: false } },
      ],
    },
  },
];
```

#### `src/index.ts` (normative)

```typescript
export { base } from "./base.js";
export { library } from "./library.js";
export { nextjs } from "./nextjs.js";
```

### Root `eslint.config.ts` (normative)

The root config is minimal — it applies the base config to root-level files:

```typescript
import { base } from "@gotts.ai/eslint-config";

export default [
  ...base,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
  {
    ignores: [
      "dist/",
      "node_modules/",
      ".turbo/",
      ".next/",
      "coverage/",
      "**/*.config.{ts,js,mjs}",
    ],
  },
];
```

### Per-Package `eslint.config.ts`

Each package has a minimal config that extends the shared preset:

**Published library:**

```typescript
import { library } from "@gotts.ai/eslint-config";

export default [
  ...library,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
];
```

**Next.js app:**

```typescript
import { nextjs } from "@gotts.ai/eslint-config";

export default [
  ...nextjs,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
];
```

### Key Decisions

**Why `perfectionist` over `eslint-plugin-import`?**

`eslint-plugin-import` has known performance issues with TypeScript and has been slow to adopt flat config. `eslint-plugin-perfectionist` provides deterministic import sorting with zero performance overhead and full flat config support.

**Why `projectService: true`?**

ESLint 10 + `typescript-eslint` 8+ supports the TypeScript project service for automatic tsconfig detection. This replaces the old `project: true` option and is faster.

**Why no `eslint-plugin-react`?**

Next.js 15+ includes `@next/eslint-plugin-next` which handles React-specific rules. Adding `eslint-plugin-react` on top creates duplicate and sometimes conflicting rules.

***

## Prettier 3.5+

Prettier handles all code formatting. ESLint does **not** handle formatting — the two tools have separate responsibilities.

### `.prettierrc` (normative)

```jsonc
{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf",
  "plugins": ["prettier-plugin-tailwindcss"],
}
```

### `.prettierignore` (normative)

```
dist
node_modules
.turbo
.next
pnpm-lock.yaml
coverage
*.sol
```

> **Solidity excluded.** Solidity formatting is handled by `forge fmt` (Foundry), not Prettier.

### Running Prettier

```bash
# Format all files
pnpm format

# Check formatting (CI)
pnpm format:check
```

***

## Pre-Commit Hooks

### Husky 9.x

[Husky](https://typicode.github.io/husky/) runs Git hooks.

#### Setup

```bash
pnpm add -Dw husky
pnpm exec husky init
```

The `prepare` script in root `package.json` (`"prepare": "husky"`) ensures Husky is installed after `pnpm install`.

#### `.husky/pre-commit` (normative)

```bash
pnpm exec lint-staged
```

#### `.husky/commit-msg` (normative)

```bash
pnpm exec commitlint --edit $1
```

### lint-staged 16.x

[lint-staged](https://github.com/lint-staged/lint-staged) runs linters on staged files only, keeping pre-commit hooks fast.

#### `lint-staged.config.mjs` (normative)

```javascript
export default {
  "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
  "*.{json,md,yml,yaml}": ["prettier --write"],
  "*.sol": ["forge fmt"],
};
```

### commitlint 19.x

[commitlint](https://commitlint.js.org/) enforces [Conventional Commits](https://www.conventionalcommits.org/):

#### `commitlint.config.ts` (normative)

```typescript
import type { UserConfig } from "@commitlint/types";

const config: UserConfig = {
  extends: ["@commitlint/config-conventional"],
  rules: {
    "scope-enum": [
      2,
      "always",
      [
        "safe",
        "vault",
        "agent-proxy",
        "ui",
        "eslint-config",
        "typescript-config",
        "web",
        "ci",
        "deps",
        "monorepo",
      ],
    ],
    "body-max-line-length": [0, "always", Infinity],
  },
};

export default config;
```

**Commit message format:**

```
type(scope): description

feat(vault): add ERC-4626 deposit tool
fix(safe): handle missing pool address in get_pool_info
chore(deps): update viem to 2.30.0
docs(monorepo): update architecture diagram
```

**Allowed types:** `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `revert`

**Allowed scopes:** `safe`, `vault`, `agent-proxy`, `ui`, `eslint-config`, `typescript-config`, `web`, `ci`, `deps`, `monorepo`
