-
-
Notifications
You must be signed in to change notification settings - Fork 81
Description
Context
Oxfmt entered beta on 2026-02-24 — a Rust-powered, Prettier-compatible formatter. Combined with oxlint (695 rules, 50-100× faster than ESLint), the Oxc toolchain is now a credible full replacement for Biome.
This issue evaluates migrating from Biome to oxfmt + oxlint.
Current Biome surface in Satus
| Capability | What Biome does today |
|---|---|
| Formatting | JS/TS/JSX/TSX, CSS/CSS Modules, JSON/JSONC |
| Linting | 60+ explicit rules + domain presets (next, react, project) |
| Import sorting | organizeImports action |
| Tailwind sorting | useSortedClasses nursery rule (error) |
| Custom plugins | 3 GritQL: no-anchor-element, no-relative-parent-imports, no-unnecessary-forwardref |
| Overrides | 5 file-type blocks (CSS, TSX, TS, App Router, CSS Modules) |
| Inline suppressions | 29 biome-ignore comments across codebase |
| Unified command | biome check = format + lint + imports in one pass |
Oxc toolchain coverage
oxfmt (formatter)
| Capability | oxfmt | Biome |
|---|---|---|
| JS/TS/JSX/TSX | ✅ 100% Prettier-compatible | ✅ ~97% |
| CSS/SCSS/Less | ✅ | ✅ |
| JSON/JSONC/JSON5 | ✅ | ✅ |
| HTML/Vue/Angular | ✅ | ❌ |
| Markdown/MDX | ✅ | ❌ |
| YAML/TOML/GraphQL | ✅ | ❌ |
| Import sorting | ✅ built-in (customizable groups) | ✅ |
| Tailwind class sorting | ✅ built-in (no plugin) | ✅ nursery rule |
| package.json sorting | ✅ | ❌ |
| CSS-in-JS formatting | ✅ | ❌ |
--migrate biome |
✅ | N/A |
| Speed | 30× Prettier, 3× Biome | Baseline |
oxlint (linter)
| Capability | oxlint | Biome |
|---|---|---|
| Total rules | 695 (108 default, 245 with fixes) | ~200 |
| ESLint core | ✅ 100+ rules | ✅ partial |
| TypeScript | ✅ 100+ rules (via tsgo/TS7) | ✅ custom inference |
| React | ✅ full plugin | ✅ domain preset |
| Next.js | ✅ plugin (no-img-element, no-async-client-component, etc.) |
✅ domain preset |
| jsx-a11y | ✅ 20+ rules | ✅ 12 explicit rules |
| Import rules | ✅ full plugin (incl. no-cycle via module graph) |
✅ partial |
| Unicorn | ✅ 80+ rules | ❌ |
| Type-aware linting | ✅ native via tsgo | ❌ |
| Auto-fix | ✅ --fix (245 rules) |
✅ --write |
| File overrides | ✅ glob-based overrides[] |
✅ |
| Config format | TOML or TS (defineConfig) |
JSON |
| Inline suppression | // oxlint-disable-line |
// biome-ignore |
| Custom plugins | ✅ JS plugins (experimental) | ✅ GritQL plugins |
| Speed | 50-100× ESLint | ~30× ESLint |
Custom GritQL plugin migration — all 3 have native oxlint paths
Our 3 custom Biome GritQL plugins all map to built-in oxlint rules — no custom plugins needed:
1. no-anchor-element → react/forbid-elements (built-in)
Oxlint's react/forbid-elements rule does exactly what our GritQL plugin does — forbids specific JSX elements with a custom error message suggesting the replacement:
{
"react/forbid-elements": ["error", {
"forbid": [{
"element": "a",
"message": "Use <Link> from '@/components/ui/link' instead"
}]
}]
}Our GritQL plugin has special allowances for <a> inside the Link component itself (checks for isExternal/isExternalSSR conditionals). With oxlint, this is handled by a file-level override disabling the rule for the Link component file specifically — simpler and more maintainable.
2. no-relative-parent-imports → import/no-relative-parent-imports (built-in)
Direct equivalent. Oxlint's import plugin has this rule built-in:
{
"import/no-relative-parent-imports": "error"
}Forbids all ../ imports, requiring @/ path aliases. Our GritQL plugin only flags ../../ (two levels deep) while allowing single ../ for colocated files. The oxlint rule is stricter (all parent imports). We'd need to evaluate if the stricter behavior is acceptable, or use a file override for specific cases.
3. no-unnecessary-forwardref → react/forward-ref-uses-ref (built-in, inverted)
Oxlint has react/forward-ref-uses-ref which flags forwardRef calls where the ref parameter isn't used. Our GritQL plugin flags ALL forwardRef usage since React 19 makes it unnecessary. Two options:
- Option A: Use
react/forbid-elementsapproach — not applicable sinceforwardRefisn't a JSX element - Option B: Use
import/no-restricted-importsto ban importingforwardRefentirely:{ "import/no-restricted-imports": ["error", { "paths": [{ "name": "react", "importNames": ["forwardRef"], "message": "forwardRef is unnecessary in React 19. ref is a regular prop." }] }] } - Option C: Write a lightweight JS plugin (ESLint-compatible API, ~15 lines) if the import restriction approach isn't sufficient
Bottom line: Zero GritQL plugins to maintain. All three become native oxlint config.
Rule-by-rule migration feasibility
Biome rules with direct oxlint equivalents
| Biome rule | oxlint equivalent | Status |
|---|---|---|
noExplicitAny |
typescript/no-explicit-any |
✅ |
noUnusedImports |
import/no-unused-imports |
✅ |
noUnusedVariables |
eslint/no-unused-vars |
✅ |
useExhaustiveDependencies |
react/exhaustive-deps |
✅ |
noImgElement |
nextjs/no-img-element |
✅ |
useImportType |
typescript/consistent-type-imports |
✅ |
useAltText |
jsx-a11y/alt-text |
✅ |
useValidAriaProps |
jsx-a11y/aria-props |
✅ |
useValidAriaRole |
jsx-a11y/aria-role |
✅ |
noDoubleEquals |
eslint/eqeqeq |
✅ |
noDebugger |
eslint/no-debugger |
✅ |
noGlobalEval |
eslint/no-eval |
✅ |
useButtonType |
react/button-has-type |
✅ |
noParameterAssign |
eslint/no-param-reassign |
✅ |
useFilenamingConvention |
unicorn/filename-case |
✅ |
noForEach |
unicorn/no-array-for-each |
✅ |
useFlatMap |
unicorn/prefer-array-flat-map |
✅ |
Features handled by oxfmt (no longer lint rules)
| Biome feature | oxfmt equivalent |
|---|---|
useSortedClasses (Tailwind) |
Built-in Tailwind class sorting |
organizeImports |
Built-in import sorting with custom groups |
Operational considerations
Pre-commit (lefthook)
Current (single tool):
biome:
run: bun biome check --write --unsafe {staged_files}Proposed (two tools, parallel via lefthook):
format:
run: bunx oxfmt --write {staged_files}
lint:
run: bunx oxlint --fix {staged_files}Both run in parallel via lefthook — no real complexity increase since lefthook already runs biome + typecheck in parallel.
CI (bun run check)
Current: biome check && tsgo --noEmit && bun test
Proposed: oxfmt --check && oxlint && tsgo --noEmit && bun test
Config files
Current: 1 file (biome.json, 266 lines) + 3 GritQL plugins
Proposed: 2 files (oxfmt.toml + oxlint.config.ts), zero plugins
Recommendation
This is worth a spike. The Oxc toolchain brings real advantages:
- 695 rules vs ~200 — significantly broader coverage including unicorn, full import plugin, type-aware linting via tsgo
- Type-aware linting — Biome uses custom type inference; oxlint uses actual tsgo. Enables rules like no-floating-promises that Biome can't do
- Multi-file analysis — Module graph enables
import/no-cyclewithout performance cliffs - All 3 custom GritQL plugins become native config — zero custom plugin maintenance
- Future-proof — Oxc ecosystem is actively converging (same parser, same resolver, same type checker across tools)
- Speed — 3× faster formatting + 50-100× faster linting (vs ESLint baseline)
Key risk: oxlint JS plugins are still "technical preview" — but since all 3 GritQL use cases map to built-in rules, this risk is mitigated. The main remaining risk is oxfmt beta stability.
Tasks
- Run
oxfmt --migrate biomeand diff output against current formatting - Benchmark oxfmt vs
biome formaton full codebase - Map all 60+ Biome rules to oxlint equivalents (start from table above)
- Test Tailwind class sorting parity in oxfmt (
cn()/clsx()function support) - Test import sorting groups match current order (React → third-party →
@/→ relative → CSS) - Prototype
oxlint.config.tscovering all current Biome lint rules + overrides - Validate
react/forbid-elementsblocks<a>with Link suggestion - Validate
import/no-relative-parent-imports— evaluate stricter behavior (all../vs just../../) - Validate
forwardRefban via restricted imports or equivalent - Prototype lefthook config with dual-tool setup
- Migrate 29
biome-ignorecomments tooxlint-disable-line - Run
bun run buildwith full Oxc toolchain, verify no regressions - Verify CSS Modules linting/formatting compatibility