Usage of presets in FlatConfig
👋 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.
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.
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.
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:
eslint-plugin-reactusesreactPlugin.configs.flat.recommended.eslint-plugin-nusesnodePlugin.configs["flat/recommended"].
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?
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.recommendedorconfigs["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"],
];