Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions astro-docs/sidebar.mts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ const technologiesGroups: SidebarItems = [
link: 'technologies/module-federation/introduction',
},
{ label: 'ESLint', link: 'technologies/eslint/introduction' },
{ label: 'OXLint', link: 'technologies/oxlint/introduction' },
],
},
{
Expand Down Expand Up @@ -967,6 +968,11 @@ const knowledgeBaseGroups: SidebarItems = [
...getTechnologyKBItems('eslint-plugin', 'eslint'),
],
},
{
label: 'OXLint',
collapsed: true,
items: [...getTechnologyKBItems('oxlint')],
},
{
label: 'Vite',
collapsed: true,
Expand Down Expand Up @@ -1103,6 +1109,11 @@ const referenceGroups: SidebarItems = [
...getTechnologyAPIItems('eslint-plugin', 'eslint', 'ESLint Plugin'),
],
},
{
label: 'OXLint',
collapsed: true,
items: [...getTechnologyAPIItems('oxlint', undefined, 'OXLint')],
},
{
label: 'Webpack',
collapsed: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ tagged with "scoped:shared" or "scope:admin".

Read more about [ESLint rule options](/docs/technologies/eslint/eslint-plugin/guides/enforce-module-boundaries).

{% /tabitem %}
{% tabitem label="OXLint" %}
Comment thread
juristr marked this conversation as resolved.
Outdated

If you're using `@nx/oxlint` instead of ESLint, configure `@nx/oxlint/boundaries-plugin` and `@nx/enforce-module-boundaries` in `.oxlintrc.json` (see [OXLint module boundaries bridge](/docs/technologies/oxlint/introduction#experimental-module-boundaries-bridge)).

{% /tabitem %}
{% tabitem label="Conformance" %}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: OXLint
description: OXLint guides and best practices for Nx workspaces
sidebar:
hidden: true
pagefind: false
---

{% sidebar_group_cards group="Knowledge Base/OXLint" /%}
9 changes: 9 additions & 0 deletions astro-docs/src/content/docs/technologies/oxlint/index.mdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: OXLint
sidebar:
hidden: true
description: Guides and API references for OXLint in Nx
pagefind: false
---

{% index_page_cards path="technologies/oxlint" /%}
111 changes: 111 additions & 0 deletions astro-docs/src/content/docs/technologies/oxlint/introduction.mdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
title: OXLint Plugin for Nx
description: Learn how to set up and use the @nx/oxlint plugin with inferred tasks, hybrid migration, and experimental module-boundary enforcement.
sidebar:
label: Introduction
filter: 'type:References'
---

The `@nx/oxlint` plugin integrates [OXLint](https://oxc.rs/docs/guide/usage/linter/) with Nx for fast lint execution in monorepos.

## Experimental status

{% aside type="note" title="Experimental" %}
`@nx/oxlint` and the OXLint JS-plugin boundary bridge are experimental.
The feature set may evolve quickly while OXLint JS plugins are still stabilizing.
{% /aside %}

## Setup

Install the plugin:

```shell
nx add @nx/oxlint
```

This registers `@nx/oxlint/plugin` (when inference plugins are enabled), installs `oxlint`, and creates a root `.oxlintrc.json` if needed.

## Inferred tasks

`@nx/oxlint/plugin` infers tasks from OXLint config files:

- `.oxlintrc.json`
- `.oxlintrc.jsonc`
- `oxlint.config.ts`

The plugin infers command-based targets and configures caching using Nx inputs.

You can view inferred tasks with:

```shell
nx show project my-project --web
```

## Target naming in hybrid workspaces

For mixed ESLint + OXLint workspaces, Nx attempts to avoid target collisions.
When adding the plugin, it tries target names in this order:

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

This enables incremental migration without removing ESLint.

## Explicit target fallback

If `useInferencePlugins` is disabled, use explicit targets with `@nx/oxlint:lint`:

```json
{
"targets": {
"oxlint": {
"executor": "@nx/oxlint:lint",
"options": {
"lintFilePatterns": ["{projectRoot}"]
}
}
}
}
```

## Hybrid migration from ESLint

Use the migration helper to register OXLint while keeping ESLint targets intact:

```shell
nx g @nx/oxlint:convert-from-eslint
```

By default this adds an `oxlint` target per converted project and leaves existing ESLint configuration untouched.

For hybrid setups that still run ESLint for some checks, you can optionally use `eslint-plugin-oxlint` to reduce overlap while migrating.

## Experimental module boundaries bridge

`@nx/oxlint` exposes an experimental JS-plugin bridge at `@nx/oxlint/boundaries-plugin`.
It reuses Nx project-graph-aware `enforce-module-boundaries` logic from `@nx/eslint-plugin`.

Example config snippet:

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

This path is intended for early adopters and may change.
2 changes: 2 additions & 0 deletions astro-docs/src/plugins/utils/plugin-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const pluginToTechnology: Record<string, string> = {
eslint: 'eslint',
'eslint-plugin': 'eslint',

oxlint: 'oxlint',

webpack: 'build-tools',
vite: 'build-tools',
rollup: 'build-tools',
Expand Down
13 changes: 13 additions & 0 deletions e2e/oxlint/jest.config.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* eslint-disable */
module.exports = {
transform: {
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
maxWorkers: 1,
globals: {},
globalSetup: '../utils/global-setup.ts',
globalTeardown: '../utils/global-teardown.ts',
displayName: 'e2e-oxlint',
preset: '../jest.preset.e2e.js',
};
8 changes: 8 additions & 0 deletions e2e/oxlint/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@nx/e2e-oxlint",
"version": "0.0.1",
"private": true,
"dependencies": {
"@nx/e2e-utils": "workspace:*"
}
}
9 changes: 9 additions & 0 deletions e2e/oxlint/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "e2e-oxlint",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "e2e/oxlint",
"projectType": "application",
"implicitDependencies": ["oxlint"],
"// targets": "to see all targets run: nx show project e2e-oxlint --web",
"targets": {}
}
162 changes: 162 additions & 0 deletions e2e/oxlint/src/linter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
cleanupProject,
newProject,
readJson,
runCLI,
uniq,
updateFile,
updateJson,
} from '@nx/e2e-utils';

describe('OXLint', () => {
const explicitLib = uniq('mylib-explicit');
const inferredLib = uniq('mylib-inferred');
const inferredViteLib = uniq('mylib-inferred-vite');
const hybridLib = uniq('mylib-hybrid');

beforeAll(() => {
newProject({
packages: ['@nx/js', '@nx/oxlint'],
});
});

afterAll(() => {
cleanupProject();
});

it('should add explicit fallback target when inference is disabled', () => {
updateJson('nx.json', (json) => {
json.useInferencePlugins = false;
return json;
});

runCLI(`generate @nx/js:lib libs/${explicitLib} --linter none`);
runCLI(`generate @nx/oxlint:lint-project --project ${explicitLib}`);

const project = readJson(`libs/${explicitLib}/project.json`);
expect(project.targets.oxlint.executor).toBe('@nx/oxlint:lint');
});

it('should report lint failures with @nx/oxlint:lint', () => {
updateFile(
`.oxlintrc.json`,
JSON.stringify(
{
rules: {
eqeqeq: 'error',
},
},
null,
2
)
);

updateFile(
`libs/${explicitLib}/src/lib/${explicitLib}.ts`,
'export const bad = (a: number) => a == 1;\n'
);

const out = runCLI(`run ${explicitLib}:oxlint`, {
silenceError: true,
env: { CI: 'false' },
});

expect(out).toContain('eqeqeq');
}, 300000);

it('should infer an OXLint target when inference plugins are enabled', () => {
updateJson('nx.json', (json) => {
json.useInferencePlugins = true;
return json;
});

runCLI(`generate @nx/js:lib libs/${inferredLib} --linter none`);
runCLI(`generate @nx/oxlint:init --addPlugin`);

const nxJson = readJson('nx.json');
const plugin = nxJson.plugins.find((p) =>
typeof p === 'string'
? p === '@nx/oxlint/plugin'
: p.plugin === '@nx/oxlint/plugin'
);
const targetName =
typeof plugin === 'string'
? 'oxlint'
: (plugin.options?.targetName ?? 'oxlint');

const project = JSON.parse(runCLI(`show project ${inferredLib} --json`));
expect(project.targets[targetName]).toBeDefined();
}, 300000);

it('should run inferred target from workspace root and avoid loading project vite config', () => {
updateJson('nx.json', (json) => {
json.useInferencePlugins = true;
return json;
});

runCLI(`generate @nx/js:lib libs/${inferredViteLib} --linter none`);
runCLI(`generate @nx/oxlint:init --addPlugin`);

updateFile(
`libs/${inferredViteLib}/vite.config.ts`,
'export default { lint: { paths: [__dirname] } };'
);

const nxJson = readJson('nx.json');
const plugin = nxJson.plugins.find((p) =>
typeof p === 'string'
? p === '@nx/oxlint/plugin'
: p.plugin === '@nx/oxlint/plugin'
);
const targetName =
typeof plugin === 'string'
? 'oxlint'
: (plugin.options?.targetName ?? 'oxlint');

const project = JSON.parse(
runCLI(`show project ${inferredViteLib} --json`)
);
expect(project.targets[targetName].options.command).toBe(
`oxlint libs/${inferredViteLib}`
);
expect(() =>
runCLI(`run ${inferredViteLib}:${targetName}`, {
env: { CI: 'false' },
})
).not.toThrow();
}, 300000);

it('should cache repeated OXLint runs', () => {
updateFile(
`libs/${explicitLib}/src/lib/${explicitLib}.ts`,
'export const good = (a: number) => a === 1;\n'
);

runCLI(`run ${explicitLib}:oxlint --verbose`, {
env: { CI: 'false' },
});

const out = runCLI(`run ${explicitLib}:oxlint --verbose`, {
env: { CI: 'false' },
});

expect(out).toMatch(
/local cache|remote cache|existing outputs match the cache/
);
}, 300000);

it('should keep eslint lint target and add oxlint target in hybrid migration', () => {
updateJson('nx.json', (json) => {
json.useInferencePlugins = false;
return json;
});

runCLI(`generate @nx/js:lib libs/${hybridLib} --linter eslint`);
runCLI(`generate @nx/oxlint:convert-from-eslint --project ${hybridLib}`);

const project = readJson(`libs/${hybridLib}/project.json`);
expect(project.targets.lint).toBeDefined();
expect(project.targets.oxlint).toBeDefined();
expect(project.targets.oxlint.executor).toBe('@nx/oxlint:lint');
}, 300000);
});
16 changes: 16 additions & 0 deletions e2e/oxlint/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": [],
"files": [],
"references": [
{
"path": "../utils"
},
{
"path": "./tsconfig.spec.json"
}
]
}
Loading
Loading