# Linting

Catch bot mistakes in your editor with the shared ESLint config and its seedcord and discord.js rules.

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://seedcord.org/docs/packages/eslint-config/latest/functions/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://seedcord.org/docs/packages/eslint-config/latest/interfaces/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://seedcord.org/docs/packages/eslint-config/latest/functions/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"
    }
}
```
