Skip to content

Conversation

@erunion
Copy link
Member

@erunion erunion commented Jul 2, 2025

🧰 Changes

I've been staring down the barrel of upgrading our shared ESLint config to support ESLint 9 over in readmeio/standards#993 and... I honestly would rather live my life instead. I've been really interested in https://biomejs.dev/ as of late and after playing around with it for not even 10 minutes I am sold.

  • It's almost a fully drop-in replacement for ESLint
  • I was able to configure and retain our opinionated import grouping ruleset1 without any trouble.
  • It found and fixed issues that ESLint didn't, and can't, detect!
  • It linted our codebase in half a second vs ESLint taking 4.

They're still working on supporting Markdown and YAML so we can't fully ditch Prettier too but as it is Biome has got the juice.

Footnotes

  1. https://github.com/readmeio/standards/blob/bc29338858c55da37631addb36582ec5feac11b0/packages/eslint-config/index.js#L25-L36

@erunion erunion requested a review from kanadgupta as a code owner July 2, 2025 00:53
import { exec } from 'node:child_process';

import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import sorting like this is an out of the box thing.

describe('existing changelogs', () => {
let simpleDoc;
let anotherDoc;
let simpleDoc: PageObject;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It flagged that these didn't have an explicit type. I don't know where to put page.types.js but I didn't want to copy-paste this PageObject interface in a handful of files.


declare module 'vitest' {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: This is the type for a custom matcher.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love that it requires you give a reason why you're ignoring a rule.

expect(yamlOutput).toMatchSnapshot();
expect(fs.writeFileSync).toHaveBeenCalledWith(getGHAFileName(fileName), expect.any(String));
expect(console.info).toHaveBeenCalledTimes(1);
expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
Copy link
Member Author

@erunion erunion Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this over to the spy because it was already available and used in other tests, and also Biome was complaining about console.info being used and we had suppressed that at the top of file with ESLint.

biome.jsonc Outdated
Comment on lines 14 to 19
// test result artifacts
"!coverage/**",

// release build artifacts
"!dist/**",
"!dist-gha/**",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought with vcs.useIgnoreFile: true we could get rid of these and have it automatically exclude files in our .gitignore file but it didn't work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels like a pretty rough oversight and doesn't inspire confidence tbh 😔 but glad the workaround is easy enough at least. mind adding a comment explaining this?


validationHookResults.successes.forEach(success => {
if (success.result && success.result.pages.length) {
if (success.result?.pages.length) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It recommended, but thankfully didn't autofix, that we move this to optional chaining.

Comment on lines +245 to 246
// biome-ignore lint/performance/noAccumulatingSpread: @todo can this be improved?
.reduce((prev, next) => Object.assign(prev, next));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to work around this rule but it's probably fine?

Comment on lines -134 to -135
.map((p: string) => Object.keys(parsedPreparedSpec.paths?.[p] || {}))
.flat()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blown away that it detected and autofixed this to a .flatMap().

function replaceRef(schemaName: string, propertyName: string, refSchemaName: string) {
const schema = schemas[schemaName];
if ('properties' in schema) {
schema.properties![propertyName] = { $ref: `#/components/schemas/${refSchemaName}` };
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrote this off of the non-null assertion because that was being picked up by their default ruleset.

Screenshot 2025-07-01 at 6 04 00 PM

@@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome"]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far this extension seems to work as well as the ESLint one.

@@ -1,8 +1,8 @@
import type { Mappings } from './readmeAPIFetch.js';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always bugged me that our ESLint import grouping config never grouped type imports like non-type imports. Really neat that Biome just does it all.

if (options.Command?.flags?.key) {
this.debug('current command has --key flag');
if (isTest()) {
// eslint-disable-next-line no-param-reassign
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not having a no-param-reassign rule might be an issue but with TS we've grown to be pretty good at not writing code that gets flagged with it.


return { pathInput: newWorkingDir, zipped: true as const, unzippedDir: newWorkingDir };
} catch (e) {
} catch (_e) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Biome flags when we don't use variables but since we can't do } catch () { it says to prefix an unused variable with an underscore.

import type { PageRequestSchema, PageResponseSchema } from './lib/types/index.js';

export type {
APIKeyRepresentation,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm full :sickos: mode over here at these imports being autofixed to be properly alphabetized now.

@erunion erunion requested a review from emilyskuo July 2, 2025 01:11
@erunion erunion changed the title refactor: moving from eslint to biome ci: moving from eslint to biome Jul 8, 2025
@erunion erunion added the enhancement New feature or request label Jul 8, 2025
prefix: chore(deps)
prefix-development: chore(deps-dev)
ignore:
# Blocked on this until we can bump our shared ESLint config to support ESLint 9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥹

// set the correct targets for GitHub Actions
current.oclif.commands.target = newTarget;
Object.values(current.oclif.hooks).forEach(hook => {
// eslint-disable-next-line no-param-reassign
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like this rule exists and is set to a warning by default: https://biomejs.dev/linter/rules/no-parameter-assign/

any idea why it's not being fired?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's not a part of the recommended ruleset.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's part of the project domain but enabling linter.domains.project: all doesn't seem to enable it. Not sure why but I've manually turned it on and addressed a couple issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very glad that this file that we've spent so long in over the last few months has like zero issues lol 😮‍💨

@erunion erunion requested a review from kanadgupta July 9, 2025 00:19
Copy link
Contributor

@kanadgupta kanadgupta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm slightly bummed about some of the rules we're losing (the eslint ecosystem is very difficult to top in this regard) but i think the tradeoffs seem worth it — eslint has been giving us grief for ages and biome's speed + out-of-the-box capabilities (the fact that it plays much better with TS 🫰🏽) + simpler configuration are all very promising.

this is a very big workflow change and i think we should make sure the team is fully on board before we move forward with other repos (and let's also make sure the custom rules support is adequate for our needs) but i'm in favor of this! let's also think about a shared config file that we publish from our standards repo at some point soon.

thanks for kicking off this effort ❤️‍🔥


import { getNodeVersion, getMajorPkgVersion } from '../dist/lib/getPkg.js';
// biome-ignore lint/correctness/useImportExtensions: This file exists but Biome wants to use the `.d.ts` file instead.
import { getMajorPkgVersion, getNodeVersion } from '../dist/lib/getPkg.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird — you'd think biome would be smart enough to know this is a .js file 🧐

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea it might be that it prefers TS over JS and sees d.ts there. will see if i can open up an issue with them

"enabled": true,
"domains": {
"project": "all",
"test": "all",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this? it looks like they auto apply it if it detects vitest in our deps: https://biomejs.dev/linter/domains/#test-dependencies

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh nvm i just played around locally and looks like we need this 😔

// biome-ignore lint/style/noParameterAssign: This is safe to rewrite our incoming questions for `prompts`.
questions = questions.map(question => ({ onRender, ...question }));
} else {
// eslint-disable-next-line no-param-reassign
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can't tell if this is biome being incomplete in its analysis or if it's actually a galaxy brain approach by not flagging this lol

*/
function sanitizeHeaders(headers: Headers) {
const raw = Array.from(headers.entries()).reduce<Record<string, string>>((prev, current) => {
// eslint-disable-next-line no-param-reassign
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't understand when it does and doesn't flag this rule 😮‍💨

},
"editor.defaultFormatter": "esbenp.prettier-vscode",
// explicitly disable ESLint to avoid conflicts with Biome
"eslint.enable": false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i added the changes in this file b/c i imagine there might be weird side effects when vscode is running eslint + biome simultaneously and i figured it's safer to be explicit here. we should probably do the same with prettier (i.e., explicitly disable the prettier extension and set the default formatter to biome) if/when we migrate off of that too.

biome.jsonc Outdated
Comment on lines 14 to 19
// test result artifacts
"!coverage/**",

// release build artifacts
"!dist/**",
"!dist-gha/**",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels like a pretty rough oversight and doesn't inspire confidence tbh 😔 but glad the workaround is easy enough at least. mind adding a comment explaining this?

@erunion erunion merged commit 96c907c into next Jul 11, 2025
9 checks passed
@erunion erunion deleted the feat/biomejs branch July 11, 2025 20:12
@kanadgupta
Copy link
Contributor

🎉 This PR is included in version 10.5.0-next.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@kanadgupta kanadgupta added released on @next needs-backport-to-v9 PRs that need to backported to the 9.x channel labels Jul 16, 2025
@kanadgupta
Copy link
Contributor

🎉 This PR is included in version 10.5.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request needs-backport-to-v9 PRs that need to backported to the 9.x channel released on @next released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants