Skip to content

chore: replace ESLint with oxlint (type-aware) and run lint in CI - #845

Merged
jhuleatt merged 1 commit into
mainfrom
chore/oxlint
Oct 29, 2026
Merged

jhuleatt merged 1 commit into
mainfrom
chore/oxlint

Conversation

@jhuleatt

Copy link
Copy Markdown
Collaborator

Summary

Replaces ESLint 8 and its plugins (@typescript-eslint v5, eslint-plugin-react, eslint-plugin-react-hooks v4, eslint-plugin-no-only-tests) with oxlint, including type-aware linting via oxlint-tsgolint. Also runs npm run lint in the test workflow before the tests.

On main, npm run lint currently fails (a require() in test/setupTests.ts), which is presumably why it wasn't in CI.

Rules

.oxlintrc.json lists rules explicitly. oxlint's categories are turned off, so upgrading oxlint doesn't change what's enforced. It's a rule-by-rule port of the latest recommended presets, keeping this repo's existing warn-level overrides:

  • ESLint recommended (minus the rules TypeScript already checks)
  • typescript-eslint recommended
  • eslint-plugin-react recommended (classic JSX runtime, matching "jsx": "react" in tsconfig)
  • eslint-plugin-react-hooks v7 recommended, including the React Compiler rules
  • vitest/no-focused-tests instead of eslint-plugin-no-only-tests

Type-aware rules from typescript-eslint's recommended-type-checked:

  • no-floating-promises, no-misused-promises, await-thenable, unbound-method, only-throw-error (promises allowed, for Suspense) and the rest of that preset are errors.
  • no-redundant-type-constituents, no-unnecessary-type-assertion and restrict-template-expressions have existing findings, so they're warnings for now. They can become errors once those are cleaned up.
  • no-deprecated (warning) replaces react/no-deprecated. It flags anything marked @deprecated in its types (e.g. RxJS's toPromise and empty, Vite 8's rollupOptions).
  • The no-unsafe-* rules are off. They flag every use of the any values that no-explicit-any already allows as a warning. require-await is off too (stylistic).

Not ported:

  • react/prop-types: TypeScript covers it.
  • react/jsx-uses-react and react/jsx-uses-vars: oxlint's no-unused-vars already handles JSX.
  • no-octal and no-dupe-args: strict mode covers them.
  • react/require-render-return and no-useless-assignment: still experimental in oxlint.

Code changes for new findings

  • test/setupTests.ts: require → import.
  • test/sdk.test.tsx, test/firebaseApp.test.tsx: await deleteApp in afterEach (no-misused-promises).
  • src/performance.tsx: suppressed react/static-components with a TODO. SuspenseWithPerf creates its fallback component during render, so React remounts it, and records an extra measure, whenever it re-renders while suspended. Fixing that changes runtime behavior, so it's left for a follow-up.
  • Disable comments for one intentional pattern in a test (react/globals) and one false positive (only-throw-error: error is always set when status === 'error').
  • test/tsconfig.json (extends tsconfig.test.json): type-aware rules use the nearest tsconfig.json, so without it, test files fell back to defaults (e.g. vi resolved to an error type). Editors pick it up too.
  • docs/reference: regenerated for a line number shift.
  • CONTRIBUTING.md: npm run lint --fix doesn't pass --fix through; changed it to npm run lint -- --fix.

Results

  • npm run lint: 0 errors, 89 warnings, ~0.3s (ESLint took ~2s).
  • Lockfile: 98 packages removed, oxlint and oxlint-tsgolint added (plus their platform binaries).
  • Locally: tsc (src and tests), the full emulator test suite, npm run docs, and npm ci + lint on Node 22 and 24 all pass.

Replace ESLint 8, @typescript-eslint v5, eslint-plugin-react,
eslint-plugin-react-hooks and eslint-plugin-no-only-tests with oxlint,
including type-aware rules via oxlint-tsgolint.

- .oxlintrc.json ports the current recommended presets (ESLint,
  typescript-eslint, eslint-plugin-react, eslint-plugin-react-hooks v7
  including the React Compiler rules) rule by rule, keeping the existing
  warn-level overrides. Categories are off so oxlint upgrades don't change
  what's enforced.
- Add typescript-eslint's recommended-type-checked rules. Rules with
  existing findings are warnings for now; the no-unsafe-* rules and
  require-await stay off. typescript/no-deprecated (warn) replaces
  react/no-deprecated.
- vitest/no-focused-tests replaces eslint-plugin-no-only-tests.
- Add test/tsconfig.json so type-aware rules (which use the nearest
  tsconfig.json) resolve test files with the test compiler settings.
- Fix findings: require -> import in test/setupTests.ts (already failing
  lint on main), and await deleteApp in two afterEach hooks.
- Suppress react/static-components in SuspenseWithPerf with a TODO
  (fixing it changes runtime behavior), plus one intentional pattern in a
  test and one only-throw-error false positive.
- Run `npm run lint` in the test workflow before the tests.
- Fix the `npm run lint -- --fix` instruction in CONTRIBUTING.md.
- Regenerate reference docs (line number shift).

@tyler-reitz tyler-reitz left a comment

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.

Approving. I think switching is the right call for this repo. Speed isn't the reason (under a second vs 1.6s here). The win is getting type-aware rules and the React Compiler hook rules without maintaining an ESLint 9 + typescript-eslint + plugin stack. The tradeoff to watch: type-aware linting runs on oxlint-tsgolint, which is built on TypeScript 7 (typescript-go), while the repo compiles with TypeScript 6.0.3. So lint and tsc can disagree, and tsgolint upgrades follow TS releases. I didn't hit a disagreement here.

On moving to VoidZero tools (now part of Cloudflare): we already depend on Vite 8 (whose bundler, Rolldown, is built on Oxc) and Vitest, so this adds to a dependency we have rather than starting a new one. Switching back would also be cheap, since every rule is listed explicitly under its ESLint name (plugin rules only need a prefix rename).

On the rebased head, lint exits 0 locally and in CI, and both tsc configs are clean. It does catch things: a probe file with .only and an un-awaited promise fails the run with both reported as errors, and misspelled rule names fail it too.

Non-blocking:

  • Lint runs in every test matrix leg. If it fails, that leg's tests are skipped.
  • The no-unsafe-* rules would add 76 findings, but only 21 in src (the rest are in test/). They could go in as warnings.

@jhuleatt
jhuleatt merged commit 9382f03 into main Oct 29, 2026
20 checks passed
@jhuleatt
jhuleatt deleted the chore/oxlint branch October 29, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants