chore: Adopt oxlint and oxfmt, and modernise the lint/format toolchain#2666
chore: Adopt oxlint and oxfmt, and modernise the lint/format toolchain#2666VincentSmedinga wants to merge 10 commits into
Conversation
Adopt the oxc toolchain: oxlint takes over the natively-supported JS/TS rules and oxfmt fully replaces Prettier. ESLint is retained but reduced -- it now only runs what oxlint cannot (perfectionist sorting, baseline-js, Storybook, JSON/Markdown/MDX linting), with eslint-plugin-oxlint disabling the overlapping rules. - Add .oxlintrc.json (generated via @oxlint/migrate, native rules only) and wire eslint-plugin-oxlint into eslint.config.mjs. - Add .oxfmtrc.json mirroring the previous Prettier style; remove prettier, eslint-config-prettier, .prettierrc.json and .prettierignore. - Convert stale eslint-disable directives to oxlint-disable. - Update scripts (format, lint:format, lint:js, lint-fix:js), lint-staged, the SVGR generate scripts, and contributor docs. MDX is left out of oxfmt for now (Prettier never formatted it); enabling it later is a small config change.
Apply oxfmt across the repo. Only an inline <script> changes: its trailing semicolon is dropped to match the no-semicolon house style.
…Markdown/MDX Move perfectionist, baseline-js and eslint-plugin-storybook into oxlint's jsPlugins loader so oxlint is the single linter for all JavaScript and TypeScript. ESLint now only lints the languages oxlint cannot: JSON, Markdown and MDX. - Add jsPlugins (perfectionist, baseline-js, storybook) to .oxlintrc.json. - Reduce eslint.config.mjs to JSON/Markdown/MDX; the Storybook partial is MDX-only. - Delete eslint.rules.mjs and the React eslint partials; drop the eslint-plugin-oxlint disabler and the now-unused ESLint JS/TS dev deps. - lint-staged runs only oxlint on JS/TS. Note: jsPlugins is alpha. Rules without an oxlint/jsPlugins equivalent are dropped: consistent-return, react/prefer-read-only-props, padding-line-between-statements (generated logos), no-invalid-this.
nano-staged is a zero-dependency, faster drop-in for lint-staged with the same pre-commit behaviour. Move the config from .lintstagedrc.json to .nano-staged.json (identical globs and commands) and run nano-staged from the husky pre-commit hook.
Add a "Linting and formatting" note covering tool ownership (oxlint for JS/TS, ESLint for JSON/Markdown/MDX, Stylelint for CSS, oxfmt for formatting), running the fast targeted tools while iterating, and not hand-formatting or hand-sorting. Add a `pnpm run format` command to the table, name the fast tools in the lint guidance, and correct the React package config-change note.
oxlint loads eslint-plugin-storybook through its jsPlugins loader, and the root ESLint config loads eslint-plugin-mdx (and, for MDX code blocks, eslint-plugin-react). These were resolved only by pnpm hoisting from storybook/ and packages/react/, so a dedupe or hoisting change could break linting at the repository root. Declare them as root devDependencies at the versions the sub-packages already use (no duplicate installs).
The ESLint markdown block spread `markdown.configs.recommended.rules`, but in @eslint/markdown 8 `recommended` is a flat-config array, so `.rules` was undefined and no markdown rules ran — content linting was a silent no-op. Switch to the GFM language with the recommended rules (so task lists and tables parse correctly and don't false-positive), ignore all CHANGELOG.md files, and turn off no-missing-label-refs (noisy on literal brackets; our docs use inline links). Fix the two real issues this surfaced: a fenced code block missing its language in definition-of-done.md and a broken `#guidelines` anchor in the Page Header README.
Coverage Report for React components
File CoverageNo changed files found. |
|
Size Change: 0 B Total Size: 351 kB ℹ️ View Unchanged
|
There was a problem hiding this comment.
Pull request overview
This PR modernises the repo’s linting/formatting toolchain by adopting the oxc stack: oxfmt for formatting and oxlint for JS/TS linting, while reducing ESLint to JSON/Markdown/MDX only and swapping lint-staged for nano-staged.
Changes:
- Replace Prettier with oxfmt (new config + root scripts) and introduce oxlint as the primary JS/TS linter (new config).
- Slim ESLint to JSON/Markdown/MDX and update Storybook/package configs accordingly.
- Replace lint-staged with nano-staged + update docs/guidance for the new tooling.
Reviewed changes
Copilot reviewed 34 out of 35 changed files in this pull request and generated 24 comments.
Show a summary per file
| File | Description |
|---|---|
| storybook/src/utils/Prose/Prose.stories.tsx | Updates lint-disable directive for oxlint in Storybook story helper. |
| storybook/src/pages/public/ProjectPage/ProjectPage.stories.tsx | Converts eslint-disable directive to oxlint-disable in story meta render. |
| storybook/src/pages/public/ProductPage/ProductPage.stories.tsx | Converts eslint-disable directive to oxlint-disable in story meta render. |
| storybook/src/pages/public/NavigationPage/NavigationPage.stories.tsx | Converts multiple eslint-disable directives to oxlint-disable for Storybook render args. |
| storybook/src/pages/public/HomePage/HomePage.stories.tsx | Converts eslint-disable directive to oxlint-disable for Storybook render args. |
| storybook/src/pages/public/FormFlow/FormFlow.stories.tsx | Converts multiple eslint-disable directives to oxlint-disable for Storybook render args. |
| storybook/src/pages/internal/HomePage/HomePage.stories.tsx | Converts eslint-disable directive to oxlint-disable for Storybook render args. |
| storybook/src/components/SearchField/SearchField.stories.tsx | Converts no-alert disable directive to oxlint variant. |
| storybook/src/components/Menu/Menu.stories.tsx | Converts eslint disable directives to oxlint variants for unused vars handling. |
| storybook/src/components/LinkList/LinkList.stories.tsx | Removes now-unneeded eslint-disable directive. |
| storybook/src/components/Grid/Grid.stories.tsx | Removes now-unneeded eslint-disable directive. |
| storybook/src/components/DateInput/DateInput.stories.tsx | Converts eslint-disable directives to oxlint-disable for unused render args. |
| storybook/eslint.config-partial.mjs | Removes JS/TS + Storybook ESLint configs; keeps MDX linting config. |
| storybook/config/preview-head.html | Formatting-only change consistent with oxfmt (semicolon removal). |
| pnpm-lock.yaml | Updates lockfile for added/removed tooling dependencies (oxlint/oxfmt/nano-staged, etc.). |
| packages/react/src/ImageSlider/utils/debounce.test.ts | Converts eslint-disable directive to oxlint-disable in test. |
| packages/react/package.json | Switches logo generation formatting to oxfmt (and keeps a lint step). |
| packages/react/eslint.config-partial.mjs | Removes React/TS ESLint partial config (JS/TS lint moved to oxlint). |
| packages/react/AGENTS.md | Updates agent guidance to reference the new lint/format tools. |
| packages/css/src/components/page-header/README.md | Fixes an internal Markdown anchor link. |
| packages-proprietary/react-icons/package.json | Switches icon generation formatting to oxfmt (and keeps a lint step). |
| packages-proprietary/react-icons/eslint.config-partial.mjs | Removes react-icons ESLint partial config (JS/TS lint moved to oxlint). |
| package.json | Adds format / lint:format; updates JS lint scripts to use oxlint+ESLint; removes Prettier. |
| eslint.rules.mjs | Removes extracted ESLint rule definitions (JS/TS lint moved to oxlint). |
| eslint.config.mjs | Slims ESLint scope to JSON/Markdown/MDX and enables Markdown content linting via GFM recommended config. |
| documentation/definition-of-done.md | Updates docs to mention oxlint/oxfmt; adds missing code fence language. |
| CONTRIBUTING.md | Updates editor tooling guidance to use Oxc VS Code extension and fix-on-save. |
| AGENTS.md | Adds/updates workflow guidance for oxlint/oxfmt and new scripts. |
| .prettierrc.json | Removes Prettier config (replaced by .oxfmtrc.json). |
| .prettierignore | Removes Prettier ignore file (replaced by .oxfmtrc.json ignorePatterns). |
| .oxlintrc.json | Adds oxlint configuration (rules + jsPlugins integrations and overrides). |
| .oxfmtrc.json | Adds oxfmt configuration (style + ignore patterns + overrides). |
| .nano-staged.json | Adds nano-staged config to replace lint-staged pre-commit behavior. |
| .lintstagedrc.json | Removes lint-staged config (replaced by nano-staged). |
| .husky/pre-commit | Switches pre-commit hook to nano-staged. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The generate-logos and generate scripts called `eslint --fix` to auto-fix generated TSX/JS files. ESLint now only covers JSON, Markdown, and MDX, so those calls were a silent no-op. Replace with `oxlint --fix` so that oxlint's auto-fixable rules (import ordering, type imports, etc.) still run on newly generated source files.
Replace the `@typescript-eslint/` prefix with the canonical oxlint names: - `@typescript-eslint/no-unused-vars` → `no-unused-vars` - `@typescript-eslint/no-explicit-any` → `typescript/no-explicit-any` The old names worked as aliases, but keeping the `@typescript-eslint/` prefix is misleading now that ESLint no longer handles TypeScript.
…config The two `react/jsx-no-undef: off` and `react/prop-types: off` entries were defensive placeholders against a React plugin that is no longer loaded by ESLint at all. Removing them also makes the rules override redundant, so collapse the flatCodeBlocks entry back to a name-only override.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 35 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
storybook/src/components/Grid/Grid.stories.tsx:36
cellMetais only referenced viaStoryObj<typeof cellMeta>(type-only), so it may be reported as unused by oxlint’sno-unused-vars. Re-introduce anoxlint-disable-next-line no-unused-varsdirective (matching the pattern used elsewhere) to prevent lint failures.
const cellMeta = {
component: Grid.Cell,
argTypes: {
span: {
control: { max: 12, min: 1, type: 'number' },
},
start: {
control: { max: 12, min: 1, type: 'number' },
},
},
} satisfies Meta<typeof Grid.Cell>
|
/Chromatic test |
There was a problem hiding this comment.
Great work!
I have yet to test and review this properly, but I’m sure the build process will be much faster and the config will be even more consolidated and consistent. And it’s great that Markdown linting now actually works, even though some work on MDX linting/formatting remains.
But a few things from the description I find unfortunate:
- “ESLint is reduced to the languages oxlint can’t lint — JSON, Markdown, and MDX.” -- it would be better if we could get rid of ESLint altogether, now we still have similar tooling for similar tasks. How does this affect the full code quality checking pipeline?
- “Root-level ESLint plugins are declared explicitly (eslint-plugin-storybook, eslint-plugin-mdx, eslint-plugin-react) instead of relying on pnpm hoisting from sub-packages.” -- I see that oxlint uses the JSON format and that cannot import partials. They do not support (M)JS? Because I actually like the separation of concerns we had with package particular rules scoped to that package. Another downside of basic JSON is that there can be no clarifying comments or use comments and newlines to improve structure and readability. The latter downsides could maybe be avoided with JSONC or JSON5.
- “MDX content linting is still off — .mdx is syntax-validated, but its content isn't linted (no remark-lint config). Intentionally out of scope here; tracked as a separate follow-up.” -- Is that issue on the backlog already?
- Rules dropped in the jsPlugins phase (no native/jsPlugins equivalent): consistent-return, react/prefer-read-only-props, padding-line-between-statements (generated logos), @typescript-eslint/no-invalid-this. -- Can you elaborate on this? Why can’t we have these rules any more? We recently added
prefer-read-only-props.
I’m going to test this with Visual Studio Code extensions for ESLint & Prettier that I currently have set up. I think that contributors should ideally be able to continue developing without any or too many workflow disruptions.
Furthermore, I’ll also add other regular contributors (@alimpens, @dlnr, @Evi-2003) as reviewers so that they can test the linting and formatting with their IDEs and set-ups.
|
|
||
| - Before submitting work, cross-check the full definition-of-done checklist in [documentation/definition-of-done.md](documentation/definition-of-done.md). | ||
| - Run the most specific relevant lint/test commands for the package you touched before relying on full `pnpm run lint` / `pnpm run test`. | ||
| - Run the most specific relevant lint/test command — e.g. `oxlint` for JS/TS or `oxfmt --check` for formatting — before relying on the full `pnpm run lint` / `pnpm run test`. |
There was a problem hiding this comment.
This tells me you haven’t included the agent hooks in this PR yet.
But now that I’ve read that part of the article again (see link above) I do see that it is agent vendor specific. Can you do some research if there’s a more generic option that at the very least works with GitHub Copilot?
There was a problem hiding this comment.
I’ve done some testing:
- Linting and formatting from the command-line works flawlessly!
- But I couldn’t rely on the ESLint and Prettier extensions in VS Code any more.
- The Oxc extension does work as expected. But I had to manually look for it; I did not see a prompt to install it.
So, in conclusion:
- I think I can accept this change after you’ve addressed my concerns in the earlier remark.
- It might be wise to warn contributors about that they have to look at their tooling.
- Reviews from regular contributors with other IDEs and set-ups might still be valuable.
RubenSibon
left a comment
There was a problem hiding this comment.
I’ve done a bit more testing by intentionally adding code and formatting mistakes to TableOfContents.tsx.
Most issues were found and automatically fixed by either the Oxc linter or formatter, but the import order rules were not applied:
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/
import type { HeadingProps } from '../Heading'
import { Heading } from '../Heading'
import { forwardRef } from 'react'
import { TableOfContentsLink } from './TableOfContentsLink'
import { clsx } from 'clsx'
import { TableOfContentsList } from './TableOfContentsList'
import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react'Above import order is incorrect.
This is because eslint-plugin-import-x is no longer included in the project. Isn’t that a major limitation? Maybe add it to the ESLint config again for now if Oxc does not support it?
Adopt oxlint and oxfmt, and modernise the lint/format toolchain
What
Moves the design system’s linting and formatting onto the oxc toolchain, with related tooling cleanups:
eslint-plugin-perfectionist,eslint-plugin-baseline-js, andeslint-plugin-storybookthrough oxlint'sjsPluginsloader.lint-staged→nano-staged(zero-dependency, faster) with the same pre-commit behaviour.markdown.configs.recommended.rulesisundefinedin@eslint/markdown8); it now uses the GFM language with the recommended rules, sorequire-alt-text,no-empty-links, etc. are enforced.eslint-plugin-storybook,eslint-plugin-mdx,eslint-plugin-react) instead of relying on pnpm hoisting from sub-packages.AGENTS.mdgains efficient linter/formatter guidance (tool ownership, fast targeted commands, "don't hand-format/sort").Removed:
prettier,eslint-config-prettier,eslint-plugin-oxlint,lint-staged, and the now-unused ESLint JS/TS plugins (@typescript-eslint/*,@vitest/eslint-plugin,eslint-plugin-import-x,eslint-import-resolver-typescript,globals,@eslint/js).Added:
oxlint,oxfmt,nano-staged. The published packages (CSS, tokens, React, assets) are unchanged — this only affects the developer toolchain.Why
ESLint and Prettier were the slowest parts of the local and CI feedback loop; oxlint and oxfmt are Rust-based and much faster, and consolidating on one toolchain removes the ESLint/Prettier coordination. Along the way this surfaced and fixed real gaps: Markdown linting was doing nothing, and the root tooling depended on plugins declared only in sub-packages.
Like many others, we haven’t been able to upgrade to ESLint 10 for a while due to a bloocking issue in eslint-plugin-react. This lead us to research alternatives, where this article pointed us at the oxc toolchain, mentioning “oxfmt now has 1:1 compatibility with Prettier […] and oxlint is also production-ready.”
How
Seven reviewable commits, each independently revertible:
replace ESLint/Prettier tooling with oxlint and oxfmt— generates.oxlintrc.json(@oxlint/migrate), adds.oxfmtrc.jsonmirroring the old Prettier style, converts staleeslint-disable→oxlint-disable.style: format with oxfmt— the one-off oxfmt pass (a single embedded-script semicolon; the codebase already matched).lint all JS/TS with oxlint via jsPlugins, slim ESLint to JSON/Markdown/MDX.replace lint-staged with nano-staged.document efficient linter and formatter use in AGENTS.md.declare the root-level ESLint plugins explicitly.enable Markdown content linting— the GFM/recommended fix, plus the two issues it surfaced (a CSS block missing its language; a broken#guidelinesanchor in the Page Header README).Parity for the JS/TS rules was verified by linting a deliberately-broken file with both tools (identical findings), and the new Markdown rules were verified to flag
require-alt-textandno-empty-links.oxfmthonours<!-- prettier-ignore -->(its documented Markdown ignore syntax), so those comments were kept.CI needs no workflow change — a new
lint:formatscript is picked up by the existingpnpm run lintaggregator.Checklist
definition-of-done.md,CONTRIBUTING.md(VS Code extension → Oxc), and two doc fixes surfaced by Markdown linting./chromatic testand verify visual regression tests pass — no visual changes expected (only lint-directive comments in story files); worth running to confirm.Additional notes
Performance (median of 3, Apple M1 Max):
prettier --check .4.39soxfmt --check .0.54slint:js(the command devs run)eslint .10.36soxlint && eslint .4.68spnpm run lint(full)For reviewers to weigh in on:
jsPluginsloader is alpha (its own schema notes "not subject to semver") and is now the only thing linting our JS/TS. It's isolated in commit 3, sogit revert <commit-3>falls back to a fully-stable setup (oxlint native rules + a reduced ESLint) without touching the Prettier → oxfmt work.consistent-return,react/prefer-read-only-props,padding-line-between-statements(generated logos),@typescript-eslint/no-invalid-this..mdxis syntax-validated but its content isn't linted (noremark-lintconfig). Intentionally out of scope here; tracked as a separate follow-up.