Skip to content

Oxlint is a linter written in Rust. The @nx/oxlint plugin runs it as a cacheable Nx task, so a change re-lints only the projects it affects, and lints every project in a run with a single Oxlint process.

Terminal window
nx add @nx/oxlint

This installs oxlint if it is not already installed, registers @nx/oxlint, and creates a root .oxlintrc.json if the workspace has no Oxlint config.

After this, the application and library generators follow the workspace. Run nx g @nx/react:app with no --linter and Nx sets up Oxlint, because the workspace now has it. An ESLint workspace gets ESLint, and a workspace part-way through a migration gets Oxlint. Nx asks only when the workspace has no linter at all, and sets up no linter when the prompt cannot run, such as in CI. Pass --linter to choose for a single project.

@nx/oxlint requires Oxlint 1.70.0 or later. The generators stop with an error on anything older.

@nx/oxlint infers a lint task for projects governed by an Oxlint config. It recognizes the four config files Oxlint discovers on its own:

  • .oxlintrc.json
  • .oxlintrc.jsonc
  • oxlint.config.ts
  • oxlint.config.mts

A project gets a task when it contains files Oxlint can lint. The workspace root also needs a package.json and a src or lib directory, so a standalone workspace that keeps its sources elsewhere gets no root task. Documentation-only and non-JavaScript projects get no target, and giving one a config of its own doesn't change that.

A project's task skips the roots of any projects nested inside it, so Oxlint lints each file once, under the project that owns it.

View the inferred task with:

Terminal window
nx show project my-project --web

The inferred task uses the @nx/oxlint:lint executor, which runs in batch mode by default. When a command lints several projects, such as nx run-many -t lint or nx affected -t lint, Nx starts one Oxlint process for all of them instead of one per project. Oxlint is fast enough that starting it dominates the time to lint a project, so this brings nx run-many -t lint close to running oxlint once at the workspace root.

Each project keeps its own result. Nx splits the Oxlint report by project, so every project has its own cache entry and nx affected still picks projects individually. A violation fails every project that lints its file, which by default is only the project that owns that file. A nested project owns its own files: the project that contains it never lints them.

To lint each project in its own process instead, pass --batch=false.

Options on the task and flags on the command line are forwarded to Oxlint, so nx run-many -t lint --config a.json runs Oxlint with --config a.json, once for the whole run. When the projects in a batch set different Oxlint options, the batch runs with the first project's options and Nx prints a warning. Lint such projects with --batch=false on every run to give each its own options. A batch caches each project's result, and a run with --batch=false reuses it until the project's inputs or options change. A path in a project's args is an Oxlint option too, so in a batch it applies only when the first project's options include it. To lint more paths in a project, add them to lintFilePatterns. Oxlint runs from the workspace root, so a path in a forwarded option is relative to it rather than to the project: a project with its own config writes "config": "libs/a/oxlint.local.json". The exceptions are the options Nx interprets itself: --max-warnings and --deny-warnings decide each project's result from that project's diagnostics, --silent empties the output, and --format is rendered by Nx from the Oxlint JSON report. Supported formats are default, agent, github, and json. default matches the Oxlint terminal output, and switches to one line per diagnostic in CI or under an AI agent, as Oxlint does.

Inferred tasks hash the files Oxlint can lint, so editing a README or a JSON file in the project does not re-lint it. They also declare the Oxlint config, every config reachable through extends, .eslintignore files in the project's ancestor directories, local jsPlugins files, and any tsconfig.json referenced from outside the project root. Changing any of them invalidates the cache. The task does not use the default named input, so what re-lints does not depend on how the workspace defines it.

Dependencies are hashed only when the config registers the module boundaries plugin, because that is the one rule that looks across projects. A jsPlugins package or workspace project becomes a dependency of every project linted under that config, so upgrading or editing the plugin invalidates the cache and marks those projects as affected.

Install the official Oxc extension (oxc.oxc-vscode) for VS Code and Cursor. nx add @nx/oxlint appends it to .vscode/extensions.json when that file already exists.

Oxc also ships extensions for Zed, JetBrains IDEs, and Neovim. Any editor with LSP support can use oxlint --lsp as its language server.

Oxlint walks up from each file to the nearest config, and that config replaces the one above it rather than merging into it. A project config needs an extends pointing back at the config above it, or that config's categories and rules stop applying to the project. The generators write that extends whenever they create a project config, targeting the closest ancestor config rather than the workspace root. A config you wrote yourself keeps whatever extends it already has.

Two configs in one directory is an error, not a precedence, so pick one format per directory. Prefer .oxlintrc.json:

  • oxlint.config.ts only works on a Node runtime that can run TypeScript directly, which means Node 22.18 or later. Node 20 cannot, even though Oxlint's error message suggests otherwise, and Oxlint's standalone binary cannot either.
  • Only JSON configs get their extends chain tracked as task inputs. Nx can't read a TypeScript config statically.
  • Framework generators can only register Oxlint plugins into a JSON config. When the nearest config above a project is TypeScript they can still update a project that already has its own .oxlintrc.json, but they can't create one. nx g @nx/react:app --linter=oxlint warns and leaves the plugins array to you. A TypeScript config further up doesn't block anything, because the nearest one is what Oxlint resolves to.

Oxlint is designed to run alongside ESLint during a migration, so the plugin claims the first available name from:

  1. lint
  2. oxlint
  3. oxlint:lint
  4. oxlint-lint

A workspace with no other linter gets lint. A workspace where ESLint already owns lint gets oxlint.

To change how one project is linted, add options to the inferred target in its project.json. Any option the executor does not define becomes an Oxlint flag, so typeAware runs Oxlint with --type-aware. Set lintFilePatterns to lint part of a project.

{
"targets": {
"lint": {
"options": {
"typeAware": true,
"lintFilePatterns": ["{projectRoot}/src"]
}
}
}
}

The plugin itself takes one option, targetName. nx add @nx/oxlint already writes it into nx.json with the name it picked from the list above, so edit that entry when you want a different one:

{
"plugins": [
{
"plugin": "@nx/oxlint",
"options": { "targetName": "oxlint" }
}
]
}

Which files Oxlint lints within those paths is not an Nx setting. Oxlint selects those itself. Narrow them with ignorePatterns in .oxlintrc.json, or scope individual rules with overrides.

The Oxlint type-aware rules need the oxlint-tsgolint package, which bundles its own TypeScript compiler. Your workspace's TypeScript version doesn't matter.

Turn them on for a single run with nx lint my-app --type-aware, which Nx forwards to Oxlint. To make it permanent, set typeAware on the target as shown in Customizing the task.

@nx/oxlint ships the @nx/enforce-module-boundaries rule as an Oxlint JavaScript plugin. Register it in .oxlintrc.json:

{
"jsPlugins": ["@nx/oxlint/boundaries-plugin"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": []
}
]
}
}

depConstraints takes the same syntax as it does under ESLint. To tag projects and write constraints, see enforce module boundaries.