Skip to content

Conversation

hyoban
Copy link
Owner

@hyoban hyoban commented May 5, 2025

Try today?

ni -D @hyoban/eslint-plugin-tailwindcss@alpha

You can also use an alias to keep the dependency name. If we can merge the changes upstream, then you can remove the alias.

{
  "eslint-plugin-tailwindcss": "npm:@hyoban/[email protected]",
}

Get the latest alpha version on npm.

For v4, you need to set the config path manually.

import tailwind from '@hyoban/eslint-plugin-tailwindcss'

export default [
  ...tailwind.configs['flat/recommended'],
  {
    settings: {
      tailwindcss: {
        config: 'full/path/to/entry.css',
      },
    },
  },
]

@hyoban hyoban force-pushed the 05-05-move-to-tailwind-api-utils branch from 9d9dd89 to f7ac36f Compare May 6, 2025 01:13
@LZL0
Copy link

LZL0 commented May 6, 2025

My boy takes initiative! 💖

@RaenonX
Copy link

RaenonX commented May 6, 2025

Just came here to say thank you <3

@rogepi
Copy link

rogepi commented May 6, 2025

亮神伟大,无须多言 ☝️

@lucasdu4rte
Copy link

🫶

@TheHanna
Copy link

TheHanna commented May 7, 2025

Where do you need to set the config path exactly? Trying to use this and not sure where to do that

@kddige
Copy link

kddige commented May 7, 2025

I think i found a way to resolve the tailwind config file, automatically. (if you want to build atop of it) -, the function does a "best effort" on finding a .css file with @import "tailwindcss" and returns the full path to that file.

Im currently using this in a monorepo, and it works 👌

/**
 * Recursively walks `dir`, looking for the first .css file
 * that has a line starting with @import "tailwindcss
 * @param {string} dir  absolute path to start searching from
 * @returns {string|null}  absolute path to matching CSS, or null if none found
 *
 * @example
 * const twCssPath = findTailwindImportCss(process.cwd())
 */
function findTailwindImportCss(dir) {
  const entries = fs.readdirSync(dir, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);

    if (entry.isDirectory()) {
      const found = findTailwindImportCss(fullPath);
      if (found) return found;
    } else if (entry.isFile() && entry.name.endsWith(".css")) {
      // read & scan lines
      const lines = fs.readFileSync(fullPath, "utf8").split(/\r?\n/);
      for (let line of lines) {
        if (line.trim().startsWith('@import "tailwindcss')) {
          return fullPath;
        }
      }
    }
  }

  return null;
}


// usage:
 export default [
  ...tailwindPlugin.configs["flat/recommended"],
  {
    settings: {
      tailwindcss: {
        config: findTailwindImportCss(process.cwd()), // <- uses process.cwd() to start looking down from where eslint is run
      },
    },
  },
 ]

@rlueder
Copy link

rlueder commented May 7, 2025

Where do you need to set the config path exactly? Trying to use this and not sure where to do that

@TheHanna here are the steps I followed on a Vite app:

On package.json add "eslint-plugin-tailwindcss": "npm:@hyoban/[email protected]" to devDependencies

On eslint.config.js add the following:

import fs from "fs";
import js from "@eslint/js";
import path from "path";
import tailwind from "eslint-plugin-tailwindcss";
import tseslint from "typescript-eslint";

// @kddige 's snippet above goes here

export default tseslint.config(
   ...
  {
    extends: [
      js.configs.recommended,
      ...tseslint.configs.recommended,
      ...tailwind.configs["flat/recommended"],
    ],
    ...
    settings: {
      tailwindcss: {
        config: findTailwindImportCss(process.cwd()), // <-- call @kddige 's function
      },
    },
  },
);

@kddige that worked great! In my case setting the relative path of src/index.css didn't work, but your function does

@coderrshyam
Copy link

If all sounds good then publish a npm package named eslint-plugin-tw and gave credits to eslint-plugin-tailwindcss. that's it.

@flossels
Copy link

flossels commented May 20, 2025

In general, this fork works with Tailwind 4, but there are still some issues. For example, the new class “isolate” is not recognized (no-custom-classname). The same applies to custom classes defined in the “@layer component” when they contain a hyphen (because the hyphen is used as a separator and the class name will be split).

@1qh
Copy link

1qh commented May 28, 2025

in my setup, the classnames-order works fine, but the rule enforces-shorthand does not. Does anyone have this issue?

@NaucMeIT
Copy link

I use PNPM Workspaces + NX Monorepo and it fails to load Tailwind 4 for me.

pnpm run lint:tailwind "./**/*.{js,jsx,ts,tsx}" --max-warnings 0      

> ui@0.0.0 lint:tailwind D:\Work\new-engine\libs\ui
> eslint "**/*.{js,jsx,ts,tsx}" --config eslint.config.js "./**/*.{js,jsx,ts,tsx}" "--max-warnings" "0"


Oops! Something went wrong! :(

ESLint: 9.26.0

Error: Could not resolve tailwindcss
    at TailwindUtils.loadConfigV4 (D:\Work\new-engine\node_modules\.pnpm\tailwind-api-utils@1.0.2_tailwindcss@4.1.6\node_modules\tailwind-api-utils\dist\index.cjs:391:13)
    at TailwindUtils.loadConfig (D:\Work\new-engine\node_modules\.pnpm\tailwind-api-utils@1.0.2_tailwindcss@4.1.6\node_modules\tailwind-api-utils\dist\index.cjs:381:18)
    at D:\Work\new-engine\node_modules\.pnpm\@hyoban+eslint-plugin-tailw_35146946690b35920ea77267a21f7779\node_modules\@hyoban\eslint-plugin-tailwindcss\lib\util\getTailwindConfigWorker.js:7:25
    at D:\Work\new-engine\node_modules\.pnpm\synckit@0.11.8\node_modules\synckit\lib\index.cjs:517:20
    at MessagePort.<anonymous> (D:\Work\new-engine\node_modules\.pnpm\synckit@0.11.8\node_modules\synckit\lib\index.cjs:539:5)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:827:20)
    at MessagePort.<anonymous> (node:internal/per_context/messageport:23:28)
 ELIFECYCLE  Command failed with exit code 2.

@FadyAmir223
Copy link

in my setup, the classnames-order works fine, but the rule enforces-shorthand does not. Does anyone have this issue?

yes, it also breaks the auto close tag rule

@francoismassart
Copy link

👋 @hyoban and thank you for you work, I started working on a full rewrite of the plugin few months ago but I was stuck when trying to load the config from TW4 as it is done in async while eslint only accepts synchronous code.
Then my main job schedule did increase a lot and I left it there.

Glad to browse your PR. I'll update few on my main projects to TW4 and see how your version handles it.
If the result is satisfying, I'll merge and prepare a v4 release.

IMO, the v4 release should only support TW4, the projects using TW4 should stick with eslint-plugin-tailwindcss v3...

👏

@hyoban
Copy link
Owner Author

hyoban commented Jun 20, 2025

@francoismassart, I'm glad you found some time to look back at eslint-plugin-tailwindcss. Thank you so much for your work.

My fork has only some minor changes to make it work with the basics of Tailwind v4. I haven't tested it extensively yet, so it may still have some issues, and I'd be happy to fix them based on feedback.

I have no problem with the new version of eslint-plugin-tailwindcss only supporting tailwind v4, which can reduce the cost of maintenance.

@BleedingDev
Copy link

Speaking of newer version, how hard would it be for you to support other linters like Oxlint or Biome? 😊

@keksiqc
Copy link

keksiqc commented Jun 20, 2025

Speaking of newer version, how hard would it be for you to support other linters like Oxlint or Biome? 😊

Biome has an integrated rule for class sorting
https://biomejs.dev/linter/rules/use-sorted-classes

@BleedingDev
Copy link

That's great, I'm looking for invalid tailwind classes. We base everything on Tailwind, so I want to ban invalid classes all together. :) Right now I use TW4 version of this plugin (which works great BTW, great job!).

@francoismassart
Copy link

Sorry, I'm not familiar with Biome and unless we start using it at work, I don't think I'll work on that.

But if I was coding it, I would use the same logic as in this PR: don't reinvent the wheel and delegate the business logic (sorting/validating class as Tailwind CSS, etc.) to the existing methods provided by the tailwindcss package itself.

@francoismassart
Copy link

@hyoban
UPDATE on my current testing, I've met 2 issues for now:

  1. Some styles broken from the migration (unrelated to eslint-plugin-tailwindcss itself)
  2. We are using .eslintrc.json file and I have to provide the absolute path to the tailwind.css file to the lint plugin... That pass will differ on my colleagues computers. I should either
    • try to pass it as a relative path
    • or migrate to an .js config file in order to inject process.cwd() in the provided path...

@hyoban
Copy link
Owner Author

hyoban commented Jun 24, 2025

All versions of eslint 8 have been marked as deprecated, so I think this should be a minor issue?

@francoismassart
Copy link

@hyoban do you mean we should release Tailwind CSS v4 support with ESLint at min v9 ?
FYI my tests looks promising 👍

@francoismassart
Copy link

I noticed a first issue with the tailwindcss/no-contradicting-classname rule:

<span className={'block flex'}>✅ working fine, detects contradicting classname</span>

<span className={'bg-black bg-white'}>❌ does not detect contradicting classname</span>

I can still get the error shown by a third party rule (cssConflict) but the eslint-plugin-tailwindcss does not get the 2nd case...

@francoismassart
Copy link

Another issue I've met, if you use w-<number>, e.g. w-123 then Tailwind CSS will create a rule width: calc(var(--spacing) * <number>);...

There is no --width-... in the @theme yet Tailwind CSS will generate some king of arbitrary value based on spacing.

But the eslint-plugin-tailwindcss will raise a warning:
Classname 'w-123' is not a Tailwind CSS class! tailwindcss/no-custom-classname

@francoismassart
Copy link

Another issue I've met, if you use w-<number>, e.g. w-123 then Tailwind CSS will create a rule width: calc(var(--spacing) * <number>);...

There is no --width-... in the @theme yet Tailwind CSS will generate some king of arbitrary value based on spacing.

But the eslint-plugin-tailwindcss will raise a warning: Classname 'w-123' is not a Tailwind CSS class! tailwindcss/no-custom-classname

Strange, the issue did disappeared, maybe it was due to some internal mechanics of NX monorepo...

@francoismassart
Copy link

@hyoban, here what I plan to do:

  1. I would merge your MR in the master branch and make a release of it in the "beta" channel. It would ship "as is" with the bugs I encountered. The release would stay in the beta channel until I release the v4 of eslint-plugin-tailwindcss. The only purpose it is to allow an early access for user already using Tailwind CSS v4... After all, it is quite easy to disable the rules causing issue and it is only available as beta.

  2. Speaking of eslint-plugin-tailwindcss v4, as it is completely rewritten from scratch and because it is a side project that will take a long time to be released. The info about v4 are on the alpha/v4 branch. I plan to start with a single rule classnames-order, release, then add the other rules in future releases.

Could you edit this PR and mark it as ready for release ?

Thank you for your help, it is greatly appreciated.

@hyoban
Copy link
Owner Author

hyoban commented Jul 16, 2025

Thanks a lot, I will create a PR

@hyoban
Copy link
Owner Author

hyoban commented Jul 16, 2025

Close to prefer francoismassart#413

@hyoban hyoban closed this Jul 16, 2025
@francoismassart
Copy link

On my main projects we are using a monorepo and pipelines which run tests in their own context/setup.

Passing an hardcoded path pointing to the CSS config for the plugin is not possible nor ideal for us.

But here is how we fixed it:

  1. We converted the ESLint config file from .eslintrc.json to eslint.config.mjs
  2. In this eslint.config.mjs we have
import { dirname } from 'path'
import { fileURLToPath } from 'url'
...
{
  settings: {
    tailwindcss: {
      config: dirname(fileURLToPath(import.meta.url)) + '/apps/site/styles/tailwind.css',
      ...

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.