Skip to content

Build: Migrate ESLint to oxlint for 50-100x faster linting#34170

Closed
kasperpeulen wants to merge 6 commits into
nextfrom
kasper/migrate-eslint-to-oxlint
Closed

Build: Migrate ESLint to oxlint for 50-100x faster linting#34170
kasperpeulen wants to merge 6 commits into
nextfrom
kasper/migrate-eslint-to-oxlint

Conversation

@kasperpeulen
Copy link
Copy Markdown
Member

@kasperpeulen kasperpeulen commented Mar 17, 2026

What I did

Replace ESLint with oxlint for ~50-100x faster linting. Lints 2935 files in ~1.5 seconds (ESLint took 30s+). Moves all lint configuration from code/ and scripts/ to a single .oxlintrc.json at root.

Performance

ESLint oxlint
Time (2935 files) ~30s+ ~1.5s
Rules active ~80 115
Config files 9 scattered 1 at root

How plugins are handled

oxlint has two plugin mechanisms:

  1. Native Rust plugins (50-100x faster) — typescript, react, import, jsx-a11y, and others are built into oxlint
  2. JS plugins via jsPlugins (alpha, ESLint v9+ compatible) — loads standard ESLint plugins as JavaScript, runs them alongside native rules

We use both:

Plugin Mechanism Status
typescript Native (built-in) All rules preserved
react / react-hooks Native (built-in) All rules preserved
import Native (built-in) All rules preserved
jsx-a11y Native (built-in) All rules preserved
eslint-plugin-storybook jsPlugins Recovered — 10 recommended rules for stories
eslint-plugin-playwright jsPlugins Recovered — 11 rules for e2e tests
local-rules (custom) jsPlugins Recovered — all 3 custom rules

Note on jsPlugins alpha: The jsPlugins feature is marked alpha and not subject to semver. However, it uses ESLint's standard rule API — our plugins are simple rule objects, not complex configs. If jsPlugins breaks in a future oxlint release, we can temporarily disable the 3 jsPlugin entries and lose only those specific rules, without affecting the 100+ native rules. The fallback is straightforward.

Dependency ban approach

The old depend/ban-dependencies rule was investigated but not re-added via jsPlugins because:

  • Its preset bans 50+ packages (execa, slash, globby, glob, ora, etc.) that the codebase legitimately uses
  • This required 86 eslint-disable comments just to suppress the preset — pure noise
  • Our custom ban list (lodash, chalk, qs, handlebars, fs-extra) is already covered by oxlint's native no-restricted-imports with allowTypeImports support
  • no-restricted-imports actually provides better coverage for our custom ban list: it includes ban messages explaining what to use instead, and allowTypeImports: true for react-aria-components and es-toolkit (matching the old @typescript-eslint/no-restricted-imports config)

Deliberate rule decisions

  • react-hooks/exhaustive-deps: off — oxlint's auto-fix for this rule is dangerous: it silently removes dependencies from hook arrays (e.g. [store][]), which can introduce subtle bugs. Disabled to prevent accidental breakage via oxlint --fix.
  • consistent-type-imports: off for .vue/.svelte — framework SFC files define types in <script> blocks where the rule produces false positives
  • no-restricted-imports: off for **/template/** — template files intentionally import banned packages for testing (e.g. NodeModuleMocking.stories.js imports lodash-es)

Warning count

The lint output shows ~3500 warnings and 0 errors. This is intentional — the warnings match the old ESLint severity levels:

  • typescript/no-explicit-any: ~2600 warnings (was warn in ESLint too)
  • typescript/no-unused-vars: ~300 warnings (was warn in ESLint too)
  • local-rules/no-uncategorized-errors: ~255 warnings (was warn in ESLint too)
  • Various other warn-level rules

These were never blocking in the old ESLint setup either. The lint command exits 0 (success) as long as there are no errors.

What changed

Added:

  • .oxlintrc.json at root — single config with native plugins + jsPlugins
  • oxlint, lint-staged, eslint-plugin-playwright in root package.json
  • Root-level scripts: yarn lint, yarn lint:fix, yarn lint:prettier

Removed:

  • code/.eslintrc.js, code/.eslintignore
  • scripts/.eslintrc.cjs, scripts/.eslintignore
  • 5 sub-package .eslintrc.json files
  • ~20 ESLint plugin dependencies from scripts/package.json
  • eslint, cross-env, lint-staged from code/package.json
  • 91 stale eslint-disable comments (86 depend/ban-dependencies, 4 compat/compat, 1 eslint-comments)

Changed:

  • Pre-commit hook: consolidated from 2 lint-staged runs (cd code && yarn lint-staged + cd scripts && yarn lint-staged) to 1 run at root (yarn lint-staged)
  • CI fork-checks: cd code && yarn lint:prettier --check .yarn lint:prettier (now checks both code/ and scripts/)
  • code/package.json ci-tests script: yarn lintyarn --cwd .. lint (delegates to root)

Kept:

  • code/lib/eslint-plugin/ (eslint-plugin-storybook) — user-facing npm package, untouched
  • Template .eslintrc.json files — these ship to users
  • All remaining eslint-disable comments — oxlint respects them for compatibility

Remaining true losses (no oxlint equivalent)

Rule Impact Notes
eslint-plugin-compat Low Only 4 inline disables existed; modern browserslist targets Chrome 131+, Safari 18.3+
eslint-comments/no-unused-disable Medium Detects stale disable comments; no equivalent exists in any tool
@typescript-eslint/dot-notation Low Style rule enforcing obj.prop over obj['prop']; TypeScript catches the important cases
react/destructuring-assignment Low Was "warn" only; style preference for const { x } = props

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Caution

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

  1. yarn install
  2. yarn lint — should complete in ~1-2 seconds with 0 errors (~3500 warnings expected)
  3. yarn lint:fix — should auto-fix safe issues
  4. Make a change to a .ts file and commit — pre-commit hook should run oxlint via lint-staged

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • New Features

    • Integrated OxLint as the primary linting tool for improved performance and consistency.
  • Chores

    • Migrated linting configuration to a centralized, unified setup across the repository.
    • Simplified lint scripts and workflows for easier development experience.
    • Removed deprecated linting directives from source files to reduce code clutter.
    • Consolidated package manager dependencies by removing unused linting packages.

Replace ESLint with oxlint for ~50-100x faster linting (2935 files in ~1.4s).
Move lint configuration from code/ and scripts/ to root level.

- Add .oxlintrc.json at root with rules matching the old ESLint config
- Add oxlint and lint-staged to root package.json devDependencies
- Move lint scripts to root: `yarn lint`, `yarn lint:fix`, `yarn lint:prettier`
- Move lint-staged config and pre-commit hook to root level
- Remove ESLint config files (.eslintrc.js, .eslintrc.cjs, .eslintrc.json, .eslintignore)
- Remove ESLint and all ESLint plugin dependencies from scripts/package.json
- Remove ESLint dependency from code/package.json
- Update fork-checks CI workflow
- Update AGENTS.md with new lint commands
- Merge depend/ban-dependencies rules into no-restricted-imports
- Keep eslint-plugin-storybook package (user-facing, not for internal linting)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces many per-package ESLint configs/ignore files with a new root OxLint config, adds root lint/prettier scripts and lint-staged, removes nested lint configs/ignore files, simplifies CI and husky lint invocations, and removes many inline ESLint suppression comments; two small destructuring tweaks remain.

Changes

Cohort / File(s) Summary
Root lint config & scripts
package.json, .oxlintrc.json
Adds a top-level OxLint config and new root lint/prettier scripts (yarn lint, yarn lint:fix, yarn lint:prettier, yarn lint:prettier:fix), a postinstall: husky hook, top-level lint-staged, and devDependencies for oxlint, lint-staged, and ESLint plugins.
CI & Git hooks
.github/workflows/fork-checks.yml, .husky/pre-commit, scripts/ci/common-jobs.ts
Simplifies/prettier and lint invocation (removes cd code and explicit --check), consolidates lint steps in CI, and makes pre-commit call yarn lint-staged without changing directories.
Removed nested ESLint configs
code/.eslintrc.js, code/core/.eslintrc.json, code/frameworks/.../.eslintrc.json, code/lib/.../.eslintrc.*, scripts/.eslintrc.cjs
Deletes many per-package ESLint configuration files and their rule overrides. Review for lost custom rule exceptions and overrides.
Removed/changed ESLint ignore files
code/.eslintignore, scripts/.eslintignore
Removes large sets of ignore patterns, bringing many build/test/docs paths back into lint scope. Check noise and CI impact.
Package-level lint removals & adjustments
code/package.json, scripts/package.json
Removes nested lint scripts, lint-staged blocks, and many ESLint-related dependencies from package-level package.json files; updates code/package.json CI script to call root lint.
Many lint-suppression comment removals
code/**, scripts/**, code/core/src/**, code/lib/cli-storybook/src/**, ...
Deletes numerous /* eslint-disable */ and // eslint-disable-next-line comments (mostly depend/ban-dependencies and a few eslint-comments) across many files — impacts static linting, not runtime.
Minor code tweaks
code/core/src/components/components/Card/Card.tsx, code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts
Small destructuring/rename cleanups with no behavioral changes.
Docs update
AGENTS.md
Updates developer lint/typecheck instructions to reference oxlint and new lint/prettier script names.
Misc small edits
code/.storybook/..., code/addons/onboarding/src/Onboarding.tsx, code/addons/onboarding/src/Survey.tsx, others
Removal of a few top-of-file ESLint disable comments near imports or userAgent usage; no runtime changes but may surface lint warnings/errors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
.oxlintrc.json (1)

22-22: Re-enable no-console for normal code paths.

Line 22 turns off no-console globally, which removes lint enforcement for the logger policy. Consider enabling it globally and keeping relaxed behavior only in test/template overrides.

Suggested tightening
-    "no-console": "off",
+    "no-console": "warn",

As per coding guidelines: "Use storybook/internal/node-logger for server-side logging instead of raw console.* calls", "Use storybook/internal/client-logger for client-side logging instead of raw console.* calls", and "Avoid console.log, console.warn, and console.error in normal code paths unless the file is isolated enough that importing the logger is not reasonable".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.oxlintrc.json at line 22, The global lint rule "no-console" in
.oxlintrc.json is currently disabled; change its value from "off" to "error" (or
"warn") to re-enable prohibition of console.* in normal code paths and
add/retain specific overrides for test/template files where console usage is
allowed; ensure the rule key "no-console" is set at the root config and that any
existing "overrides" array contains entries (e.g., matching test/**/*.js or
templates/**) that explicitly set "no-console": "off" so only those paths remain
relaxed while all other code must use the prescribed logger modules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@AGENTS.md`:
- Line 239: Update AGENTS.md to restore and align the per-file linting guidance
with the project's required command: replace or augment the current single-line
instruction referencing `yarn lint:fix` by adding the per-file JS/TS lint
command `yarn --cwd code lint:js:cmd <file-relative-to-code-folder> --fix` (you
may keep `yarn lint:fix` as a global option) so the document explicitly
instructs developers to run `yarn --cwd code lint:js:cmd
<file-relative-to-code-folder> --fix` before submitting changes; ensure the file
mentions both commands and clearly labels the per-file command as the required
workflow.

---

Nitpick comments:
In @.oxlintrc.json:
- Line 22: The global lint rule "no-console" in .oxlintrc.json is currently
disabled; change its value from "off" to "error" (or "warn") to re-enable
prohibition of console.* in normal code paths and add/retain specific overrides
for test/template files where console usage is allowed; ensure the rule key
"no-console" is set at the root config and that any existing "overrides" array
contains entries (e.g., matching test/**/*.js or templates/**) that explicitly
set "no-console": "off" so only those paths remain relaxed while all other code
must use the prescribed logger modules.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0762d574-abd6-4125-adc6-8aff1fcabd9c

📥 Commits

Reviewing files that changed from the base of the PR and between 546aece and 0c0ab4e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (18)
  • .github/workflows/fork-checks.yml
  • .husky/pre-commit
  • .oxlintrc.json
  • AGENTS.md
  • code/.eslintignore
  • code/.eslintrc.js
  • code/core/.eslintrc.json
  • code/core/src/components/components/Card/Card.tsx
  • code/frameworks/nextjs-vite/.eslintrc.json
  • code/frameworks/nextjs/.eslintrc.json
  • code/lib/cli-storybook/.eslintrc.cjs
  • code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts
  • code/lib/create-storybook/.eslintrc.cjs
  • code/package.json
  • package.json
  • scripts/.eslintignore
  • scripts/.eslintrc.cjs
  • scripts/package.json
💤 Files with no reviewable changes (11)
  • code/.eslintrc.js
  • code/lib/create-storybook/.eslintrc.cjs
  • scripts/package.json
  • scripts/.eslintrc.cjs
  • scripts/.eslintignore
  • code/frameworks/nextjs-vite/.eslintrc.json
  • code/lib/cli-storybook/.eslintrc.cjs
  • .husky/pre-commit
  • code/.eslintignore
  • code/core/.eslintrc.json
  • code/frameworks/nextjs/.eslintrc.json

Comment thread AGENTS.md

1. Format with `yarn prettier --write <file>`
2. Lint with `yarn --cwd code lint:js:cmd <file-relative-to-code-folder> --fix` or `cd code && yarn lint:js:cmd <file-relative-to-code-folder>`
2. Lint with `yarn lint:fix` (runs oxlint with auto-fix on code/ and scripts/)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep AGENTS lint guidance aligned with the required per-file JS/TS lint workflow.

Line 239 now documents only yarn lint:fix, which diverges from the required per-file command and can make single-file linting guidance inconsistent.

As per coding guidelines: "Lint JavaScript/TypeScript files with yarn --cwd code lint:js:cmd <file-relative-to-code-folder> --fix before submitting changes".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` at line 239, Update AGENTS.md to restore and align the per-file
linting guidance with the project's required command: replace or augment the
current single-line instruction referencing `yarn lint:fix` by adding the
per-file JS/TS lint command `yarn --cwd code lint:js:cmd
<file-relative-to-code-folder> --fix` (you may keep `yarn lint:fix` as a global
option) so the document explicitly instructs developers to run `yarn --cwd code
lint:js:cmd <file-relative-to-code-folder> --fix` before submitting changes;
ensure the file mentions both commands and clearly labels the per-file command
as the required workflow.

@kasperpeulen kasperpeulen changed the title Migrate ESLint to oxlint for 50-100x faster linting Build: Migrate ESLint to oxlint for 50-100x faster linting Mar 17, 2026
@kasperpeulen kasperpeulen added build Internal-facing build tooling & test updates ci:normal labels Mar 17, 2026
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Mar 17, 2026

View your CI Pipeline Execution ↗ for commit ae93a73

Command Status Duration Result
nx run-many -t compile,check,knip,test,pretty-d... ⛔ Cancelled 4m 18s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-17 09:21:44 UTC

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Mar 17, 2026

View your CI Pipeline Execution ↗ for commit 0c0ab4e


☁️ Nx Cloud last updated this comment at 2026-03-17 07:59:07 UTC

- Add allowTypeImports to no-restricted-imports for react-aria-components
  and es-toolkit (matching old ESLint behavior)
- Add typescript/no-wrapper-object-types and no-unsafe-function-type rules
  (were "warn" in old config, missing from migration)
- Restore EJS linting in root lint-staged config
Use oxlint's jsPlugins feature to load ESLint plugins that were lost in migration:

- eslint-plugin-storybook: all recommended rules for stories
- eslint-plugin-playwright: e2e test best practices (19 spec files)
- local-rules: 3 custom rules (no-uncategorized-errors, storybook-monorepo-imports,
  no-duplicated-error-codes)

This recovers all previously "lost" plugins without needing ESLint installed.
Clean up disable comments for rules that no longer exist:
- 86 depend/ban-dependencies comments (plugin removed, replaced by no-restricted-imports)
- 4 compat/compat comments (plugin removed)
- 1 eslint-comments/no-unlimited-disable comment (plugin removed)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.oxlintrc.json (1)

75-125: Consider hardening lodash and lodash-es restrictions to block subpath imports.

Lines 100-105 restrict only exact roots (lodash, lodash-es). Subpath imports like lodash-es/add and lodash-es/sum currently bypass these restrictions. Adding patterns field closes this gap.

Note: Test files (code/core/template/stories/test/NodeModuleMocking.stories.js) intentionally demonstrate mocking of lodash-es/add and lodash-es/sum for documentation purposes. If patterns are added, these test examples may require exclusion or the overrides section may need updating.

Suggested change
     "no-restricted-imports": [
       "error",
       {
         "paths": [
           {
             "name": "lodash",
             "message": "lodash is banned. Use es-toolkit or native alternatives."
           },
           {
             "name": "lodash-es",
             "message": "lodash-es is banned. Use es-toolkit or native alternatives."
           }
-        ]
+        ],
+        "patterns": ["lodash/*", "lodash-es/*"]
       }
     ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.oxlintrc.json around lines 75 - 125, The no-restricted-imports rule
currently bans only the exact module roots "lodash" and "lodash-es" but allows
subpath imports like "lodash-es/add"; update the "no-restricted-imports"
configuration for the "lodash" and "lodash-es" entries to include a "patterns"
field that blocks subpath imports (e.g., "lodash/*" and "lodash-es/*") so
submodule imports are prohibited as well, and ensure you update or add an
override to exempt the documented test file
(code/core/template/stories/test/NodeModuleMocking.stories.js) if those examples
must remain permitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.oxlintrc.json:
- Around line 75-125: The no-restricted-imports rule currently bans only the
exact module roots "lodash" and "lodash-es" but allows subpath imports like
"lodash-es/add"; update the "no-restricted-imports" configuration for the
"lodash" and "lodash-es" entries to include a "patterns" field that blocks
subpath imports (e.g., "lodash/*" and "lodash-es/*") so submodule imports are
prohibited as well, and ensure you update or add an override to exempt the
documented test file
(code/core/template/stories/test/NodeModuleMocking.stories.js) if those examples
must remain permitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72e032ef-7b63-4a29-ba53-a976d62b4a64

📥 Commits

Reviewing files that changed from the base of the PR and between 7a88e94 and 41e016e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (2)
  • .oxlintrc.json
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

- CRITICAL: Fix scripts/ci/common-jobs.ts lint job referencing deleted scripts
- Fix react-hooks/ rule prefix to react/ (oxlint uses react/ for all react rules)
- Add eslint-plugin-storybook and sort-package-json to root devDependencies
- Add storybook/use-storybook-expect: off override for core stories
- Add storybook/no-renderer-packages: off override for renderer stories
- Add *.compat.* files to local-rules exclusion
- Remove orphaned eslint-enable comment in preview.tsx
Remove 20 unnecessary "off" rules that aren't default-on, 4 unnecessary
overrides (angular no-useless-constructor, test no-empty-function,
stories no-console, .d.ts no-var), 2 redundant "warn" rules that match
defaults (no-wrapper-object-types, jsx-a11y/no-autofocus), the env block,
duplicate ignorePattern, and the lint:prettier:fix script.

262 lines → 183 lines, same 115 rules, same 0 errors.
@kasperpeulen kasperpeulen marked this pull request as draft March 17, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Internal-facing build tooling & test updates ci:normal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants