# ESLint v9 Flat Config Migration Instructions for LLM

## Overview

These instructions guide you through finishing the migration of an Nx workspace to ESLint v9.

ESLint v9 makes flat config (`eslint.config.{mjs,cjs,js}`) the default config format. The legacy eslintrc format (`.eslintrc.*`) still works at runtime, but only when `ESLINT_USE_FLAT_CONFIG=false` is set, so Nx converts workspaces to flat config instead of relying on that escape hatch.

The migration runs in two halves:

1. A deterministic pre-pass (the `@nx/eslint:convert-to-flat-config` generator) that already converted the JSON and YAML eslintrc configs.
2. This prompt: finish the parts that need judgment and leave the workspace lint-passing.

Work systematically through each section below.

<pre_pass_summary note="a deterministic pre-pass already applied these edits; verify the new shape is in place rather than redoing them">

The pre-pass handled, mechanically:

- Converted the root and per-project JSON/YAML eslintrc files to `eslint.config.mjs`:
  - `eslint:recommended` to `js.configs.recommended`
  - `@nx/*` presets to their flat-config equivalents
  - `env` to `languageOptions.globals`
  - `parser` / `parserOptions` to `languageOptions`
  - `plugins` to the flat `plugins` object
  - `ignorePatterns` and `.eslintignore` to `ignores`
  - stale `.eslintrc`/`.eslintignore` references in `nx.json` and `project.json` inputs
- Added `@eslint/js` and `@eslint/eslintrc` to `package.json` when the converted config needs them.

The pre-pass does NOT:

- Convert JavaScript-based eslintrc files (`.eslintrc.js`, `.eslintrc.cjs`). It cannot evaluate them safely.
- Change the output formatter a lint target uses.
- Decide whether a generated `FlatCompat` shim should become flat-native config.
- Make the workspace pass lint after ESLint v9 changed which rules its preset defaults enable.

Everything the pre-pass could not finish is forwarded to you in `<advisory_context>`.

How to read the wrapper sections above this file:

- `<files_changed>` lists files the pre-pass wrote. Verify the new shape is in place; do not re-apply the same edit. It is absent when the pre-pass made no changes (for example a workspace that was already on flat config).
- `<advisory_context>` lists detections the pre-pass forwarded because it could not safely complete them. Every entry is pending work. Address each one in the relevant section below.

</pre_pass_summary>

<handoff_guidance>
In your handoff `summary` (1 to 3 sentences per the system prompt), name the sections you applied and explicitly call out any you skipped because they did not apply (for example "no JavaScript-based configs and no removed formatters in this workspace").
</handoff_guidance>

## Pre-Migration Checklist

1. **Confirm the ESLint version is v9**:

   ```bash
   npx eslint --version
   ```

2. **Locate all ESLint config files**:
   - Flat configs: `eslint.config.{mjs,cjs,js}` at the root and in each project.
   - Any remaining eslintrc files: `.eslintrc`, `.eslintrc.json`, `.eslintrc.yaml`, `.eslintrc.yml`, `.eslintrc.js`, `.eslintrc.cjs`.
   - Ignore files: `.eslintignore`.

3. **Identify all lint targets**:

   ```bash
   nx show projects --with-target lint
   ```

   Check `project.json` files for the `@nx/eslint:lint` executor or `eslint` run-commands. Workspaces using the inferred plugin (`@nx/eslint/plugin`) get lint targets from the presence of `eslint.config.*`; inspect them with `nx show project <name> --json`.

4. **Identify local ESLint rules or plugins** authored inside the workspace. These use the rule API that changed in v9 (see section 6).

---

## Nx-Specific Notes (read first)

- **Flat config is the default in v9**. eslintrc only resolves when `ESLINT_USE_FLAT_CONFIG=false` is set. Nx converts the workspace to flat config so that no environment variable is required.
- **Shared base config pattern**: many Nx workspaces have a root `eslint.config.mjs` that each project imports, for example `import baseConfig from '../../eslint.config.mjs'`. Convert and verify the base config first, then the per-project configs.
- **Inferred plugin targets**: `@nx/eslint/plugin` infers the lint target from the presence of `eslint.config.*`. Renaming or moving the config invalidates inference. After config edits, run `nx reset && nx show project <name>` on a sample project to confirm the target is still present.
- **FlatCompat shim**: when the pre-pass could not translate a third-party `extends` or a complex override natively, it emitted a `FlatCompat` shim (from the `@eslint/eslintrc` package). That config works as-is, but section 3 covers replacing it with flat-native config where low-risk.

---

## 1. Already on flat config? Verify only

If the workspace already uses `eslint.config.*` at the root and in every project, with no remaining `.eslintrc.*` files, do NOT restructure it. The only required work is the passing-state check in section 4: a workspace on ESLint v9 can newly fail because v9 and typescript-eslint v8 changed which rules their recommended sets enable, even when the config was already flat.

## 2. Convert JavaScript-based ESLint configs the pre-pass skipped

**Search pattern**: `.eslintrc.js` and `.eslintrc.cjs` files (forwarded in `<advisory_context>`).

**What changed**: the pre-pass only converts JSON and YAML eslintrc files. JavaScript-based configs run arbitrary code, so they need manual conversion.

```js
// BEFORE (.eslintrc.js)
module.exports = {
  extends: ['../../.eslintrc.json'],
  overrides: [
    {
      files: ['*.ts'],
      rules: { '@typescript-eslint/no-explicit-any': 'error' },
    },
  ],
};
```

```js
// AFTER (eslint.config.mjs)
import baseConfig from '../../eslint.config.mjs';

export default [
  ...baseConfig,
  {
    files: ['**/*.ts'],
    rules: { '@typescript-eslint/no-explicit-any': 'error' },
  },
];
```

**Action items**:

- [ ] Convert each JavaScript-based config to `eslint.config.mjs`, mirroring the structure the pre-pass produced for the JSON/YAML configs.
- [ ] Preserve the existing rules, plugins, parser options, and overrides.
- [ ] Delete the original `.eslintrc.js` / `.eslintrc.cjs` once the flat config replaces it.
- [ ] Update any `project.json` / `nx.json` inputs that referenced the old file name.

## 3. Convert FlatCompat shims to flat-native config where low-risk

**Search pattern**: `FlatCompat`, `@eslint/eslintrc`, `compat.extends(`, `compat.config(` in the generated `eslint.config.*` files (listed in `<files_changed>`).

**What changed**: `FlatCompat` is a runtime shim that adapts eslintrc-style `extends` into flat config. Many plugins now ship native flat presets, which are clearer and avoid the shim.

**Decision rule**: convert a `FlatCompat` usage to flat-native config when it is low-risk, otherwise keep the shim.

- Low-risk (prefer flat-native): typescript-eslint configs, and plugins that document a flat preset (for example `eslint-plugin-react`, `eslint-plugin-import`).
- Keep the shim: third-party shared configs that do not document a flat-config entry point.

```js
// BEFORE (FlatCompat shim, eslint.config.mjs)
import js from '@eslint/js';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { FlatCompat } from '@eslint/eslintrc';

const compat = new FlatCompat({
  baseDirectory: dirname(fileURLToPath(import.meta.url)),
  recommendedConfig: js.configs.recommended,
});

export default [...compat.extends('plugin:@typescript-eslint/recommended')];
```

```js
// AFTER (flat-native, eslint.config.mjs)
import tseslint from 'typescript-eslint';

export default [...tseslint.configs.recommended];
```

**Action items**:

- [ ] For each `FlatCompat` usage, decide flat-native vs keep-the-shim using the rule above.
- [ ] When converting, drop the now-unused `@eslint/eslintrc` import if no shim remains in that file.
- [ ] Re-run lint after each change to confirm the rule set did not silently shift.

## 4. Restore the passing baseline (required)

This is the core requirement of the migration: the workspace must lint cleanly when you are done.

ESLint v9 and typescript-eslint v8 changed which rules their recommended sets enable. A rule the user never configured may now report errors. Disable those rules; do not edit source files to satisfy them.

The set of rules the user explicitly configured before the migration is in `<advisory_context>` (the entry that starts with "Passing-state requirement").

**Procedure**:

1. Run lint across the workspace:

   ```bash
   nx run-many -t lint
   ```

2. Tell a rule violation apart from a plugin crash. If a project fails with a thrown error instead of rule findings - a `TypeError` such as `context.getAncestors is not a function`, a `Could not find "<rule>" in plugin "<name>"` / `couldn't find the config "<name>" to extend from`, or a plugin that fails to load - the plugin predates ESLint v9; this is not a changed preset default. Do NOT disable the rule (that silently drops its coverage); update the plugin instead (section 6), then re-run lint and continue.

3. For each rule that now reports errors (findings, not a thrown error):
   - If the rule ID is NOT in the user's explicit list, it came from a changed preset default. Disable it in the relevant flat config with a short comment explaining why.
   - If the rule ID IS in the user's explicit list, the user chose it. Leave it as-is and report it in your handoff summary.

```js
// Disable a rule that a changed preset default newly enabled (eslint.config.mjs).
export default [
  ...baseConfig,
  {
    files: ['**/*.ts'],
    rules: {
      // Newly enabled by the ESLint v9 recommended set; was not enforced before the upgrade.
      'no-unused-expressions': 'off',
    },
  },
];
```

**Action items**:

- [ ] Run lint and collect every newly reported rule.
- [ ] Treat a plugin crash (a thrown error, not rule findings) as a version incompatibility: update the plugin (section 6), never disable its rules to silence it.
- [ ] Disable preset-originated rules that the user did not configure.
- [ ] Never disable or weaken a rule the user explicitly configured.
- [ ] Never edit source files to satisfy a newly enabled rule.

<fail_if note="if you cannot reach a passing state without editing source or disabling a user-configured rule, stop and report">
You cannot make lint pass without either editing source files or disabling a rule the user explicitly configured. Write status: failed and explain which rule and project in your summary. Do not guess.
</fail_if>

## 5. Fix removed output formatters

**Search pattern**: the `format` option on lint targets (forwarded in `<advisory_context>`).

**What changed**: ESLint v9 removed several built-in output formatters. The built-ins that remain are `stylish`, `html`, `json`, and `json-with-metadata`. Removed: `compact`, `codeframe`, `unix`, `visualstudio`, `table`, `checkstyle`, `jslint-xml`, `junit`, `tap`.

**Fix**: switch the target to a built-in that remains, or install the matching community package and reference it by its package name.

```bash
# Example: keep junit output by installing the community formatter package.
npm install --save-dev eslint-formatter-junit
```

```jsonc
// project.json (reference the community formatter by package name)
"lint": {
  "executor": "@nx/eslint:lint",
  "options": { "format": "eslint-formatter-junit" }
}
```

**Action items**:

- [ ] For each flagged target, switch to a remaining built-in formatter or a community package.
- [ ] When using a community package, add it to `devDependencies`.

## 6. Other ESLint v9 runtime breaking changes

**Search pattern**: lint executor options, run-commands invoking `eslint`, and local rule/plugin source.

- **Removed CLI flags and executor options**: `--rulesdir`, `--ext`, and `--resolve-plugins-relative-to` were removed. The matching `@nx/eslint:lint` options (`rulesdir`, `resolvePluginsRelativeTo`, `ignorePath`) are not supported for flat config. Move file targeting into the config via the `files` and `ignores` keys.
- **No eslintrc auto-merge**: flat config does not merge `.eslintrc.*` files found up the tree. Every setting must live in `eslint.config.*`.
- **Third-party plugins that predate ESLint v9**: an installed ESLint plugin that was not updated for v9 breaks at lint time - its rules call the removed `context` APIs below, or it only ships an eslintrc config that no longer loads. This surfaces as a thrown error (see section 4), not a new rule violation. List the installed plugins from `package.json` (`dependencies`/`devDependencies` matching `eslint-plugin-*` or `@<scope>/eslint-plugin-*`), and for each confirm its version supports ESLint v9 (its changelog, or that `peerDependencies.eslint` allows `>=9`). Update any that do not, and prefer the plugin's flat entry point where it ships one (for example `eslint-plugin-cypress/flat`). Update the plugin rather than disabling its rules.
- **Local rule API moved to `SourceCode`** (only relevant if the workspace authors its own rules):
  - `context.getScope()` to `sourceCode.getScope(node)`
  - `context.getAncestors()` to `sourceCode.getAncestors(node)`
  - `context.getDeclaredVariables()` to `sourceCode.getDeclaredVariables(node)`
  - `context.markVariableAsUsed(name)` to `sourceCode.markVariableAsUsed(name, node)`
  - `context.getSource()` to `sourceCode.getText()`
  - `context.parserServices` to `sourceCode.parserServices`
- **Stricter rule schema**: a custom rule that accepts options must declare `meta.schema` in v9.

**Action items**:

- [ ] Remove unsupported CLI flags and executor options; move targeting into `files` / `ignores`.
- [ ] Update any installed third-party ESLint plugin that does not yet support ESLint v9, preferring its flat entry point; do not disable its rules to work around a load error.
- [ ] Update local rules to the `SourceCode` API and add `meta.schema` where required.

---

## Post-Migration Verification

1. Clear the inference cache so renamed configs are re-detected:

   ```bash
   nx reset
   ```

2. Confirm lint passes across the workspace:

   ```bash
   nx run-many -t lint
   ```

3. Spot-check that a converted project resolves its config:

   ```bash
   npx eslint --print-config <a-file-in-the-project>
   ```

4. Confirm no `.eslintrc.*` files remain unless one was intentionally kept.

## References

- ESLint configuration files (flat config): https://eslint.org/docs/latest/use/configure/configuration-files
- Migrate to ESLint v9.0.0: https://eslint.org/docs/latest/use/migrate-to-9.0.0
- typescript-eslint configs: https://typescript-eslint.io/users/configs
- Nx ESLint plugin: https://nx.dev/nx-api/eslint
