Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

📎 Tailwind class sorting lint rule #1274

Open
15 of 47 tasks
Tracked by #718
DaniGuardiola opened this issue Dec 21, 2023 · 62 comments
Open
15 of 47 tasks
Tracked by #718

📎 Tailwind class sorting lint rule #1274

DaniGuardiola opened this issue Dec 21, 2023 · 62 comments
Assignees
Labels
L-JavaScript Language: JavaScript and super languages S-Feature Status: new feature to implement S-Funding Status: open to funding and implemented by external contributors

Comments

@DaniGuardiola
Copy link
Contributor

DaniGuardiola commented Dec 21, 2023

Update: I'm working on this! Follow the updates in this Twitter thread: https://twitter.com/daniguardio_la/status/1739715412131238122

PRs

Description

Port the TailwindCSS Prettier plugin that sorts Tailwind CSS utility classes. In Biome, it is being implemented as a lint rule that formats classes in JavaScript files though auto-fixes (ref).

Feature support / tasks checklist

Features with an asterisk are not likely to be implemented as part of this task. They are listed for exhaustiveness, and considered in architectural decisions so that future support is possible, but they are not high priority.

Features with a question mark are ideas that I'm not sure about, or I don't know enough about to determine if they make sense or how they could be implemented.

  • Visitor:
    • Custom functions (clsx, etc).
    • Custom functions: tagged template support
    • JSX attributes (class and className)
    • Custom JSX attributes
    • Object properties (e.g. in objects passed to clsx)
  • Sorting:
    • Utility classes
    • Utility classes: arbitrary values
    • Static variants
    • Dynamic variants
    • Arbitrary variants
    • Arbitrary CSS
    • Parasite utilities
    • Screens e.g. md:, max-md: (simple config, px-only)
    • Screens (advanced config)
    • Negative values
    • With prefix
    • With important prefix (!class)
    • With custom separator
  • Preset/config generation:
    • Tailwind CSS v3 (WIP)
    • Tailwind CSS v4
    • UnoCSS*
  • Presets:
    • Tailwind CSS preset
    • UnoCSS presets?*
  • Config:
    • Custom functions
    • Custom JSX attributes
    • Class order
    • Exclude classes?*
    • Utilities/layers
    • Variants
    • Screens
    • Preset (Tailwind CSS or no preset)
    • Extend utilities
    • Extend variants
    • Extend screens? (replace might be the only sane option)
    • Prefix
    • Separator
  • Other features:
    • Automatic migration/sync tailwind.config.js -> biome.json
    • Resiliency to UnoCSS variant groups (naive sorting could mangle these) docs?*
    • UnoCSS variant groups sorting?*
    • UnoCSS attributify mode sorting?*
  • Other tasks
    • Lint rule documentation
    • Config documentation
    • Migration documentation
  • Polish:
    • PartialOrd / improved check perf
    • Single sort pass / lexicographical sort as last compare sort?
    • Better name for the rule (useSortedUtilityClasses?)
    • Avoid string allocations whenever possible.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar
@ematipico
Copy link
Member

Hey @DaniGuardiola , thank you for opening this.

If you have the knowledge, do you know how this sorting thing works? Does Biome need to read the tailwind configuration file?

@DaniGuardiola
Copy link
Contributor Author

@ematipico yeah I'm like 99% sure the config file is read. JavaScript is involved. Not sure how it fits in the biome way of doing things.

Is running JavaScript a possibility, at least to load the config file and transfer it to the rust program (or however that'd work)? or is it considered too slow?

@ematipico
Copy link
Member

What I am interested in is the business logic of the plugin and how the configuration is involved in the sorting to the CSS classes.

I don't know Tailwind that well (as well as other people), so if someone with more knowledge can shed some light, it would help us to implement the feature in a faster way.

@DaniGuardiola
Copy link
Contributor Author

@ematipico I will do a bit of research and get back to you.

@DaniGuardiola
Copy link
Contributor Author

DaniGuardiola commented Dec 21, 2023

Leaving these as notes for the future.

This is where it all ultimately happens: https://github.com/tailwindlabs/tailwindcss/blob/master/src/lib/setupContextUtils.js#L930-L969

This function defers to this other function: https://github.com/tailwindlabs/tailwindcss/blob/master/src/lib/generateRules.js#L885-L930

The way sorting works is by leveraging the exact same mechanism that tailwind uses to sort the outputs. For example, p-4 pt-6 and pt-6 p-4 result in the same css, something like this:

.p-4 {
  padding: 1rem;
}

.pt-6 {
  padding-top: 1.5rem;
}

So the class order after sorting will match this: p-4 pt-6.

This is explained here: https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted

Regarding configuration. First let me disambiguate to avoid confusion. There are two separate concepts here:

  • The Tailwind sorting options. This are strictly related to the sorting plugin. For example, one can indicate a list of functions whose string arguments (in function calls) will be interpreted as class names and sorted (e.g. clsx, cva, etc).
  • The Tailwind CSS config. These are the options for Tailwind itself, like the theme, the files to process, the plugins, prefix, etc.

Sorting options are trivial, and just act as input for the formatting tool. What I'm talking about here is the latter, the involvement of the tailwind config in this process.

First, about this config: it is a JavaScript file that needs to run and be "resolved" (e.g. plugins are functions that need to call stuff like addUtility and such to register stuff for Tailwind to use. So, the final config is created by "resolving", a.k.a. executing a bunch of stuff like that.

Its role in sorting spans multiple things:

  • General config like prefix (e.g. if the configured prefix is tw, classes will look like tw-m-4), the ! important strategy, etc. These affect how classes are interpreted.
  • The available utilities and variants, and their order (I'm guessing any of these coming from custom plugins are ordered just as the plugins are ordered in the config file).

The prettier plugin seems to "hook" into all of the existing logic, essentially reusing the exact same mechanism that is used to order the output. This makes sense, because why wouldn't they?

But I would argue that such complexity is not necessary, and biome could go with a way simpler approach based on simple heuristics and basic parsing/regexp.

I've done custom "reimplementations" of tailwind-like parsing for two projects now:

So I would propose an approach like this + extensive testing. Of course, only using actual tailwind can guarantee that there won't be any false positives, but I think it is a very good compromise.

Regarding tailwind config, plugins and such, I would suggest that those options are moved to the formatter options. I think this biome feature should be generalize as a "utility class sorting" thing, with a tailwind preset.

So, in line with that, it should be simple enough to just add the utilities and variants that a plugin might add through config.

Sorry if this got a bit long / chaotic, I'm just researching and brainstorming to get things rolling. I'd be happy to start playing with this in a PR.

@DaniGuardiola
Copy link
Contributor Author

To expand on the pros/cons of my proposed approach:

Pros

  • Simpler. Like tens of thousands of LOC simpler.
  • Covers 99.8% of cases. Yes, I made this number up, but I'm strongly convinced of it. I will explain that missing 0.2% and why it largely doesn't matter.
  • It is generalizable to utility classes in general, which should cover other projects like nativewind, UnoCSS, etc.

Cons

  • Can't use tailwindcss config directly. The user would need to re-specify these settings for the biome feature. Honestly, I think options like prefix are largely unused so I don't think it's a big deal. Regarding plugin-provided utilities and variants, the simplest solution is to provide a way to define these in the config as well. All in all, not the worst thing imo.
  • False positives. Tailwind can tell the difference between something like hover:px-4 and hover:px-8268. The first one is valid, and the second is not. This is because 4 is in the theme scale, and 8268 is not. The proposed approach would count both as valid, and sort them both. This is the 0.2% I was talking about. Anyone using hover:px-8268 as a manually defined class should consider changing careers anyway. It's a non-issue.

@estubmo
Copy link

estubmo commented Dec 22, 2023

Yet to begin, because of events in my personal life.

@DaniGuardiola
Copy link
Contributor Author

Thanks for the update. I'm gonna give it a go then :)

@Reed-Anderson
Copy link

Hey @DaniGuardiola , thanks so much for taking this on, it will be a huge upgrade for me. I hope this is an appropriate place to ask, is there any interest in supporting this popular tailwind/prettier request that was never implemented?

tailwindlabs/tailwindcss#7763

@itsezc
Copy link

itsezc commented Dec 23, 2023

Cons

  • Can't use tailwindcss config directly. The user would need to re-specify these settings for the biome feature. Honestly, I think options like prefix are largely unused so I don't think it's a big deal. Regarding plugin-provided utilities and variants, the simplest solution is to provide a way to define these in the config as well. All in all, not the worst thing imo.
  • False positives. Tailwind can tell the difference between something like hover:px-4 and hover:px-8268. The first one is valid, and the second is not. This is because 4 is in the theme scale, and 8268 is not. The proposed approach would count both as valid, and sort them both. This is the 0.2% I was talking about. Anyone using hover:px-8268 as a manually defined class should consider changing careers anyway. It's a non-issue.

First issue, can cause some friction for DX, and repeating configs is not ideal especially when there may be several devs in a team / project with an evolving tailwind config, but you're right its not the worst thing either.

Second, is a non issue as you've said. But, many people use arbitrary values i.e. hover:px-[8rem] or hover:px-extra if they're using a custom setup theme within their tailwind config. As long as these scenarios are handled to how the existing prettier plugin does, I don't think that'll be a huge issue.

@DaniGuardiola
Copy link
Contributor Author

@Reed-Anderson thanks for bringing this up. While I share your pain, and think some tooling could definitely help, I think that's out of scope for this task.

I did see some approaches I liked in that thread, like one that turned multi-line strings into a normal, single line string on build (iirc). I don't think this task is the place to try something like that though.

@DaniGuardiola
Copy link
Contributor Author

@itsezc good call-out. While I'm not going for perfection, I do wanna consider all of these cases, and I have some ideas to solve this that I'll share soon here for public feedback.

I'm a heavy tailwind user myself so I definitely want something that works across all reasonable use-cases.

@DaniGuardiola
Copy link
Contributor Author

Here's my current thinking. Read my earlier messages on this thread for context.

Tailwind CSS class sorting -> class sorting

While the main goal of this task is to provide a good replacement for the Prettier Tailwind CSS plugin, generalizing just makes sense to me, because other tools use the concept of utility classes as well. It also just seems like something that could be generally helpful, the ability to sort classes according to a configuration.

Approach

The main challenge to solve is how the order of the classes is decided. Tailwind relies on its internal logic that also decides how the output is ordered, but we can't do that here, so we need a different approach. My thinking so far has been along the lines of having a config like this:

// simplified example, obviously
{
  "class-patterns": [
      // some kind of pseudo-regex / templating way of specifying these
     "p{y|x}-${scale}", // "special" value types like scale, numeric, etc
     "p-${scale}",
     "m{y|x}-${scale}",
     "m-${scale}",
     "grow$", // match "grow" without a suffix first ("exact")
     "grow-${numeric}" // then match other with a suffix
  ]
}

"Special" value types could be extended/customized:

{
  "types": {
    // add values
    "scale": { "extend": ["extra"] },
    // replace values
    "screens": { "values": ["md", "lg"] }
  }
}

The sorting algorithm would use this config to decide the order. Some other configs can be specified as well:

{
  "prefix": "tw-",
  // etc...
}

Variants

Variants (hover:active:focus:<utility class>) also need to be taken into consideration, and they are ordered as well iirc. The order could be specified similarly. The separator (colon by default) could also be configurable.

Presets

Of course, having and maintaining all of this in a config JSON file is annoying, so Biome would ship with presets for Tailwind and any other libraries that are popular enough.

{
  "preset": "tailwind-css",
}

Manual configuration

As I described in a previous comment, a drawback of this approach is that there's a bunch of manual configuration to maintain, which has to be kept in sync with the Tailwind config. To alleviate this, it'd be doable to create a package that automatically reads the Tailwind config and syncs the Biome config to it. Doesn't necessarily need to be part of Biome itself, but it can be a simple npm package. E.g.

$ npm i -D sync-tailwind-biome
$ sync-tailwind-biome

I think this should make that pain much more manageable.


These are my thoughts so far. On the development side of things, my focus is going to be creating a minimal POC that works for a default tailwind config. This is the first time using Rust so it's enough to keep me busy for a while :P

Feedback is super welcome though, I'd like to polish these ideas so that I can be confident when it's time to implement the advanced features.

@ematipico
Copy link
Member

ematipico commented Dec 28, 2023

Tailwind CSS class sorting -> class sorting

Unfortunately I can't venture an opinion because I don't know much of the logic we are about to implement. Surely, I am a big fan of consistency, so it's definitely a good start.

Approach

What are the JSON snippets that you shared? I miss some context even though I read the previous messages.

Variants

I don't understand the context of this, can you expand a bit please?

Manual configuration

I would suggest a different approach. I like having a separated package that does the work, and I think we can also have it under our organization, maintained by some volunteers.

The package should return a JSON file via stdout, and then we can use this package in our command biome migrate The command can read the result of this package via stdout, and use it to update the configuration file. Here's why:

  • Biome has already a parser and deserialiser for biome.json, let's use it.
  • By using our internal deserializer, we can catch errors from the package very early.
  • The package stays slim, and doesn't require any parser/regex to update the biome.json.
  • Other tools shouldn't mess with biome.json. Using Biome itself is more trustworthy.

@tcoopman
Copy link

tcoopman commented Jan 4, 2024

  • Honestly, I think options like prefix are largely unused so I don't think it's a big deal.

This comment is not completely clear to me. Does this mean that you won't provide support for prefixes or that they'll have to be specified in the biome config?

@DaniGuardiola
Copy link
Contributor Author

@tcoopman the latter. In any case, the plan is to provide an automated way to update the config, and I'd expect that to be the most popular way to use this, so it's even less of a big deal.

@XiNiHa
Copy link
Contributor

XiNiHa commented Jan 12, 2024

I wonder if it'd be possible to support UnoCSS transformer features like variant groups...

@johnpyp
Copy link

johnpyp commented Jan 12, 2024

It might also be useful to look at https://github.com/avencera/rustywind, as this is a pure rust implementation of tailwind sorting which also doesn't look at the tailwind.config.js file afaict

an interesting approach it takes is optionally configuring the sort order based on the output css file of tailwind, not sure if that would really be applicable here though

@DaniGuardiola
Copy link
Contributor Author

@XiNiHa it's possible, but it's a bit out of scope for now. Maybe in a future iteration.

@johnpyp thanks for the suggestion. I'm already very familiar with how Tailwind CSS sorting works, and an initial version is about to merge with only a couple of details missing, so there's no value in researching other projects anymore at this stage. Fwiw, I did take a look around that project but ended up using Tailwind CSS itself as the source of truth, which I believe is the best way to ensure correctness.

@ematipico ematipico pinned this issue Feb 5, 2024
@ematipico ematipico added L-JavaScript Language: JavaScript and super languages S-Feature Status: new feature to implement labels Feb 5, 2024
@polar-sh polar-sh bot added the S-Funding Status: open to funding and implemented by external contributors label Feb 5, 2024
@GustavoBonfimS
Copy link

Could this rule be a safe action? Today we can only organize classes using --apply-unsafe. If it were a safe rule it could be automatically organized with the VS Code extension when the file is saved.

#1714

@afonsojramos
Copy link
Contributor

Would be probably good to also add to biome all of the rules that https://github.com/francoismassart/eslint-plugin-tailwindcss supports, such as:
enforces-negative-arbitrary-values: make sure to use negative arbitrary values classname without the negative classname e.g. -top-[5px] should become top-[-5px]
enforces-shorthand: merge multiple classnames into shorthand if possible e.g. mx-5 my-5 should become m-5
no-arbitrary-value: forbid using arbitrary values in classnames (turned off by default)
no-custom-classname: only allow classnames from Tailwind CSS and the values from the whitelist option
no-contradicting-classname: e.g. avoid p-2 p-3, different Tailwind CSS classnames (pt-2 & pt-3) but targeting the same property several times for the same variant.
no-unnecessary-arbitrary-value: e.g. replacing m-[1.25rem] by its configuration based classname m-5

Which I would say are rather important!

@MrOxMasTer
Copy link

I am the only one with such a problem that he complains about the wrong order of classes (although in my opinion they were sorted by eslint before) and at the same time he does not format not through vs code, not through the cli

  ! These CSS classes should be sorted.

  > 21 │             className="w-full mt-6"
       │                       ^^^^^^^^^^^^^
    23 │           >

  i Unsafe fix: Sort the classes.

    21    │ - ············className="w-full·mt-6"
       21 │ + ············className='mt-6·w-full'
    23 23 │             >


Checked 1 file in 4ms. No fixes applied.
Found 1 warning.

image

pnpm biome format src\...\component.tsx
Checked 1 file in 2ms. No fixes applied.

@Gomah
Copy link

Gomah commented Jul 2, 2024

pnpm biome format src\...\component.tsx
Checked 1 file in 2ms. No fixes applied.

@MrOxMasTer You're running the formatter, not the linter, here's what you're looking for:

biome lint . --write --unsafe

Or if you want to run the formatter, linter and import sorting:

biome check . --write --unsafe

@DaniGuardiola
Copy link
Contributor Author

Update: variant sorting (with some caveats) will be supported in the next version of Biome, thanks to the excellent work by @lutaok :)

@MrOxMasTer
Copy link

biome check . --write --unsafe

unsafe really worked. How can I set unsafe in the extension itself for formatting?
image

@DaniGuardiola
Copy link
Contributor Author

@MrOxMasTer it's not possible. Related: #1274 (comment)

Please keep the discussion here on topic, for bug reports and specific discussion about the feature. Feel free to use Discord or GitHub Discussions for questions and discussion of more general topics.

@ematipico
Copy link
Member

ematipico commented Jul 2, 2024

biome check . --write --unsafe

unsafe really worked. How can I set unsafe in the extension itself for formatting?
image

@MrOxMasTer

Please, search the website! We have everything in there: https://biomejs.dev/linter/#configure-the-rule-fix

@DaniGuardiola this is now possible since v1.8.0

@hangaoke1

This comment has been minimized.

@MrOxMasTer
Copy link

Please, search the website! We have everything in there: https://biomejs.dev/linter/#configure-the-rule-fix

If you are talking about the "fix" setting, which takes the values "safe" or "unsafe" for each rule, then I can say that I used it and it did not work for me

@ematipico
Copy link
Member

@MrOxMasTer I advise you to open a new issue with a reproduction, this issue is for developments only and we don't want to ping subscribers with messages that are unrelated

@damianobarbati
Copy link

damianobarbati commented Jul 26, 2024

@DaniGuardiola other few things to consider on why the tailwind config is needed and possible problems:

  1. A feature needed to move away from eslint + tailwind plugin is the https://github.com/francoismassart/eslint-plugin-tailwindcss/blob/master/docs/rules/no-custom-classname.md rule which gives you error if the class you specified does not exist.
  2. Some classes are automatically generated depending on the tailwind config file: for instance, if you declare a color {"red":{"blood":"#F00"}}, you can have use the class fill-red-blood.
  3. Some classes are inside your .css files, and those are valid.
  4. In a monorepo, you can have the tailwind config inside the single package folder and not in the root monorepo folder.

Hope this helps.

@Goldziher

This comment was marked as off-topic.

@DaniGuardiola

This comment was marked as off-topic.

@BorisZubchenko

This comment was marked as off-topic.

@ematipico

This comment was marked as off-topic.

@vgseven
Copy link

vgseven commented Aug 23, 2024

add support for rules for un-supported or invalid class of tailwind, inspired by eslint-plugin-tailwind

@warrenbhw

This comment has been minimized.

@ematipico

This comment has been minimized.

@lobosan
Copy link

lobosan commented Sep 4, 2024

Hi, I'm migrating my Astro project to Biome 1.8.3, and I love the simple setup, the only thing remaining for me is Tailwind class sorting.

This is my biome.json

{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "css": {
    "formatter": {
      "enabled": true
    },
    "linter": {
      "enabled": true
    },
    "parser": {
      "cssModules": true
    }
  },
  "files": {
    "ignore": [".astro", "dist", "node_modules"]
  },
  "formatter": {
    "formatWithErrors": true,
    "indentStyle": "space",
    "lineWidth": 100
  },
  "javascript": {
    "formatter": {
      "arrowParentheses": "asNeeded"
    }
  },
  "linter": {
    "rules": {
      "nursery": {
        "useSortedClasses": "error"
      }
    }
  }
}

This is my settings.json for VSCode

{
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true
}

However, when I run
npx @biomejs/biome check --unsafe

I don't see any error in my .astro file that has the invalid example of the documentation
<div class="px-2 foo p-4 bar" />

Can someone please help me to fix the issue? Also, ideally I would love to sort the classes when saving.

@Trombach
Copy link

Trombach commented Sep 4, 2024

Hi, I'm migrating my Astro project to Biome 1.8.3, and I love the simple setup, the only thing remaining for me is Tailwind class sorting.

This is my biome.json

{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "css": {
    "formatter": {
      "enabled": true
    },
    "linter": {
      "enabled": true
    },
    "parser": {
      "cssModules": true
    }
  },
  "files": {
    "ignore": [".astro", "dist", "node_modules"]
  },
  "formatter": {
    "formatWithErrors": true,
    "indentStyle": "space",
    "lineWidth": 100
  },
  "javascript": {
    "formatter": {
      "arrowParentheses": "asNeeded"
    }
  },
  "linter": {
    "rules": {
      "nursery": {
        "useSortedClasses": "error"
      }
    }
  }
}

This is my settings.json for VSCode

{
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true
}

However, when I run npx @biomejs/biome check --unsafe

I don't see any error in my .astro file that has the invalid example of the documentation <div class="px-2 foo p-4 bar" />

Can someone please help me to fix the issue? Also, ideally I would love to sort the classes when saving.

Biome can't yet parse the html template section of Astro files, so nothing gets formatted/linted in there, including class names. THis will only work once biome has full support for Astro files

@MarkLyck
Copy link

After upgrading to biome 1.9.0 the Quick Fix for sorting the classes stopped working for me.

Downgrading back to 1.8.2 worked, and I can quick fix my classes sorting again.

@ematipico
Copy link
Member

@MarkLyck update to 1.9.1. That issue was reported already and fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
L-JavaScript Language: JavaScript and super languages S-Feature Status: new feature to implement S-Funding Status: open to funding and implemented by external contributors
Projects
None yet
Development

No branches or pull requests