# Linting

Catch bot mistakes in your editor with the shared ESLint config and its seedcord and discord.js rules. Covers the config file, what each rule set catches, turning one rule off, the other options, formatting, and adding all of it to an existing project.

Some bot mistakes compile cleanly and only fail when a user clicks. A handler without its route decorator never answers, and a builder with a name Discord rejects fails when you deploy. The seedcord and discord.js lint rules report both in your editor, before the bot ever starts.

The scaffold writes `eslint.config.ts` for you, plus two scripts that run ESLint over your `src` folder.

```sh
pnpm run lint
```

`lint:fix` runs the same rules and writes back the fixes it can apply.

## The config file

Setting up typescript-eslint, import ordering, and several more plugins by hand takes a long config file that you then maintain. The scaffold's file is one call instead.

```ts title="eslint.config.ts"
import createConfig from '@seedcord/eslint-config';

export default createConfig({
    tsconfigRootDir: import.meta.dirname,
    registerDiscordjsPlugin: true,
    registerSeedcordPlugin: true,
    generalIgnores: ['**/seedcord-gen.d.ts']
});
```

[`createConfig`](https://docs.seedcord.org/packages/eslint-config/latest/create-config) returns a flat config array, so ESLint reads it directly. By default it turns on the typescript-eslint presets, import ordering, TSDoc syntax checking, unicorn, the security plugin, and prettier compatibility. `tsconfigRootDir` points the type-aware rules at your `tsconfig.json`. `import.meta.dirname` is the folder holding `eslint.config.ts`. That value is right whenever your `tsconfig.json` is in the same folder as `eslint.config.ts`.

`registerDiscordjsPlugin` and `registerSeedcordPlugin` add the rules written for bots. If you leave either one out, its rules never run. The next two sections list what each one brings in.

## What the seedcord rules catch

These read your handlers, commands, events, and subscribers. Some catch a class that never registers. The rest catch a call that bypasses the framework.

{/* prettier-ignore-start */}

| rule                                       | what it catches                                                                                      |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `interaction-handler-missing-route`        | a handler missing its route decorator. Its clicks reach the unhandled default                        |
| `command-builder-missing-register-command` | a command missing `@RegisterCommand`, which keeps it from deploying to Discord                       |
| `event-handler-missing-register-event`     | an event handler missing `@RegisterEvent`, so it never fires. Gateway only                           |
| `middleware-missing-register-decorator`    | middleware missing `@Middleware`. It never runs on a request                                         |
| `subscriber-missing-decorators`            | a subscriber missing `@Subscribe`, or a `WebhookLog` missing `@WebhookUrl`                           |
| `no-raw-client-events`                     | a `client.on` listener, which skips the dispatcher and every middleware on it. Gateway only          |
| `no-raw-interaction-acks`                  | a raw discord.js `reply`, `defer`, `update`, or `showModal` on a handler's interaction               |
| `no-djs-builder-import`                    | a component builder imported from `discord.js`. Only the `@discordjs/builders` copy survives nesting |
| `use-custom-id-codec`                      | a hand-written `setCustomId` string, which drifts from the route that reads it                       |
| `use-paint-in-logs`                        | chalk inside a logger call. A terminal theme remaps chalk's color names                              |

{/* prettier-ignore-end */}

## What the discord.js rules catch

Builders get their own set. Most of these rules stop a payload Discord would reject, or one discord.js refuses to build.

{/* prettier-ignore-start */}

| rule                              | what it catches                                                                                     |
| --------------------------------- | --------------------------------------------------------------------------------------------------- |
| `valid-command-name`              | a name outside Discord's rules: lowercase letters, digits, hyphens, underscores, 1 to 32 characters |
| `required-option-before-optional` | a required slash option declared after an optional one                                              |
| `no-choices-and-autocomplete`     | autocomplete and choices on the same option, which throws a `RangeError`                            |
| `no-discord-limit-exceeded`       | more items than a builder allows, whenever the count is written in the code                         |
| `require-button-props`            | a button missing the props that its style requires, like a url on a Link button                     |
| `no-conflicting-button-props`     | a button carrying props that its style forbids, like a customId next to a url                       |
| `select-menu-min-exceeds-max`     | a select menu whose minimum selections go above its maximum                                         |
| `require-components-v2-flag`      | v2 components sent without `MessageFlags.IsComponentsV2`                                            |
| `no-mixed-message-format`         | builder components mixed with content, embeds, poll, or stickers                                    |
| `prefer-ephemeral-flag`           | the deprecated `ephemeral` option. `lint:fix` rewrites it to `flags`                                |
| `prefer-v2-component`             | an embed where a container and a text display fit                                                   |

{/* prettier-ignore-end */}

`prefer-ephemeral-flag` and `prefer-v2-component` report as warnings, since both describe a preference. Every other rule on this page reports as an error.

## Turning one off

Sometimes a rule fits most of your bot and not one folder, like old code you haven't moved to v2 components yet. The seedcord rules carry a `@seedcord/` prefix, and the discord.js rules carry `discordjs/`. The rule tables leave those prefixes out, so write the full name when you change a rule.

`userConfigs` goes in last, so anything you put there overrides the blocks above it. This one turns `prefer-v2-component` off for `src/legacy` only.

```ts title="eslint.config.ts"
export default createConfig({
    tsconfigRootDir: import.meta.dirname,
    registerDiscordjsPlugin: true,
    registerSeedcordPlugin: true,
    userConfigs: [
        {
            files: ['src/legacy/**/*.ts'],
            rules: { 'discordjs/prefer-v2-component': 'off' }
        }
    ]
});
```

## The other options

Every one of these is optional. The defaults suit a normal bot. For the full set, including the Tailwind and MDX keys a web project would set, see [`SeedcordConfigOptions`](https://docs.seedcord.org/packages/eslint-config/latest/seedcord-config-options).

{/* prettier-ignore-start */}

| option                      | default               | what it does                                                                                                                                                      |
| --------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tsconfigRootDir`           | the working directory | where the type-aware rules look for `tsconfig.json`                                                                                                               |
| `generalIgnores`            | `[]`                  | globs to skip, added to `dist`, `node_modules`, `logs`, and `tests/temp`                                                                                          |
| `userConfigs`               | `[]`                  | your own config blocks, applied last                                                                                                                              |
| `registerImportPlugin`      | `'all'`               | `'fast'` skips the cycle and deprecation checks. `'off'` drops the plugin                                                                                         |
| `registerUnicornPlugin`     | `true`                | unicorn requires ESLint 10.4, so set `false` on ESLint 9                                                                                                          |
| `registerSecurityPlugin`    | `true`                | the `eslint-plugin-security` recommended set                                                                                                                      |
| `registerTsdocPlugin`       | `true`                | TSDoc syntax checking in your comments                                                                                                                            |
| `registerTypescriptConfigs` | `true`                | the typescript-eslint recommended, type-checked, strict, and stylistic presets. `'no-type-checked'` keeps them and turns off the rules that read the type checker |

{/* prettier-ignore-end */}

> **Tip**
>
> When your lint run gets slow, setting `registerTypescriptConfigs` to `'no-type-checked'` saves the most. Dropping the rules that read the type checker cuts about a fifth of the run time on the seedcord packages. `registerImportPlugin: 'fast'` removes the two rules that parse every file an import resolves to. That one saves closer to a tenth.

## Formatting

Prettier formats your code and reads its own config file. The scaffold points that file at the shared prettier config, so formatting matches across seedcord projects.

```ts title="prettier.config.ts"
import { createPrettierConfig } from '@seedcord/eslint-config/prettier';

export default createPrettierConfig();
```

[`createPrettierConfig`](https://docs.seedcord.org/packages/eslint-config/latest/create-prettier-config) also takes per-glob `overrides`, and a `tailwind` option for web projects that sort Tailwind classes.

```ts title="prettier.config.ts"
export default createPrettierConfig({
    overrides: [{ files: '*.md', options: { proseWrap: 'always' } }]
});
```

`createConfig` already applies `eslint-config-prettier`, which switches off every ESLint rule that overlaps prettier's formatting. You don't install it or add it to `userConfigs`.

## Starting from an existing project

```sh
pnpm add -D @seedcord/eslint-config eslint jiti typescript
```

`@seedcord/eslint-config` brings the seedcord and discord.js rule plugins with it, so that one line installs everything. ESLint loads a TypeScript config file through `jiti`. Add the `eslint.config.ts` from the top of this page along with the two scripts.

```json title="package.json"
{
    "scripts": {
        "lint": "eslint 'src/**/*.ts' --cache",
        "lint:fix": "eslint 'src/**/*.ts' --fix --cache"
    }
}
```
