Skip to content

Comments

Chore use a sane linter#31

Closed
Atchferox wants to merge 4 commits intoIGNE-Agency:mainfrom
Atchferox:chore-use-a-sane-linter
Closed

Chore use a sane linter#31
Atchferox wants to merge 4 commits intoIGNE-Agency:mainfrom
Atchferox:chore-use-a-sane-linter

Conversation

@Atchferox
Copy link
Contributor

@Atchferox Atchferox commented Jan 13, 2026

🔧 Migrate from Biome to Oxlint + Oxfmt

Summary

This PR replaces Biome (lint + format) and Prettier with Oxlint (type-aware linting) and Oxfmt (formatting), streamlining our tooling to the Oxc ecosystem.

Why Oxlint + Oxfmt?

  • Blazing fast – Oxlint is significantly faster than ESLint; Oxfmt is ~30x faster than Prettier and ~2x faster than Biome
  • Type-aware linting – Leverages TypeScript's type system for smarter, more accurate lint rules
  • Unified ecosystem – Both tools are part of the Oxc project, ensuring consistent behavior and maintenance
  • Prettier-compatible – Oxfmt matches Prettier's JavaScript formatting, making migration seamless
  • Built-in features – Import sorting, Tailwind CSS class sorting, and embedded formatting (CSS-in-JS) come out of the box

What Changed

Removed:

  • @biomejs/biome
  • prettier
  • prettier-plugin-css-order
  • biome.json
  • .prettierrc

Added:

  • oxlint – Type-aware linting
  • oxfmt – Formatting (JS, TS, JSON, SCSS, HTML, Markdown, etc.)
  • .oxlintrc.jsonc – Linter configuration with categories and rules
  • .oxfmtrc.jsonc – Formatter configuration (ported from Prettier)
  • .vscode/settings.json – Editor integration for Oxc extensions

Lefthook Pre-commit

Updated to run:

  • oxlint --type-aware on all staged files
  • oxfmt --check on all staged files

Configuration Parity

The new setup mirrors our previous Biome rules:

  • eslint/no-console: error
  • eslint/no-unused-vars: warn
  • Categories enabled: correctness, suspicious
  • Ignored: node_modules, *.d.ts, *.gen.ts

It does introduce a sane print width of 100 (default for oxfmt)

Related

@maanlamp
Copy link
Collaborator

maanlamp commented Jan 13, 2026

Great suggestion. @publicJorn what do you think?

I like that this is an all-in-one solution, like Biome, but it seems to support scss as well. This alone is a great reason to ditch our current lint/format chain IMO. I think the oxc ecosystem has a better attitude towards non-standard-but-widespread-usage stuff as well as the push for vanilla & native.

I see there are some experimental settings enabled, some pre 1.0.0 releases being used, and some installed package versions aren't pinned. I'm not comfortable with merging any of those things into our production template, so before accepting we need to weigh these options. Would removing those settings be a reason not to go forward with this? Or worded differently: what do these experimental/prerelease features bring to the table?

Also, it seems you accidentally included changes from your other PR?

- Replace Biome with Oxlint (type-aware) for linting
- Replace Prettier with Oxfmt for formatting (including SCSS)
- Update lefthook pre-commit hooks
- Update VSCode settings for Oxc extension
- Remove Base UI dependencies and revert to original form components
- Update README with Oxc setup instructions
@Atchferox Atchferox force-pushed the chore-use-a-sane-linter branch from 2b4cf97 to fcc223a Compare January 13, 2026 18:13
- Removed glob pattern from the Oxlint job in lefthook.yml to streamline pre-commit hook execution.
@Atchferox
Copy link
Contributor Author

Atchferox commented Jan 14, 2026

Great suggestion. @publicJorn what do you think?

I like that this is an all-in-one solution, like Biome, but it seems to support scss as well. This alone is a great reason to ditch our current lint/format chain IMO. I think the oxc ecosystem has a better attitude towards non-standard-but-widespread-usage stuff as well as the push for vanilla & native.

I see there are some experimental settings enabled, some pre 1.0.0 releases being used, and some installed package versions aren't pinned. I'm not comfortable with merging any of those things into our production template, so before accepting we need to weigh these options. Would removing those settings be a reason not to go forward with this? Or worded differently: what do these experimental/prerelease features bring to the table?

Also, it seems you accidentally included changes from your other PR?

vercel/next.js#85451
Don't think the experimental stuff is going to be an issue, lot of large codebases from mayor players are switching

also the oxc.fmt.experimental is only for the formatter to work in the editor on save, cli is fully featured!

@maanlamp
Copy link
Collaborator

Don't think the experimental stuff is going to be an issue, lot of large codebases from mayor players are switching

I'm not convinced that others taking a risk is a good reason for us to follow, there's a reason it's a draft PR 😉.

Looking at their website, the entire formatter is still in alpha. They do claim that "Oxfmt currently passes approximately 95% of Prettier's JavaScript and TypeScript test suite.", which sounds reasonably acceptable to me since this PR proves our codebase fits within that percentage. Although I'd like Jorn's input as well.

also the oxc.fmt.experimental is only for the formatter to work in the editor on save, cli is fully featured!

Hmm, I've read the docs and there is no mention of that being necessary... https://oxc.rs/docs/guide/usage/formatter/editors.html#vs-code

apparently the oxlint only works with json :D
@maanlamp
Copy link
Collaborator

maanlamp commented Jan 17, 2026

I've now played around with oxfmt and oxlint and it is quite good. Works great out-of-the-box and runs wihtout problem on this template and several of my personal projects.

The following is not pertinent to whether we want to use this in our template, but I thought it was worth a mention.

Only problem is that custom plugins do not yet support in-editor diagnostincs. Not a big deal, but it does mean that the plugin I just wrote (😉) to require readonly types will only error when checked through the CLI.

View readonly-plugin.ts
import { defineRule, type Plugin, type Visitor } from "oxlint";

const isMutableType = (node: Parameters<NonNullable<Visitor[string]>>[0]) => {
  switch (node.type) {
    case "TSTypeLiteral": {
      if (
        node.parent.type === "TSTypeReference" &&
        node.parent.typeName.type === "Identifier" &&
        node.parent.typeName.name.startsWith("Readonly")
      ) {
        return false;
      }
      return node.members.some(
        (member) => member.type === "TSPropertySignature" && member.readonly !== true,
      );
    }
    case "TSTypeOperator": {
      if (node.operator === "readonly") {
        return false;
      }
      return isMutableType(node.typeAnnotation);
    }
    case "TSArrayType": {
      return true;
    }
    case "TSUnionType":
    case "TSIntersectionType": {
      if (
        node.parent.type === "TSTypeReference" &&
        node.parent.typeName.type === "Identifier" &&
        node.parent.typeName.name.startsWith("Readonly")
      ) {
        return false;
      }
      return node.types.some(isMutableType);
    }
    default: {
      return false;
    }
  }
};

const rule = defineRule({
  createOnce(context) {
    return {
      TSTypeAliasDeclaration(node) {
        const typeNode = node.typeAnnotation;

        if (isMutableType(typeNode)) {
          context.report({
            node,
            message: `Type alias "${node.id.name}" must be readonly`,
          });
        }
      },
    };
  },
});

const plugin: Plugin = {
  meta: {
    name: "readonly-plugin",
  },
  rules: {
    readonly: rule,
  },
};

export default plugin;

This one will do the following:

// BAD!
type ButtonProps = {
  type: Whatever;
};
// GOOD!
type ButtonProps = {
  readonly type: Whatever;
};
// GOOD!
type ButtonProps = Readonly<{
  type: Whatever;
}>;

// BAD!
type CoolArray = number[];
// GOOD!
type ButtonProps = readonly number[];
// GOOD!
type ButtonProps = ReadonlyArray<number>;

While absolutely not an issue for applying this in our workflow, I'd like to propose we do run this rule to prevent mutating code at the typescript level.

@Atchferox
Copy link
Contributor Author

Atchferox commented Jan 19, 2026

I've now played around with oxfmt and oxlint and it is quite good. Works great out-of-the-box and runs wihtout problem on this template and several of my personal projects.

The following is not pertinent to whether we want to use this in our template, but I thought it was worth a mention.

Only problem is that custom plugins do not yet support in-editor diagnostincs. Not a big deal, but it does mean that the plugin I just wrote (😉) to require readonly types will only error when checked through the CLI.

View readonly-plugin.ts

import { defineRule, type Plugin, type Visitor } from "oxlint";

const isMutableType = (node: Parameters<NonNullable<Visitor[string]>>[0]) => {
  switch (node.type) {
    case "TSTypeLiteral": {
      if (
        node.parent.type === "TSTypeReference" &&
        node.parent.typeName.type === "Identifier" &&
        node.parent.typeName.name.startsWith("Readonly")
      ) {
        return false;
      }
      return node.members.some(
        (member) => member.type === "TSPropertySignature" && member.readonly !== true,
      );
    }
    case "TSTypeOperator": {
      if (node.operator === "readonly") {
        return false;
      }
      return isMutableType(node.typeAnnotation);
    }
    case "TSArrayType": {
      return true;
    }
    case "TSUnionType":
    case "TSIntersectionType": {
      if (
        node.parent.type === "TSTypeReference" &&
        node.parent.typeName.type === "Identifier" &&
        node.parent.typeName.name.startsWith("Readonly")
      ) {
        return false;
      }
      return node.types.some(isMutableType);
    }
    default: {
      return false;
    }
  }
};

const rule = defineRule({
  createOnce(context) {
    return {
      TSTypeAliasDeclaration(node) {
        const typeNode = node.typeAnnotation;

        if (isMutableType(typeNode)) {
          context.report({
            node,
            message: `Type alias "${node.id.name}" must be readonly`,
          });
        }
      },
    };
  },
});

const plugin: Plugin = {
  meta: {
    name: "readonly-plugin",
  },
  rules: {
    readonly: rule,
  },
};

export default plugin;

This one will do the following:

// BAD!
type ButtonProps = {
  type: Whatever;
};
// GOOD!
type ButtonProps = {
  readonly type: Whatever;
};
// GOOD!
type ButtonProps = Readonly<{
  type: Whatever;
}>;

// BAD!
type CoolArray = number[];
// GOOD!
type ButtonProps = readonly number[];
// GOOD!
type ButtonProps = ReadonlyArray<number>;

While absolutely not an issue for applying this in our workflow, I'd like to propose we do run this rule to prevent mutating code at the typescript level.

This will be supported in the tsgolint type-aware linting. tsgolint its one of the last 14 rules that are to be implemented! That is if this rule complies with your idea of the readonly params. This is the rule typescript-eslint

edit:
Never mind, your plugin and that rule don't do the same :D

@maanlamp
Copy link
Collaborator

This will be supported in the tsgolint type-aware linting. tsgolint its one of the last 14 rules that are to be implemented! That is if this rule complies with your idea of the readonly params. This is the rule typescript-eslint

Hmm, not exactly... but it seems like that would solve the same issues, just in a different way.

I'd like to propose we use that plugin for our projects (when it's implemented), but let's see what everyone thinks.

@Atchferox
Copy link
Contributor Author

I'd like to propose we use that plugin for our projects (when it's implemented), but let's see what everyone thinks.

The lsp support for custom plugins is coming in Q1 oxc-project/oxc#14826, there is currently a draft pr open to implement the LSP

@publicJorn
Copy link
Member

publicJorn commented Jan 19, 2026

Don't have time to really dive into this now. I like the idea of unifying the linting/formatting system. But updating just because its hip and new I don't find necessary.
If I see "alpha" and "0.x" in most places I usually want to investigate first. Especially if we didn't really have a problem in the first place.

Most important points:

  • It should run in workspace (no global installs required), so different projects won't have version issues
  • It should be easy to update existing projects with a --fix flag or something alike (eg. compatible with our earlier rules)
  • It has reasonable maturity

If those 3 boxes tick, and you guys are enthousiastic: go ahead.

@maanlamp
Copy link
Collaborator

maanlamp commented Jan 20, 2026

  • It should run in workspace (no global installs required), so different projects won't have version issues

oxfmt/oxlint seem to install a local binary for every install. While maybe a bit wasteful, that prevents the issues that we've been having with biome. Just ensure we install them as dev dependencies and pin their versions for every project.

  • It should be easy to update existing projects with a --fix flag or something alike (eg. compatible with our earlier rules)

Cli for both tools will fix matched files by default (they can be changed to only check with the --check flag). They provide migrating instructions from prettier and eslint so projects using any of those tools will work without too much trouble. I'm not sure about initialising from a biome config; there seems to be no mention of a migration path from biome in any of the tools.

  • It has reasonable maturity

Not sure what is reasonable maturity in your opinion. Oxlint is past v1.0.0 and is used by some big parties, such as shopify, and funnily enough also hey-api.

oxfmt is in alpha. In my opinion not yet ready for prod use. Missing some major features like in-editor diagnostics for JavaScript-plugins, but most common functionality is already built-in. Looks like they're speeding along, so we might see a v1 release sometime soon. When that happens I'd like to revisit.

tsgolint is used for providing type-aware linting, and is also still in alpha, just like the underlying tsgo. Same story: not yet v1 but actively developed and looks like it might be ready for prod use in the near future.

@publicJorn
Copy link
Member

I re-read all comments. In my opinion we should freeze this PR until the oxfmt is on version 1.

Again: we don't really have a problem with our current biome setup afaik. The combo with prettier is fast enough and it works great in the editor.
If any project is still using biome v1.x instead of v2.x then that project should be updated, which is a pretty straight forward process (I updated 3 already).

That said, if oxfmt is released I do see benefit in unifying the linting stack.

@publicJorn publicJorn closed this Feb 4, 2026
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.

3 participants