eslint-plugin-readable-tailwind icon indicating copy to clipboard operation
eslint-plugin-readable-tailwind copied to clipboard

Usage of presets in FlatConfig

Open kachkaev opened this issue 5 months ago • 5 comments

👋 again @schoero! I am using FlatConfig and I believe that I have found an incompatibility of the presets. Here is an MWE (eslint.config.js):

import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";

/** @type {import("eslint").Linter.Config[]} */
const configObjects = [
  // ...other configs...

  eslintPluginBetterTailwindcss.configs["recommended"] ?? {},
];

export default configObjects;

If type-checking is on, eslintPluginBetterTailwindcss.configs["recommended"] produces this tsc error:

Type '{ plugins: string[]; rules: { [x: string]: "warn" | "error"; }; } | {}' is not assignable to type 'Config<RulesRecord>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
  Type '{ plugins: string[]; rules: { [x: string]: "warn" | "error"; }; }' is not assignable to type 'Config<RulesRecord>'.
    Types of property 'plugins' are incompatible.
      Type 'string[]' is not assignable to type 'Record<string, Plugin>'.
        Index signature for type 'string' is missing in type 'string[]'.ts(2375)

Running eslint with the above config gives a runtime error:

Oops! Something went wrong! :(

ESLint: 9.29.0


A config object has a "plugins" key defined as an array of strings. It looks something like this:

    {
        "plugins": ["better-tailwindcss"]
    }

Flat config requires "plugins" to be an object, like this:

    {
        plugins: {
            better-tailwindcss: pluginObject
        }
    }

Please see the following page for information on how to convert your config object into the correct format:
https://eslint.org/docs/latest/use/configure/migration-guide#importing-plugins-and-custom-parsers

If you're using a shareable config that you cannot rewrite in flat config format, then use the compatibility utility:
https://eslint.org/docs/latest/use/configure/migration-guide#using-eslintrc-configs-in-flat-config

I was able to do this as a workaround for Tailwind v4:

import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";

/** @type {import("eslint").Linter.Config[]} */
const configObjects = [
  // ...other configs...

  {
    plugins: {
      "better-tailwindcss": eslintPluginBetterTailwindcss,
    },
    settings: {
      "better-tailwindcss": {
        entryPoint: `${import.meta.dirname}/path/to/entry-point.css`,
      },
    },
    rules: {
      "better-tailwindcss/enforce-consistent-class-order": "warn",
      "better-tailwindcss/enforce-consistent-variable-syntax": "warn",
      "better-tailwindcss/no-conflicting-classes": "error",
      "better-tailwindcss/no-duplicate-classes": "warn",
      "better-tailwindcss/no-restricted-classes": "error",
      "better-tailwindcss/no-unnecessary-whitespace": "warn",
      "better-tailwindcss/no-unregistered-classes": "error",
    },
  },
];

export default configObjects;

It was necessary to hand-pick the rules but everything worked in the end!

If you are looking for inspiration, take a look at [email protected]. This plugin supports both FlatConfig and LegacyConfig and I can use it like this:

import eslintPluginReactHooks from "eslint-plugin-react-hooks";

/** @type {import("eslint").Linter.Config[]} */
const configObjects = [
  // ...other configs...

  eslintPluginReactHooks.configs.recommended,
];

export default configObjects;

PS: I’m really glad that I’ve discovered eslint-plugin-better-tailwindcss, many thanks for working on it! It finally unblocks my migration to TailwindCSS v4 which was previously impossible because of https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/325 / https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/295.

kachkaev avatar Jul 01 '25 00:07 kachkaev

I think this would be a breaking change unfortunately. If I add plugins correctly, ESLint will throw this error because I already have declared a plugin named "better-tailwindcss".

Oops! Something went wrong! :(

ESLint: 9.30.0

ConfigError: Config (unnamed): Key "plugins": Cannot redefine plugin "better-tailwindcss".

All users who followed the example configurations will have already declared the plugin as well.

I'm not too hesitant about releasing major versions, but I have a few other changes in mind that I’d like to include, which will require some preparation first.

schoero avatar Jul 01 '25 05:07 schoero

I think this would be a breaking change unfortunately.

In theory, it may be avoidable if you add a new entry point:

import eslintPluginBetterTailwindcssConfigs from "eslint-plugin-better-tailwindcss/configs";

/** @type {import("eslint").Linter.Config[]} */
const configObjects = [
  // ...other configs...

  eslintPluginBetterTailwindcssConfigs.recommended,
];

export default configObjects;

Example: https://eslint-community.github.io/eslint-plugin-eslint-comments/#%F0%9F%93%96-usage

This entry point can be deprecated or removed in the next major version. Having said that, I believe that it's not a very high priority so your plan makes total sense.

kachkaev avatar Jul 01 '25 08:07 kachkaev

I'm also running into this issue. I think a good approach that is also backwards compatible is to duplicate the configs for the new flat config format and prefix them. Other eslint plugins also do this:

joshuajaco avatar Jul 05 '25 21:07 joshuajaco

I don't like that approach because the flat config is now the default and we shouldn't have to use a special entry point for the default. If anything, the legacy config should be separated into configs.legacy.recommended or configs["legacy/recommended"].

But again, this would be a breaking change.

What is the benefit of having plugins corrected? Is it just the simplified config or is there more that I'm missing?

schoero avatar Jul 06 '25 12:07 schoero

I don't like that approach because the flat config is now the default and we shouldn't have to use a special entry point for the default. If anything, the legacy config should be separated into configs.legacy.recommended or configs["legacy/recommended"].

I agree. I also maintain an eslint plugin and my plan is to swap them in the next major release (i.e. flat/recommended -> recommended and recommended -> legacy/recommended).

What is the benefit of having plugins corrected? Is it just the simplified config or is there more that I'm missing?

Yes. It's just less code to write.

Instead of:

export default [
  // ...
  plugins: { "better-tailwindcss": tailwindPlugin },
  rules: {
     // ...
     ...tailwindPlugin.configs.recommended.rules,
  },
];

You could write:

export default [
  // ...
  tailwindPlugin.configs["flat/recommended"],
];

joshuajaco avatar Jul 06 '25 13:07 joshuajaco