feat: add eslint-plugin-better-tailwindcss for Tailwind v4 linting (#9245)

## Summary

Add `eslint-plugin-better-tailwindcss` to the ESLint toolchain for
Tailwind CSS v4 class linting.

## Changes

- **What**: Integrate `eslint-plugin-better-tailwindcss` (v4.3.1) with
the recommended config, pointed at the design-system CSS entry point for
v4 theme resolution. Five rules are enabled initially:
`enforce-canonical-classes`, `no-deprecated-classes`,
`no-conflicting-classes`, `no-duplicate-classes`,
`no-unnecessary-whitespace`. Three rules are disabled pending follow-up:
`no-unknown-classes` (needs PrimeIcon/custom class whitelisting),
`enforce-consistent-line-wrapping` (oxfmt conflict risk),
`enforce-consistent-class-order` (large batch change).
- **Dependencies**: `eslint-plugin-better-tailwindcss` ^4.3.1
- Fix conflicting `outline outline-1` classes in
`FormDropdownMenuActions.vue` (caught by the new
`no-conflicting-classes` rule).

## Review Focus

- Is the rule severity/enablement strategy appropriate for incremental
adoption?
- The 700 warnings (mostly `enforce-canonical-classes` and
`no-deprecated-classes`) are all auto-fixable via `eslint --fix` —
should we batch-fix them in this PR or a follow-up?

Fixes COM-15518

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9245-feat-add-eslint-plugin-better-tailwindcss-for-Tailwind-v4-linting-3136d73d365081df8a64dd55962d073f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
Christian Byrne
2026-03-04 15:34:23 -08:00
committed by GitHub
parent 57a919fad2
commit b8edb11ac1
10 changed files with 109 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import pluginJs from '@eslint/js'
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
import betterTailwindcss from 'eslint-plugin-better-tailwindcss'
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
import { importX } from 'eslint-plugin-import-x'
import oxlint from 'eslint-plugin-oxlint'
@@ -111,6 +112,28 @@ export default defineConfig([
tseslintConfigs.recommended,
// Difference in typecheck on CI vs Local
pluginVue.configs['flat/recommended'],
// Tailwind CSS v4 linting (class ordering, duplicates, conflicts, etc.)
betterTailwindcss.configs.recommended,
{
settings: {
'better-tailwindcss': {
entryPoint: 'packages/design-system/src/css/style.css'
}
},
rules: {
// Off: requires whitelisting non-Tailwind classes (PrimeIcons, custom CSS)
'better-tailwindcss/no-unknown-classes': 'off',
// Off: may conflict with oxfmt formatting
'better-tailwindcss/enforce-consistent-line-wrapping': 'off',
// Off: large batch change, enable and apply with `eslint --fix`
'better-tailwindcss/enforce-consistent-class-order': 'off',
// Off: large batch change (v3→v4 renames like rounded→rounded-sm),
// enable and apply with `eslint --fix` in a follow-up PR
'better-tailwindcss/enforce-canonical-classes': 'off',
// Off: large batch change, enable and apply with `eslint --fix`
'better-tailwindcss/no-deprecated-classes': 'off'
}
},
// Disables ESLint rules that conflict with formatters
eslintConfigPrettier,
// @ts-expect-error Type incompatibility between storybook plugin and ESLint config types

View File

@@ -146,6 +146,7 @@
"eslint": "catalog:",
"eslint-config-prettier": "catalog:",
"eslint-import-resolver-typescript": "catalog:",
"eslint-plugin-better-tailwindcss": "catalog:",
"eslint-plugin-import-x": "catalog:",
"eslint-plugin-oxlint": "catalog:",
"eslint-plugin-storybook": "catalog:",

75
pnpm-lock.yaml generated
View File

@@ -180,6 +180,9 @@ catalogs:
eslint-import-resolver-typescript:
specifier: ^4.4.4
version: 4.4.4
eslint-plugin-better-tailwindcss:
specifier: ^4.3.1
version: 4.3.1
eslint-plugin-import-x:
specifier: ^4.16.1
version: 4.16.1
@@ -636,6 +639,9 @@ importers:
eslint-import-resolver-typescript:
specifier: 'catalog:'
version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-better-tailwindcss:
specifier: 'catalog:'
version: 4.3.1(eslint@9.39.1(jiti@2.6.1))(oxlint@1.49.0(oxlint-tsgolint@0.14.2))(tailwindcss@4.2.0)(typescript@5.9.3)
eslint-plugin-import-x:
specifier: 'catalog:'
version: 4.16.1(@typescript-eslint/utils@8.56.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))
@@ -1916,6 +1922,10 @@ packages:
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/css-tree@3.6.9':
resolution: {integrity: sha512-3D5/OHibNEGk+wKwNwMbz63NMf367EoR4mVNNpxddCHKEb2Nez7z62J2U6YjtErSsZDoY0CsccmoUpdEbkogNA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
'@eslint/eslintrc@3.3.3':
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5314,6 +5324,19 @@ packages:
eslint-import-resolver-webpack:
optional: true
eslint-plugin-better-tailwindcss@4.3.1:
resolution: {integrity: sha512-b6xM31GukKz0WlgMD0tQdY/rLjf/9mWIk8EcA45ngOKJPPQf1C482xZtBlT357jyunQE2mOk4NlPcL4i9Pr85A==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23.0.0}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0
oxlint: ^1.35.0
tailwindcss: ^3.3.0 || ^4.1.17
peerDependenciesMeta:
eslint:
optional: true
oxlint:
optional: true
eslint-plugin-import-x@4.16.1:
resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -6558,6 +6581,9 @@ packages:
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
mdn-data@2.23.0:
resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
@@ -7780,6 +7806,10 @@ packages:
resolution: {integrity: sha512-2SG1TnJGjMkD4+gblONMGYSrwAzYi+ymOitD+Jb/iMYm57nH20PlkVeMQRah3yDMKEa0QQYUF/QPWpdW7C6zNg==}
engines: {node: ^14.18.0 || >=16.0.0}
synckit@0.11.12:
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
engines: {node: ^14.18.0 || >=16.0.0}
table@6.9.0:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'}
@@ -7788,6 +7818,10 @@ packages:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
tailwind-csstree@0.1.4:
resolution: {integrity: sha512-FzD187HuFIZEyeR7Xy6sJbJll2d4SybS90satC8SKIuaNRC05CxMvdzN7BUsfDQffcnabckRM5OIcfArjsZ0mg==}
engines: {node: '>=18.18'}
tailwind-merge@2.6.0:
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
@@ -7918,6 +7952,10 @@ packages:
ts-map@1.0.3:
resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==}
tsconfig-paths-webpack-plugin@4.2.0:
resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==}
engines: {node: '>=10.13.0'}
tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
@@ -9708,6 +9746,11 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
'@eslint/css-tree@3.6.9':
dependencies:
mdn-data: 2.23.0
source-map-js: 1.2.1
'@eslint/eslintrc@3.3.3':
dependencies:
ajv: 6.14.0
@@ -13457,6 +13500,23 @@ snapshots:
- supports-color
optional: true
eslint-plugin-better-tailwindcss@4.3.1(eslint@9.39.1(jiti@2.6.1))(oxlint@1.49.0(oxlint-tsgolint@0.14.2))(tailwindcss@4.2.0)(typescript@5.9.3):
dependencies:
'@eslint/css-tree': 3.6.9
'@valibot/to-json-schema': 1.5.0(valibot@1.2.0(typescript@5.9.3))
enhanced-resolve: 5.19.0
jiti: 2.6.1
synckit: 0.11.12
tailwind-csstree: 0.1.4
tailwindcss: 4.2.0
tsconfig-paths-webpack-plugin: 4.2.0
valibot: 1.2.0(typescript@5.9.3)
optionalDependencies:
eslint: 9.39.1(jiti@2.6.1)
oxlint: 1.49.0(oxlint-tsgolint@0.14.2)
transitivePeerDependencies:
- typescript
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@typescript-eslint/types': 8.56.0
@@ -14893,6 +14953,8 @@ snapshots:
mdn-data@2.12.2: {}
mdn-data@2.23.0: {}
mdurl@2.0.0: {}
media-encoder-host-broker@8.0.19:
@@ -16571,6 +16633,10 @@ snapshots:
'@pkgr/core': 0.2.9
tslib: 2.8.1
synckit@0.11.12:
dependencies:
'@pkgr/core': 0.2.9
table@6.9.0:
dependencies:
ajv: 8.18.0
@@ -16581,6 +16647,8 @@ snapshots:
tagged-tag@1.0.0: {}
tailwind-csstree@0.1.4: {}
tailwind-merge@2.6.0: {}
tailwindcss-primeui@0.6.1(tailwindcss@4.2.0):
@@ -16691,6 +16759,13 @@ snapshots:
ts-map@1.0.3: {}
tsconfig-paths-webpack-plugin@4.2.0:
dependencies:
chalk: 4.1.2
enhanced-resolve: 5.19.0
tapable: 2.3.0
tsconfig-paths: 4.2.0
tsconfig-paths@3.15.0:
dependencies:
'@types/json5': 0.0.29

View File

@@ -61,6 +61,7 @@ catalog:
eslint: ^9.39.1
eslint-config-prettier: ^10.1.8
eslint-import-resolver-typescript: ^4.4.4
eslint-plugin-better-tailwindcss: ^4.3.1
eslint-plugin-import-x: ^4.16.1
eslint-plugin-oxlint: 1.25.0
eslint-plugin-storybook: ^10.2.10

View File

@@ -64,7 +64,7 @@
layout="vertical"
:pt:gutter="
cn(
'rounded-tl-lg rounded-tr-lg ',
'rounded-tl-lg rounded-tr-lg',
!(bottomPanelVisible && !focusMode) && 'hidden'
)
"

View File

@@ -12,7 +12,7 @@
:class="
cn(
'flex items-center gap-1 rounded-full hover:bg-interface-button-hover-surface justify-center',
compact && 'size-full '
compact && 'size-full'
)
"
>

View File

@@ -15,7 +15,7 @@
v-if="collapsible"
:class="
cn(
'pi transition-transform duration-200 text-xs text-text-secondary ',
'pi transition-transform duration-200 text-xs text-text-secondary',
isCollapsed ? 'pi-chevron-right' : 'pi-chevron-down'
)
"

View File

@@ -175,7 +175,7 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
class: cn(
context.top && 'border-t-node-component-tooltip-border',
context.bottom && 'border-b-node-component-tooltip-border',
context.left && 'border-l-node-component-tooltip-border ',
context.left && 'border-l-node-component-tooltip-border',
context.right && 'border-r-node-component-tooltip-border'
)
})

View File

@@ -42,7 +42,7 @@ const baseModelSelected = defineModel<Set<string>>('baseModelSelected', {
})
const actionButtonStyle = cn(
'h-8 bg-zinc-500/20 rounded-lg outline outline-1 outline-offset-[-1px] outline-node-component-border transition-all duration-150'
'h-8 bg-zinc-500/20 rounded-lg outline-1 -outline-offset-1 outline-node-component-border transition-all duration-150'
)
const layoutSwitchItemStyle =
@@ -157,7 +157,7 @@ function toggleBaseModelSelection(item: FilterOption) {
cn(
'flex flex-col gap-2 p-2 min-w-32',
'bg-component-node-background',
'rounded-lg outline outline-offset-[-1px] outline-component-node-border'
'rounded-lg outline -outline-offset-1 outline-component-node-border'
)
"
>
@@ -219,7 +219,7 @@ function toggleBaseModelSelection(item: FilterOption) {
cn(
'flex flex-col gap-2 p-2 min-w-32',
'bg-component-node-background',
'rounded-lg outline outline-offset-[-1px] outline-component-node-border'
'rounded-lg outline -outline-offset-1 outline-component-node-border'
)
"
>
@@ -281,7 +281,7 @@ function toggleBaseModelSelection(item: FilterOption) {
cn(
'flex flex-col gap-2 p-2 min-w-32',
'bg-component-node-background',
'rounded-lg outline outline-offset-[-1px] outline-component-node-border'
'rounded-lg outline -outline-offset-1 outline-component-node-border'
)
"
>

View File

@@ -1,7 +1,7 @@
import { cn } from '@comfyorg/tailwind-utils'
const sharedButtonClasses = cn(
'inline-flex items-center justify-center border-0 bg-transparent text-inherit transition-colors duration-150 ease-in-out ',
'inline-flex items-center justify-center border-0 bg-transparent text-inherit transition-colors duration-150 ease-in-out',
'hover:bg-node-component-surface-hovered active:bg-node-component-surface-selected',
'disabled:bg-node-component-disabled disabled:text-node-icon-disabled disabled:cursor-not-allowed'
)