From c0dd000c3cb174219d329207fbb03f21d6cc99af Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 1 Oct 2024 13:23:24 +0200 Subject: [PATCH] Template migrations: Add automatic var injection codemods (#14526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In v4, we're [removing automatic var injection](https://github.com/tailwindlabs/tailwindcss/pull/13657) (please refer to this PR for more detail as to why). Automatic var injection made it so that if you have a candidate like `bg-[--my-color]`, v3 would automatically wrap the content of the arbitrary section with a `var(…)`, resulting in the same as typing `bg-[var(--my-color)]`. This PR adds codemods that go over various arbitrary fields and does the `var(…)` injection for you. To be precise, we will add `var(…)` to: - Modifiers, e.g.: `bg-red-500/[var(--my-opacity)]` - Variants, e.g.: `supports-[var(--test)]:flex` - Arbitrary candidates, e.g.: `[color:var(--my-color)]` - Arbitrary values for functional candidates, e.g.: `bg-[var(--my-color)]` --------- Co-authored-by: Robin Malfait --- CHANGELOG.md | 5 +- integrations/upgrade/index.test.ts | 4 +- .../codemods/automatic-var-injection.test.ts | 59 +++++++ .../codemods/automatic-var-injection.ts | 155 ++++++++++++++++++ .../src/template/migrate.ts | 3 +- 5 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b7eab50d4f9..2467d850eee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501)) - Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553)) - Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556)) -- _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537])) - _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514)) +- _Experimental_: Add template codemods for removal of automatic `var(…)` injection ([#14526](https://github.com/tailwindlabs/tailwindcss/pull/14526)) +- _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537])) +- _Experimental_: Add template codemods for migrating important utilities (e.g. `!flex` to `flex!`) ([#14502](https://github.com/tailwindlabs/tailwindcss/pull/14502)) ### Fixed @@ -39,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Experimental_: Add CSS codemods for `@apply` ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14434)) - _Experimental_: Add CSS codemods for migrating `@tailwind` directives ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14411), [#14504](https://github.com/tailwindlabs/tailwindcss/pull/14504)) - _Experimental_: Add CSS codemods for migrating `@layer utilities` and `@layer components` ([#14455](https://github.com/tailwindlabs/tailwindcss/pull/14455)) -- _Experimental_: Add template codemods for migrating important utilities (e.g. `!flex` to `flex!`) ([#14502](https://github.com/tailwindlabs/tailwindcss/pull/14502)) ### Fixed diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 6393fbaa8c2b..a4e276e7cda1 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -19,7 +19,7 @@ test( `, 'src/index.html': html`

🤠👋

-
+
`, 'src/input.css': css` @tailwind base; @@ -35,7 +35,7 @@ test( 'src/index.html', html`

🤠👋

-
+
`, ) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts new file mode 100644 index 000000000000..538c1dc6550c --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts @@ -0,0 +1,59 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { expect, test } from 'vitest' +import { automaticVarInjection } from './automatic-var-injection' + +test.each([ + // Arbitrary candidates + ['[color:--my-color]', '[color:var(--my-color)]'], + ['[--my-color:red]', '[--my-color:red]'], + ['[--my-color:--my-other-color]', '[--my-color:var(--my-other-color)]'], + + // Arbitrary values for functional candidates + ['bg-[--my-color]', 'bg-[var(--my-color)]'], + ['bg-[color:--my-color]', 'bg-[color:var(--my-color)]'], + ['border-[length:--my-length]', 'border-[length:var(--my-length)]'], + ['border-[line-width:--my-width]', 'border-[line-width:var(--my-width)]'], + + // Can clean up the workaround for opting out of automatic var injection + ['bg-[_--my-color]', 'bg-[--my-color]'], + ['bg-[color:_--my-color]', 'bg-[color:--my-color]'], + ['border-[length:_--my-length]', 'border-[length:--my-length]'], + ['border-[line-width:_--my-width]', 'border-[line-width:--my-width]'], + + // Modifiers + ['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/[var(--my-opacity)]'], + ['bg-red-500/[--my-opacity]', 'bg-red-500/[var(--my-opacity)]'], + ['bg-[--my-color]/[--my-opacity]', 'bg-[var(--my-color)]/[var(--my-opacity)]'], + ['bg-[color:--my-color]/[--my-opacity]', 'bg-[color:var(--my-color)]/[var(--my-opacity)]'], + + // Can clean up the workaround for opting out of automatic var injection + ['[color:--my-color]/[_--my-opacity]', '[color:var(--my-color)]/[--my-opacity]'], + ['bg-red-500/[_--my-opacity]', 'bg-red-500/[--my-opacity]'], + ['bg-[--my-color]/[_--my-opacity]', 'bg-[var(--my-color)]/[--my-opacity]'], + ['bg-[color:--my-color]/[_--my-opacity]', 'bg-[color:var(--my-color)]/[--my-opacity]'], + + // Variants + ['supports-[--test]:flex', 'supports-[var(--test)]:flex'], + ['supports-[_--test]:flex', 'supports-[--test]:flex'], + + // Some properties never had var() injection in v3. + ['[scroll-timeline-name:--myTimeline]', '[scroll-timeline-name:--myTimeline]'], + ['[timeline-scope:--myScope]', '[timeline-scope:--myScope]'], + ['[view-timeline-name:--myTimeline]', '[view-timeline-name:--myTimeline]'], + ['[font-palette:--myPalette]', '[font-palette:--myPalette]'], + ['[anchor-name:--myAnchor]', '[anchor-name:--myAnchor]'], + ['[anchor-scope:--myScope]', '[anchor-scope:--myScope]'], + ['[position-anchor:--myAnchor]', '[position-anchor:--myAnchor]'], + ['[position-try-options:--myAnchor]', '[position-try-options:--myAnchor]'], + ['[scroll-timeline:--myTimeline]', '[scroll-timeline:--myTimeline]'], + ['[animation-timeline:--myAnimation]', '[animation-timeline:--myAnimation]'], + ['[view-timeline:--myTimeline]', '[view-timeline:--myTimeline]'], + ['[position-try:--myAnchor]', '[position-try:--myAnchor]'], +])('%s => %s', async (candidate, result) => { + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + let migrated = automaticVarInjection(designSystem, candidate) + expect(migrated).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts new file mode 100644 index 000000000000..52512802141d --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts @@ -0,0 +1,155 @@ +import { walk, WalkAction } from '../../../../tailwindcss/src/ast' +import type { Candidate, Variant } from '../../../../tailwindcss/src/candidate' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { printCandidate } from '../candidates' + +export function automaticVarInjection(designSystem: DesignSystem, rawCandidate: string): string { + for (let candidate of designSystem.parseCandidate(rawCandidate)) { + let didChange = false + + // Add `var(…)` in modifier position, e.g.: + // + // `bg-red-500/[--my-opacity]` => `bg-red-500/[var(--my-opacity)]` + if ( + 'modifier' in candidate && + candidate.modifier?.kind === 'arbitrary' && + !isAutomaticVarInjectionException(designSystem, candidate, candidate.modifier.value) + ) { + let { value, didChange: modifierDidChange } = injectVar(candidate.modifier.value) + candidate.modifier.value = value + didChange ||= modifierDidChange + } + + // Add `var(…)` to all variants, e.g.: + // + // `supports-[--test]:flex'` => `supports-[var(--test)]:flex` + for (let variant of candidate.variants) { + let didChangeVariant = injectVarIntoVariant(designSystem, variant) + if (didChangeVariant) { + didChange = true + } + } + + // Add `var(…)` to arbitrary candidates, e.g.: + // + // `[color:--my-color]` => `[color:var(--my-color)]` + if ( + candidate.kind === 'arbitrary' && + !isAutomaticVarInjectionException(designSystem, candidate, candidate.value) + ) { + let { value, didChange: valueDidChange } = injectVar(candidate.value) + candidate.value = value + didChange ||= valueDidChange + } + + // Add `var(…)` to arbitrary values for functional candidates, e.g.: + // + // `bg-[--my-color]` => `bg-[var(--my-color)]` + if ( + candidate.kind === 'functional' && + candidate.value && + candidate.value.kind === 'arbitrary' && + !isAutomaticVarInjectionException(designSystem, candidate, candidate.value.value) + ) { + let { value, didChange: valueDidChange } = injectVar(candidate.value.value) + candidate.value.value = value + didChange ||= valueDidChange + } + + if (didChange) { + return printCandidate(candidate) + } + } + return rawCandidate +} + +function injectVar(value: string): { value: string; didChange: boolean } { + let didChange = false + if (value.startsWith('--')) { + value = `var(${value})` + didChange = true + } else if (value.startsWith(' --')) { + value = value.slice(1) + didChange = true + } + return { value, didChange } +} + +function injectVarIntoVariant(designSystem: DesignSystem, variant: Variant): boolean { + let didChange = false + if ( + variant.kind === 'functional' && + variant.value && + variant.value.kind === 'arbitrary' && + !isAutomaticVarInjectionException( + designSystem, + createEmptyCandidate(variant), + variant.value.value, + ) + ) { + let { value, didChange: valueDidChange } = injectVar(variant.value.value) + variant.value.value = value + didChange ||= valueDidChange + } + + if (variant.kind === 'compound') { + let compoundDidChange = injectVarIntoVariant(designSystem, variant.variant) + if (compoundDidChange) { + didChange = true + } + } + + return didChange +} + +function createEmptyCandidate(variant: Variant) { + return { + kind: 'arbitrary' as const, + property: 'color', + value: 'red', + modifier: null, + variants: [variant], + important: false, + raw: 'candidate', + } satisfies Candidate +} + +const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([ + // Concrete properties + 'scroll-timeline-name', + 'timeline-scope', + 'view-timeline-name', + 'font-palette', + 'anchor-name', + 'anchor-scope', + 'position-anchor', + 'position-try-options', + + // Shorthand properties + 'scroll-timeline', + 'animation-timeline', + 'view-timeline', + 'position-try', +]) +// Some properties never had var() injection in v3. We need to convert the candidate to CSS +// so we can check the properties used by the utility. +function isAutomaticVarInjectionException( + designSystem: DesignSystem, + candidate: Candidate, + value: string, +): boolean { + let ast = designSystem.compileAstNodes(candidate).map((n) => n.node) + + let isException = false + walk(ast, (node) => { + if ( + node.kind === 'declaration' && + AUTO_VAR_INJECTION_EXCEPTIONS.has(node.property) && + node.value == value + ) { + isException = true + return WalkAction.Stop + } + }) + return isException +} diff --git a/packages/@tailwindcss-upgrade/src/template/migrate.ts b/packages/@tailwindcss-upgrade/src/template/migrate.ts index 44f2c6215119..c12f59139e1e 100644 --- a/packages/@tailwindcss-upgrade/src/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/template/migrate.ts @@ -2,6 +2,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import type { DesignSystem } from '../../../tailwindcss/src/design-system' import { extractRawCandidates, replaceCandidateInContent } from './candidates' +import { automaticVarInjection } from './codemods/automatic-var-injection' import { bgGradient } from './codemods/bg-gradient' import { important } from './codemods/important' @@ -10,7 +11,7 @@ export type Migration = (designSystem: DesignSystem, rawCandidate: string) => st export default async function migrateContents( designSystem: DesignSystem, contents: string, - migrations: Migration[] = [important, bgGradient], + migrations: Migration[] = [important, automaticVarInjection, bgGradient], ): Promise { let candidates = await extractRawCandidates(contents)