From e6d3fa0cc4f6fb45008a17759a468b04506a55ae Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 11 Oct 2024 16:41:37 +0200 Subject: [PATCH] Migrate `aria-*`, `data-*` and `supports-*` variants from arbitrary values to bare values (#14644) This PR adds a new codemod that can migrate `data-*` and `aria-*` variants using arbitrary values to bare values. In Tailwind CSS v3, if you want to conditionally apply a class using data attributes, then you can write `data-[selected]:flex`. This requires the DOM element to have a `data-selected=""` attribute. In Tailwind CSS v4 we can simplify this, by dropping the brackets and by using `data-selected:flex` directly. This migration operates on the internal AST, which means that this also just works for compound variants such as `group-has-data-[selected]:flex` (which turns into `group-has-data-selected:flex`). Additionally, this codemod is also applicable to `aria-*` variants. The biggest difference is that in v4 `aria-selected` maps to an attribute of `aria-selected="true"`. This means that we can only migrate `aria=[selected="true"]:flex` to `aria-selected:flex`. Last but not least, we also migrate `supports-[gap]` to `supports-gap` if the passed in value looks like a property. If not, e.g.: `supports-[display:grid]` then it stays as-is. --- CHANGELOG.md | 1 + .../arbitrary-value-to-bare-value.test.ts | 31 ++++++ .../codemods/arbitrary-value-to-bare-value.ts | 94 +++++++++++++++++++ .../src/template/migrate.ts | 2 + 4 files changed, 128 insertions(+) create mode 100644 packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f8f342d6..e1ad6f1f6 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 000000000..2f29c130f --- /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 000000000..065f10807 --- /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 df9d22b15..50f2ca60e 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, ]