# Linting

> ESLint and Prettier rules - consistent formatting, flat config, husky/lint-staged integration, CI/CD

- Skill: `14bryanespinoza/linting` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 14bryanespinoza/linting`
- Raw SKILL.md: https://api.skillmd.com/api/skills/14bryanespinoza/linting/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: 14BryanEspinoza (https://skillmd.com/u/14bryanespinoza)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/14bryanespinoza/linting

---


# Linting — ESLint & Prettier

---

## 1. Philosophy

1. **Flat config only** — `eslint.config.js` (ESLint 9+). Legacy `.eslintrc*` not supported.
2. **Prettier for formatting** — ESLint only for code quality. No rule overlap.
3. **Pre-commit via husky** — `lint-staged` runs on staged files only. Fast, deterministic.
4. **CI fails on warnings** — Zero-tolerance in pipeline. Local can warn.
5. **TypeScript-aware** — Type-checked rules via `typescript-eslint`.

---

## 2. Minimum Versions

| Technology | Minimum Version |
| ---------- | --------------- |
| ESLint     | 9.0+            |
| Prettier   | 3.3+            |
| Node.js    | 22+             |
| pnpm       | 11+             |

---

## 3. ESLint — Flat Config (`eslint.config.js`)

### Structure

```js
// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginJsxA11y from "eslint-plugin-jsx-a11y";
import prettierConfig from "eslint-config-prettier";

export default tseslint.config(
  { ignores: ["dist/", "node_modules/", "*.config.js", "*.config.ts"] },
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",
      parserOptions: {
        project: "./tsconfig.json",
        tsconfigRootDir: import.meta.dirname,
      },
    },
    settings: { react: { version: "18.3" } },
    plugins: {
      react: pluginReact,
      "react-hooks": pluginReactHooks,
      "jsx-a11y": pluginJsxA11y,
    },
    rules: {
      // React
      "react/react-in-jsx-scope": "off",
      "react/prop-types": "off",
      "react-hooks/rules-of-hooks": "error",
      "react-hooks/exhaustive-deps": "warn",
      // JSX a11y
      "jsx-a11y/anchor-is-valid": "error",
      "jsx-a11y/click-events-have-key-events": "warn",
      "jsx-a11y/no-noninteractive-element-interactions": "warn",
      // TypeScript (type-checked)
      "@typescript-eslint/no-unused-vars": [
        "error",
        { argsIgnorePattern: "^_" },
      ],
      "@typescript-eslint/consistent-type-imports": "error",
      "@typescript-eslint/no-floating-promises": "warn",
      // General
      "no-console": ["warn", { allow: ["warn", "error"] }],
      eqeqeq: ["error", "always"],
    },
  },
  prettierConfig,
);
```

### Key rules

| Rule                                         | Level                     | Why                           |
| -------------------------------------------- | ------------------------- | ----------------------------- |
| `react/react-in-jsx-scope`                   | `off`                     | React 17+ auto-import         |
| `react/prop-types`                           | `off`                     | TypeScript replaces it        |
| `react-hooks/exhaustive-deps`                | `warn`                    | Catches stale closures        |
| `@typescript-eslint/no-unused-vars`          | `error`                   | Clean code, allows `_` prefix |
| `@typescript-eslint/consistent-type-imports` | `error`                   | Enables `import type`         |
| `no-console`                                 | `warn` (allow warn/error) | Keeps debug logs intentional  |
| `eqeqeq`                                     | `error`                   | Strict equality               |

---

## 4. Prettier

### `.prettierrc`

```json
{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "es5",
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "bracketSpacing": true,
  "arrowParens": "avoid",
  "endOfLine": "lf",
  "plugins": ["prettier-plugin-organize-imports"]
}
```

### Rules

- **`printWidth: 100`** — Modern screens, matches MD013 prose wrap
- **`trailingComma: "es5"`** — Clean diffs, valid ES5+
- **`arrowParens: "avoid"`** — `x => x` not `(x) => x`
- **`prettier-plugin-organize-imports`** — Auto-sorts imports

---

## 5. ESLint + Prettier Integration

```bash
pnpm add -D eslint-config-prettier
```

```js
// eslint.config.js (add to config array)
import prettierConfig from "eslint-config-prettier";

export default [
  // ... other configs
  prettierConfig, // Must be LAST — disables formatting rules
];
```

### What it does

- Disables all ESLint rules that conflict with Prettier
- No `.prettierrc` needed in ESLint config
- Prettier runs separately (via `lint-staged` or editor)

---

## 6. Husky + lint-staged

> **Setup owned by `git` skill** — this skill references it.

### Minimal setup (in `package.json`)

```json
{
  "scripts": {
    "prepare": "husky",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check ."
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,yml,yaml,md,css}": ["prettier --write"]
  }
}
```

### Husky hook (`.husky/pre-commit`)

```bash
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm lint-staged
```

### Rules Husky

- **`lint-staged` only** — runs on staged files, not whole repo
- **ESLint first, then Prettier** — fix logic, then format
- **`prepare` script** — auto-installs husky on `pnpm install`

---

## 7. CI/CD

> **Full workflow in `deploy` skill** — this skill defines the lint command.

```yaml
# .github/workflows/lint.yml
- name: Lint
  run: pnpm lint
- name: Format check
  run: pnpm format:check
```

### Rules CI/CD

- **`pnpm lint`** — fails on error, warns on warning
- **`pnpm format:check`** — fails if Prettier would change files
- **Run in PR pipeline** — blocks merge on failure

---

## 8. Editor Config (VS Code)

### `.vscode/settings.json`

```json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.validate": [
    "javascript",
    "typescript",
    "javascriptreact",
    "typescriptreact"
  ],
  "prettier.requireConfig": true
}
```

### Rules Editor

- **Format on save** — Prettier via extension
- **ESLint fix on save** — explicit (not auto) to avoid conflicts
- **Require Prettier config** — prevents formatting without config

---

## 9. Linting for Other Files

### JSON / YAML / Markdown

```js
// eslint.config.js (add to config array)
import pluginJson from "eslint-plugin-jsonc";
import pluginYaml from "eslint-plugin-yml";
import pluginMarkdown from "eslint-plugin-markdown";

export default [
  // ... other configs
  {
    files: ["**/*.json", "**/*.jsonc"],
    plugins: { jsonc: pluginJson },
    languageOptions: { parser: pluginJson.parsers.jsonc },
    rules: {
      "jsonc/sort-keys": ["warn", { order: { type: "asc" } }],
      "jsonc/no-comments": "off", // JSONC allows comments
    },
  },
  {
    files: ["**/*.yml", "**/*.yaml"],
    plugins: { yml: pluginYaml },
    languageOptions: { parser: pluginYaml.parsers.yaml },
    rules: { "yml/sort-keys": "warn" },
  },
  {
    files: ["**/*.md"],
    plugins: { markdown: pluginMarkdown },
    processor: "markdown/markdown",
    rules: {
      "markdown/no-html": "off",
      "markdown/fenced-code-blocks": "warn",
    },
  },
];
```

### CSS (via Stylelint — separate tool)

```bash
pnpm add -D stylelint stylelint-config-standard stylelint-prettier
```

```json
// .stylelintrc.json
{
  "extends": ["stylelint-config-standard", "stylelint-prettier"],
  "rules": { "color-hex-case": "lower" }
}
```

> Stylelint runs separately: `pnpm stylelint "**/*.css"`

---

## 10. Methodology

Before using ANY ESLint/Prettier config/rule/plugin not documented in
this skill:

1. **MCP Context7** (priority): `context7_resolve-library-id` +
   `context7_query-docs` for ESLint/Prettier/plugins.
2. **Official docs**: eslint.org, prettier.io — verify current rules + options.
3. **Project config**: `eslint.config.js`, `.prettierrc`, `package.json`
   scripts — verify against actual setup.
4. **HARD RULE**: If not in this skill AND cannot be verified against
   2 authoritative sources → DO NOT USE IT. Document as assumption or risk in
   report to orchestrator.

---

## 11. Prohibitions

- ❌ Do not use legacy `.eslintrc*` — flat config only (ESLint 9+)
- ❌ Do not enable formatting rules in ESLint — Prettier owns formatting
- ❌ Do not run `eslint .` on whole repo in pre-commit — use `lint-staged`
- ❌ Do not skip `format:check` in CI — catches formatting drift
- ❌ Do not disable `react-hooks/exhaustive-deps` — catches real bugs
- ❌ Do not use `any` in TypeScript — enable `@typescript-eslint/no-explicit-any`
- ❌ Do not ignore `dist/`, `node_modules/`, `*.config.*` — add to `ignores`
- ❌ Do not run Prettier without config — `prettier.requireConfig: true`

---

## 12. References

> **Note:** For Git hooks (husky/lint-staged), see [Git](../git/SKILL.md)
> **Note:** For CI/CD workflow templates, see [Deploy](../deploy/SKILL.md)
> **Note:** For JavaScript conventions, see [JavaScript](../javascript/SKILL.md)
> **Note:** For TypeScript rules, see [TypeScript](../typescript/SKILL.md)

---

Last updated: 2026-08

