diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e62f00b5122..b7304af6781c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Upgrade (experimental)_: Fully convert simple JS configs to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639)) - _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603)) - _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635)) +- _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644)) ### Fixed diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts new file mode 100644 index 000000000000..2f29c130fb67 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts @@ -0,0 +1,31 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { expect, test } from 'vitest' +import { arbitraryValueToBareValue } from './arbitrary-value-to-bare-value' + +test.each([ + ['data-[selected]:flex', 'data-selected:flex'], + ['data-[foo=bar]:flex', 'data-[foo=bar]:flex'], + + ['supports-[gap]:flex', 'supports-gap:flex'], + ['supports-[display:grid]:flex', 'supports-[display:grid]:flex'], + + ['group-data-[selected]:flex', 'group-data-selected:flex'], + ['group-data-[foo=bar]:flex', 'group-data-[foo=bar]:flex'], + ['group-has-data-[selected]:flex', 'group-has-data-selected:flex'], + + ['aria-[selected]:flex', 'aria-[selected]:flex'], + ['aria-[selected="true"]:flex', 'aria-selected:flex'], + ['aria-[selected*="true"]:flex', 'aria-[selected*="true"]:flex'], + + ['group-aria-[selected]:flex', 'group-aria-[selected]:flex'], + ['group-aria-[selected="true"]:flex', 'group-aria-selected:flex'], + ['group-has-aria-[selected]:flex', 'group-has-aria-[selected]:flex'], + + ['max-lg:hover:data-[selected]:flex!', 'max-lg:hover:data-selected:flex!'], +])('%s => %s', async (candidate, result) => { + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + expect(arbitraryValueToBareValue(designSystem, {}, candidate)).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts new file mode 100644 index 000000000000..065f10807326 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts @@ -0,0 +1,94 @@ +import type { Config } from 'tailwindcss' +import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { segment } from '../../../../tailwindcss/src/utils/segment' +import { printCandidate } from '../candidates' + +export function arbitraryValueToBareValue( + designSystem: DesignSystem, + _userConfig: Config, + rawCandidate: string, +): string { + for (let candidate of parseCandidate(rawCandidate, designSystem)) { + let clone = structuredClone(candidate) + let changed = false + for (let variant of variants(clone)) { + // Convert `data-[selected]` to `data-selected` + if ( + variant.kind === 'functional' && + variant.root === 'data' && + variant.value?.kind === 'arbitrary' && + !variant.value.value.includes('=') + ) { + changed = true + variant.value = { + kind: 'named', + value: variant.value.value, + } + } + + // Convert `aria-[selected="true"]` to `aria-selected` + else if ( + variant.kind === 'functional' && + variant.root === 'aria' && + variant.value?.kind === 'arbitrary' && + (variant.value.value.endsWith('=true') || + variant.value.value.endsWith('="true"') || + variant.value.value.endsWith("='true'")) + ) { + let [key, _value] = segment(variant.value.value, '=') + if ( + // aria-[foo~="true"] + key[key.length - 1] === '~' || + // aria-[foo|="true"] + key[key.length - 1] === '|' || + // aria-[foo^="true"] + key[key.length - 1] === '^' || + // aria-[foo$="true"] + key[key.length - 1] === '$' || + // aria-[foo*="true"] + key[key.length - 1] === '*' + ) { + continue + } + + changed = true + variant.value = { + kind: 'named', + value: variant.value.value.slice(0, variant.value.value.indexOf('=')), + } + } + + // Convert `supports-[gap]` to `supports-gap` + else if ( + variant.kind === 'functional' && + variant.root === 'supports' && + variant.value?.kind === 'arbitrary' && + /^[a-z-][a-z0-9-]*$/i.test(variant.value.value) + ) { + changed = true + variant.value = { + kind: 'named', + value: variant.value.value, + } + } + } + + return changed ? printCandidate(designSystem, clone) : rawCandidate + } + + return rawCandidate +} + +function* variants(candidate: Candidate) { + function* inner(variant: Variant): Iterable { + yield variant + if (variant.kind === 'compound') { + yield* inner(variant.variant) + } + } + + for (let variant of candidate.variants) { + yield* inner(variant) + } +} diff --git a/packages/@tailwindcss-upgrade/src/template/migrate.ts b/packages/@tailwindcss-upgrade/src/template/migrate.ts index df9d22b15af8..50f2ca60e87f 100644 --- a/packages/@tailwindcss-upgrade/src/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/template/migrate.ts @@ -3,6 +3,7 @@ import path, { extname } from 'node:path' import type { Config } from 'tailwindcss' import type { DesignSystem } from '../../../tailwindcss/src/design-system' import { extractRawCandidates, replaceCandidateInContent } from './candidates' +import { arbitraryValueToBareValue } from './codemods/arbitrary-value-to-bare-value' import { automaticVarInjection } from './codemods/automatic-var-injection' import { bgGradient } from './codemods/bg-gradient' import { important } from './codemods/important' @@ -22,6 +23,7 @@ export const DEFAULT_MIGRATIONS: Migration[] = [ automaticVarInjection, bgGradient, simpleLegacyClasses, + arbitraryValueToBareValue, variantOrder, ]