Skip to content

Commit c0dd000

Browse files
Template migrations: Add automatic var injection codemods (#14526)
In v4, we're [removing automatic var injection](#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 <malfait.robin@gmail.com>
1 parent 6a50e6e commit c0dd000

File tree

5 files changed

+221
-5
lines changed

5 files changed

+221
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501))
1313
- Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553))
1414
- Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556))
15-
- _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537]))
1615
- _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514))
16+
- _Experimental_: Add template codemods for removal of automatic `var(…)` injection ([#14526](https://github.com/tailwindlabs/tailwindcss/pull/14526))
17+
- _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537]))
18+
- _Experimental_: Add template codemods for migrating important utilities (e.g. `!flex` to `flex!`) ([#14502](https://github.com/tailwindlabs/tailwindcss/pull/14502))
1719

1820
### Fixed
1921

@@ -39,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3941
- _Experimental_: Add CSS codemods for `@apply` ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14434))
4042
- _Experimental_: Add CSS codemods for migrating `@tailwind` directives ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14411), [#14504](https://github.com/tailwindlabs/tailwindcss/pull/14504))
4143
- _Experimental_: Add CSS codemods for migrating `@layer utilities` and `@layer components` ([#14455](https://github.com/tailwindlabs/tailwindcss/pull/14455))
42-
- _Experimental_: Add template codemods for migrating important utilities (e.g. `!flex` to `flex!`) ([#14502](https://github.com/tailwindlabs/tailwindcss/pull/14502))
4344

4445
### Fixed
4546

integrations/upgrade/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ test(
1919
`,
2020
'src/index.html': html`
2121
<h1>🤠👋</h1>
22-
<div class="!flex sm:!block bg-gradient-to-t"></div>
22+
<div class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"></div>
2323
`,
2424
'src/input.css': css`
2525
@tailwind base;
@@ -35,7 +35,7 @@ test(
3535
'src/index.html',
3636
html`
3737
<h1>🤠👋</h1>
38-
<div class="flex! sm:block! bg-linear-to-t"></div>
38+
<div class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"></div>
3939
`,
4040
)
4141

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
2+
import { expect, test } from 'vitest'
3+
import { automaticVarInjection } from './automatic-var-injection'
4+
5+
test.each([
6+
// Arbitrary candidates
7+
['[color:--my-color]', '[color:var(--my-color)]'],
8+
['[--my-color:red]', '[--my-color:red]'],
9+
['[--my-color:--my-other-color]', '[--my-color:var(--my-other-color)]'],
10+
11+
// Arbitrary values for functional candidates
12+
['bg-[--my-color]', 'bg-[var(--my-color)]'],
13+
['bg-[color:--my-color]', 'bg-[color:var(--my-color)]'],
14+
['border-[length:--my-length]', 'border-[length:var(--my-length)]'],
15+
['border-[line-width:--my-width]', 'border-[line-width:var(--my-width)]'],
16+
17+
// Can clean up the workaround for opting out of automatic var injection
18+
['bg-[_--my-color]', 'bg-[--my-color]'],
19+
['bg-[color:_--my-color]', 'bg-[color:--my-color]'],
20+
['border-[length:_--my-length]', 'border-[length:--my-length]'],
21+
['border-[line-width:_--my-width]', 'border-[line-width:--my-width]'],
22+
23+
// Modifiers
24+
['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/[var(--my-opacity)]'],
25+
['bg-red-500/[--my-opacity]', 'bg-red-500/[var(--my-opacity)]'],
26+
['bg-[--my-color]/[--my-opacity]', 'bg-[var(--my-color)]/[var(--my-opacity)]'],
27+
['bg-[color:--my-color]/[--my-opacity]', 'bg-[color:var(--my-color)]/[var(--my-opacity)]'],
28+
29+
// Can clean up the workaround for opting out of automatic var injection
30+
['[color:--my-color]/[_--my-opacity]', '[color:var(--my-color)]/[--my-opacity]'],
31+
['bg-red-500/[_--my-opacity]', 'bg-red-500/[--my-opacity]'],
32+
['bg-[--my-color]/[_--my-opacity]', 'bg-[var(--my-color)]/[--my-opacity]'],
33+
['bg-[color:--my-color]/[_--my-opacity]', 'bg-[color:var(--my-color)]/[--my-opacity]'],
34+
35+
// Variants
36+
['supports-[--test]:flex', 'supports-[var(--test)]:flex'],
37+
['supports-[_--test]:flex', 'supports-[--test]:flex'],
38+
39+
// Some properties never had var() injection in v3.
40+
['[scroll-timeline-name:--myTimeline]', '[scroll-timeline-name:--myTimeline]'],
41+
['[timeline-scope:--myScope]', '[timeline-scope:--myScope]'],
42+
['[view-timeline-name:--myTimeline]', '[view-timeline-name:--myTimeline]'],
43+
['[font-palette:--myPalette]', '[font-palette:--myPalette]'],
44+
['[anchor-name:--myAnchor]', '[anchor-name:--myAnchor]'],
45+
['[anchor-scope:--myScope]', '[anchor-scope:--myScope]'],
46+
['[position-anchor:--myAnchor]', '[position-anchor:--myAnchor]'],
47+
['[position-try-options:--myAnchor]', '[position-try-options:--myAnchor]'],
48+
['[scroll-timeline:--myTimeline]', '[scroll-timeline:--myTimeline]'],
49+
['[animation-timeline:--myAnimation]', '[animation-timeline:--myAnimation]'],
50+
['[view-timeline:--myTimeline]', '[view-timeline:--myTimeline]'],
51+
['[position-try:--myAnchor]', '[position-try:--myAnchor]'],
52+
])('%s => %s', async (candidate, result) => {
53+
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
54+
base: __dirname,
55+
})
56+
57+
let migrated = automaticVarInjection(designSystem, candidate)
58+
expect(migrated).toEqual(result)
59+
})
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { walk, WalkAction } from '../../../../tailwindcss/src/ast'
2+
import type { Candidate, Variant } from '../../../../tailwindcss/src/candidate'
3+
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
4+
import { printCandidate } from '../candidates'
5+
6+
export function automaticVarInjection(designSystem: DesignSystem, rawCandidate: string): string {
7+
for (let candidate of designSystem.parseCandidate(rawCandidate)) {
8+
let didChange = false
9+
10+
// Add `var(…)` in modifier position, e.g.:
11+
//
12+
// `bg-red-500/[--my-opacity]` => `bg-red-500/[var(--my-opacity)]`
13+
if (
14+
'modifier' in candidate &&
15+
candidate.modifier?.kind === 'arbitrary' &&
16+
!isAutomaticVarInjectionException(designSystem, candidate, candidate.modifier.value)
17+
) {
18+
let { value, didChange: modifierDidChange } = injectVar(candidate.modifier.value)
19+
candidate.modifier.value = value
20+
didChange ||= modifierDidChange
21+
}
22+
23+
// Add `var(…)` to all variants, e.g.:
24+
//
25+
// `supports-[--test]:flex'` => `supports-[var(--test)]:flex`
26+
for (let variant of candidate.variants) {
27+
let didChangeVariant = injectVarIntoVariant(designSystem, variant)
28+
if (didChangeVariant) {
29+
didChange = true
30+
}
31+
}
32+
33+
// Add `var(…)` to arbitrary candidates, e.g.:
34+
//
35+
// `[color:--my-color]` => `[color:var(--my-color)]`
36+
if (
37+
candidate.kind === 'arbitrary' &&
38+
!isAutomaticVarInjectionException(designSystem, candidate, candidate.value)
39+
) {
40+
let { value, didChange: valueDidChange } = injectVar(candidate.value)
41+
candidate.value = value
42+
didChange ||= valueDidChange
43+
}
44+
45+
// Add `var(…)` to arbitrary values for functional candidates, e.g.:
46+
//
47+
// `bg-[--my-color]` => `bg-[var(--my-color)]`
48+
if (
49+
candidate.kind === 'functional' &&
50+
candidate.value &&
51+
candidate.value.kind === 'arbitrary' &&
52+
!isAutomaticVarInjectionException(designSystem, candidate, candidate.value.value)
53+
) {
54+
let { value, didChange: valueDidChange } = injectVar(candidate.value.value)
55+
candidate.value.value = value
56+
didChange ||= valueDidChange
57+
}
58+
59+
if (didChange) {
60+
return printCandidate(candidate)
61+
}
62+
}
63+
return rawCandidate
64+
}
65+
66+
function injectVar(value: string): { value: string; didChange: boolean } {
67+
let didChange = false
68+
if (value.startsWith('--')) {
69+
value = `var(${value})`
70+
didChange = true
71+
} else if (value.startsWith(' --')) {
72+
value = value.slice(1)
73+
didChange = true
74+
}
75+
return { value, didChange }
76+
}
77+
78+
function injectVarIntoVariant(designSystem: DesignSystem, variant: Variant): boolean {
79+
let didChange = false
80+
if (
81+
variant.kind === 'functional' &&
82+
variant.value &&
83+
variant.value.kind === 'arbitrary' &&
84+
!isAutomaticVarInjectionException(
85+
designSystem,
86+
createEmptyCandidate(variant),
87+
variant.value.value,
88+
)
89+
) {
90+
let { value, didChange: valueDidChange } = injectVar(variant.value.value)
91+
variant.value.value = value
92+
didChange ||= valueDidChange
93+
}
94+
95+
if (variant.kind === 'compound') {
96+
let compoundDidChange = injectVarIntoVariant(designSystem, variant.variant)
97+
if (compoundDidChange) {
98+
didChange = true
99+
}
100+
}
101+
102+
return didChange
103+
}
104+
105+
function createEmptyCandidate(variant: Variant) {
106+
return {
107+
kind: 'arbitrary' as const,
108+
property: 'color',
109+
value: 'red',
110+
modifier: null,
111+
variants: [variant],
112+
important: false,
113+
raw: 'candidate',
114+
} satisfies Candidate
115+
}
116+
117+
const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([
118+
// Concrete properties
119+
'scroll-timeline-name',
120+
'timeline-scope',
121+
'view-timeline-name',
122+
'font-palette',
123+
'anchor-name',
124+
'anchor-scope',
125+
'position-anchor',
126+
'position-try-options',
127+
128+
// Shorthand properties
129+
'scroll-timeline',
130+
'animation-timeline',
131+
'view-timeline',
132+
'position-try',
133+
])
134+
// Some properties never had var() injection in v3. We need to convert the candidate to CSS
135+
// so we can check the properties used by the utility.
136+
function isAutomaticVarInjectionException(
137+
designSystem: DesignSystem,
138+
candidate: Candidate,
139+
value: string,
140+
): boolean {
141+
let ast = designSystem.compileAstNodes(candidate).map((n) => n.node)
142+
143+
let isException = false
144+
walk(ast, (node) => {
145+
if (
146+
node.kind === 'declaration' &&
147+
AUTO_VAR_INJECTION_EXCEPTIONS.has(node.property) &&
148+
node.value == value
149+
) {
150+
isException = true
151+
return WalkAction.Stop
152+
}
153+
})
154+
return isException
155+
}

packages/@tailwindcss-upgrade/src/template/migrate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
22
import path from 'node:path'
33
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
44
import { extractRawCandidates, replaceCandidateInContent } from './candidates'
5+
import { automaticVarInjection } from './codemods/automatic-var-injection'
56
import { bgGradient } from './codemods/bg-gradient'
67
import { important } from './codemods/important'
78

@@ -10,7 +11,7 @@ export type Migration = (designSystem: DesignSystem, rawCandidate: string) => st
1011
export default async function migrateContents(
1112
designSystem: DesignSystem,
1213
contents: string,
13-
migrations: Migration[] = [important, bgGradient],
14+
migrations: Migration[] = [important, automaticVarInjection, bgGradient],
1415
): Promise<string> {
1516
let candidates = await extractRawCandidates(contents)
1617

0 commit comments

Comments
 (0)