Skip to content

@nx/eslint - Migrations

For an overview of the plugin and setup instructions, see the @nx/eslint introduction.

The @nx/eslint plugin provides various migrations to help you migrate to newer versions of eslint projects within your Nx workspace. Below is a complete reference for all available migrations.

Version: 23.1.0-beta.0

Convert remaining ESLint configs to flat config for ESLint v9 and keep the workspace lint-passing, disabling rules whose preset defaults changed.

NameVersion
eslint>=9.0.0

ESLint v9 Flat Config Migration Instructions for LLM

Section titled “ESLint v9 Flat Config Migration Instructions for LLM”

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>

  1. Confirm the ESLint version is v9:

    Terminal window
    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:

    Terminal window
    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).


  • 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.

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

Section titled “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.

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

Section titled “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.
// 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')];
// 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)

Section titled “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:

    Terminal window
    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.
// 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>

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.

Terminal window
# Example: keep junit output by installing the community formatter package.
npm install --save-dev eslint-formatter-junit
// 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

Section titled “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.

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

    Terminal window
    nx reset
  2. Confirm lint passes across the workspace:

    Terminal window
    nx run-many -t lint
  3. Spot-check that a converted project resolves its config:

    Terminal window
    npx eslint --print-config <a-file-in-the-project>
  4. Confirm no .eslintrc.* files remain unless one was intentionally kept.

update-23-1-0-remove-removed-typescript-eslint-extension-rules

Section titled “update-23-1-0-remove-removed-typescript-eslint-extension-rules”

Version: 23.1.0-beta.5

Handle typescript-eslint rules removed in v8 in ESLint flat configs, since referencing a removed rule stops the config from loading. Deletes the removed formatting/extension rules (e.g. @typescript-eslint/no-extra-semi) and rewrites the safe 1:1 renames (no-throw-literal -> only-throw-error, no-useless-template-literals -> no-unnecessary-template-expression) to preserve enforcement.

Version: 23.1.0-beta.5

Migrate the removed @typescript-eslint/ban-types rule to its v8 successors (no-empty-object-type, no-unsafe-function-type, no-wrapper-object-types), whose options do not map 1:1 - so it is driven by an AI prompt rather than a deterministic codemod.

Version: 23.1.0-beta.0

The following packages will be updated:

NameVersionAlways add to package.json
eslint^9.8.0Updated only
typescript-eslint^8.40.0Updated only
@typescript-eslint/eslint-plugin^8.40.0Updated only
@typescript-eslint/parser^8.40.0Updated only
@typescript-eslint/utils^8.40.0Updated only
@typescript-eslint/type-utils^8.40.0Updated only
@typescript-eslint/rule-tester^8.40.0Updated only
@typescript-eslint/scope-manager^8.40.0Updated only
@typescript-eslint/typescript-estree^8.40.0Updated only
eslint-config-prettier^10.0.0Updated only

Version: 23.1.0-beta.0

The following packages will be updated:

NameVersionAlways add to package.json
typescript-eslint^8.58.0Updated only
@typescript-eslint/eslint-plugin^8.58.0Updated only
@typescript-eslint/parser^8.58.0Updated only
@typescript-eslint/utils^8.58.0Updated only
@typescript-eslint/rule-tester^8.58.0Updated only
@typescript-eslint/scope-manager^8.58.0Updated only
@typescript-eslint/typescript-estree^8.58.0Updated only

Version: 23.1.0-beta.0

The following packages will be updated:

NameVersionAlways add to package.json
typescript-eslint^8.58.0Updated only
@typescript-eslint/eslint-plugin^8.58.0Updated only
@typescript-eslint/parser^8.58.0Updated only
@typescript-eslint/utils^8.58.0Updated only
@typescript-eslint/rule-tester^8.58.0Updated only
@typescript-eslint/scope-manager^8.58.0Updated only
@typescript-eslint/typescript-estree^8.58.0Updated only

Version: 23.0.0-beta.17

Rewrites @nx/eslint/src/* subpath imports now that the ./src/* subpath is no longer exposed by @nx/eslint’s exports map. Named imports/exports of public symbols are routed to @nx/eslint and the rest to the new @nx/eslint/internal entry; require, dynamic import and jest.mock calls reference the whole module and are routed to @nx/eslint/internal.

update-23-0-0-migrate-create-nodes-v2-import

Section titled “update-23-0-0-migrate-create-nodes-v2-import”

Version: 23.0.0-beta.24

Rename imports of createNodesV2 from @nx/eslint/plugin to the canonical createNodes export.

Rename createNodesV2 imports to createNodes

Section titled “Rename createNodesV2 imports to createNodes”

@nx/eslint renamed its primary inferred-plugin export from createNodesV2 to createNodes. The createNodesV2 name is preserved as a deprecated alias for now, but new code should use createNodes.

This migration scans every .ts, .tsx, .cts, and .mts file in your workspace and rewrites named imports and re-exports of createNodesV2 from @nx/eslint/plugin to createNodes.

import { createNodesV2 } from '@nx/eslint/plugin';
import { createNodes } from '@nx/eslint/plugin';

Aliases are preserved (createNodesV2 as cn becomes createNodes as cn), and if a file already imports both names ({ createNodes, createNodesV2 }) the redundant binding is dropped.

Only static import/export named bindings from @nx/eslint/plugin are rewritten. Namespace imports, dynamic import(...), require(...) destructuring, and property access such as plugin.createNodesV2 are left untouched — they keep working through the createNodesV2 runtime alias. Update those by hand if you want to drop the deprecated name everywhere.

Version: 22.7.0-beta.12

Add missing inputs to @nx/eslint:lint executor target defaults

Version: 21.5.0-beta.2

The following packages will be updated:

NameVersionAlways add to package.json
typescript-eslint^8.40.0Updated only
@typescript-eslint/eslint-plugin^8.40.0Updated only
@typescript-eslint/parser^8.40.0Updated only
@typescript-eslint/utils^8.40.0Updated only
@typescript-eslint/rule-tester^8.40.0Updated only
@typescript-eslint/scope-manager^8.40.0Updated only
@typescript-eslint/typescript-estree^8.40.0Updated only

Version: 21.5.0-beta.2

The following packages will be updated:

NameVersionAlways add to package.json
typescript-eslint^8.40.0Updated only
@typescript-eslint/eslint-plugin^8.40.0Updated only
@typescript-eslint/parser^8.40.0Updated only
@typescript-eslint/utils^8.40.0Updated only
@typescript-eslint/rule-tester^8.40.0Updated only
@typescript-eslint/scope-manager^8.40.0Updated only
@typescript-eslint/typescript-estree^8.40.0Updated only

Version: 21.2.0-beta.0

The following packages will be updated:

NameVersionAlways add to package.json
typescript-eslint^8.29.0Updated only
@typescript-eslint/eslint-plugin^8.29.0Updated only
@typescript-eslint/parser^8.29.0Updated only
@typescript-eslint/utils^8.29.0Updated only
@typescript-eslint/rule-tester^8.29.0Updated only
@typescript-eslint/scope-manager^8.29.0Updated only
@typescript-eslint/typescript-estree^8.29.0Updated only

Version: 21.2.0-beta.0

The following packages will be updated:

NameVersionAlways add to package.json
typescript-eslint^8.29.0Updated only
@typescript-eslint/eslint-plugin^8.29.0Updated only
@typescript-eslint/parser^8.29.0Updated only
@typescript-eslint/utils^8.29.0Updated only
@typescript-eslint/rule-tester^8.29.0Updated only
@typescript-eslint/scope-manager^8.29.0Updated only
@typescript-eslint/typescript-estree^8.29.0Updated only