Skip to content
Fran Gonzalez
← Back to blog
(updated Sep 30, 2026)·Clanker·4 min read

Replacing ESLint with Oxlint in an Astro site

How I migrated an Astro blog from ESLint and typescript-eslint to type-aware Oxlint, and why astro check still blocks TypeScript 7 here.

Some matmuls wrote this slop, sorry. My goal with this content is to document some work I (a real human bean) do while poking the Clanker, and try to learn something along the way.

I migrated my Astro blog from ESLint to type-aware Oxlint. The lint half of the TypeScript 7 story carried over cleanly from the Tauri app I migrated earlier; the compiler half stalled on Astro’s own TypeScript gate.

Note

Tested with Astro 7, Oxlint 1.82.0, and TypeScript 6.0.3 on 2026-09-12.

The Problem

This site linted with eslint plus typescript-eslint’s recommended set, six explicitly enabled type-aware rules (no-unsafe-argument, no-unsafe-assignment, no-unsafe-call, no-unsafe-member-access, no-unsafe-return, no-explicit-any), and a dedicated tsconfig.eslint.json to feed the parser type information.

typescript-eslint cannot survive TypeScript 7: the typescript@7 package no longer ships the JS Compiler API its type-aware rules run on. I had already replaced it with Oxlint’s tsgolint-driven type-aware mode in the other repo, so the question here was whether an Astro project with a TypeScript ESLint config (eslint.config.ts) migrates as cleanly.

What Changed

The migration tool accepts the TypeScript config path directly:

pnpm dlx @oxlint/migrate eslint.config.ts --type-aware --details

It produced a .oxlintrc.json with 29 rules and nothing skipped. All six custom type-aware rules exist in Oxlint’s type-aware set; I verified that first with a canary file before touching dependencies, since losing no-unsafe-* coverage would have been a silent security regression:

// canary.ts: 7 errors across 6 rules, proving type-aware coverage
declare function getAny(): any;
export function run() {
  const assigned: any = getAny();
  getAny();
  const member = getAny().prop;
  return getAny();
}
export const explicit: any = 1;

The interesting Astro-specific wrinkle was tsconfig.eslint.json. It existed only to give ESLint’s parser a project spanning scripts/, tests/, and src/ for type information. Oxlint ignores it and auto-discovers the nearest tsconfig.json per file, which worked here for one non-obvious reason: Astro’s default root tsconfig.json has no include array, so it defaults to **/* and covers every directory the ESLint config did. If the root tsconfig had been narrowed to src/, type-aware rules would have silently lost coverage outside it. I confirmed with canaries in both src/ and scripts/ before deleting tsconfig.eslint.json.

Two housekeeping notes for anyone doing the same:

  • The .astro/ generated directory needs to stay in ignorePatterns (the migration tool carried it over automatically).
  • knip initially flagged oxlint-tsgolint as needing an ignoreDependencies entry; after the migration it resolves the package through Oxlint’s config, and the entry became a “remove this” hint instead.

The dependency diff removed eslint, typescript-eslint, @typescript-eslint/parser, @typescript-eslint/eslint-plugin, and eslint-config-prettier (Oxlint does no stylistic rules; Prettier keeps that job), and added oxlint 1.82.0 with oxlint-tsgolint 7.0.2001.

Results

Measured on this repo (41 lintable files), before → after:

  • Lint: eslint . 2.3s → oxlint ~0.3s with type-aware rules active
  • Lockfile: −500 lines net
  • Gates unchanged: astro check, markdownlint-cli2, Vitest (21 tests), Prettier, and Playwright all pass as before

What TS7 Needs Here

Upgrading the compiler is a separate decision in an Astro project. astro check runs through @astrojs/check, which peers typescript ^5.0.0 || ^6.0.0. That is the same shape of blocker typescript-eslint posed in the other repo: a tool that depends on the JS Compiler API. So this site now runs Oxlint with typescript still at 6.0.3, and the compiler bump waits on Astro shipping TS7 support. When it does, the lint side needs no further changes.

What I’d Do Differently

Nothing went wrong this time, mostly because the first migration taught me the checks: verify rule coverage with canaries before swapping dependencies, and validate the generated JSON for duplicate keys (@oxlint/migrate regenerating a file I had hand-edited caused that once). I also re-ran knip against the stashed pre-migration state to confirm its two remaining findings existed before my changes, which kept the scope of the migration honest.

References

This post was written with AI assistance.