Skip to content

Commit 49484f0

Browse files
Do not migrate legacy classes with custom values (#14976)
This PR fixes an issue where we migrated classes such as `rounded` to `rounded-sm` (see: #14875) However, if you override the values in your `tailwind.config.js` file, then the migration might not be correct. This PR makes sure to only migrate the classes if you haven't overridden the values in your `tailwind.config.js` file. --------- Co-authored-by: Philipp Spiess <[email protected]>
1 parent dd85aad commit 49484f0

File tree

11 files changed

+497
-111
lines changed

11 files changed

+497
-111
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
### Fixed
1717

1818
- Ensure that CSS inside Svelte `<style>` blocks always run the expected Svelte processors when using the Vite extension ([#14981](https://github.com/tailwindlabs/tailwindcss/pull/14981))
19+
- _Upgrade (experimental)_: Ensure it's safe to migrate `blur`, `rounded`, or `shadow` ([#14979](https://github.com/tailwindlabs/tailwindcss/pull/14979))
20+
- _Upgrade (experimental)_: Do not rename classes using custom defined theme values ([#14976](https://github.com/tailwindlabs/tailwindcss/pull/14976))
1921

2022
## [4.0.0-alpha.33] - 2024-11-11
2123

integrations/upgrade/index.test.ts

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect } from 'vitest'
2-
import { candidate, css, html, js, json, test } from '../utils'
2+
import { candidate, css, html, js, json, test, ts } from '../utils'
33

44
test(
55
'error when no CSS file with @tailwind is used',
@@ -1642,3 +1642,200 @@ test(
16421642
expect(pkg.devDependencies['prettier-plugin-tailwindcss']).not.toEqual('0.5.0')
16431643
},
16441644
)
1645+
1646+
test(
1647+
'only migrate legacy classes when it is safe to do so',
1648+
{
1649+
fs: {
1650+
'package.json': json`
1651+
{
1652+
"dependencies": {
1653+
"tailwindcss": "^3.4.14",
1654+
"@tailwindcss/upgrade": "workspace:^"
1655+
},
1656+
"devDependencies": {
1657+
"prettier-plugin-tailwindcss": "0.5.0"
1658+
}
1659+
}
1660+
`,
1661+
'tailwind.config.js': js`
1662+
module.exports = {
1663+
content: ['./*.html'],
1664+
theme: {
1665+
// Overrides the default boxShadow entirely so none of the
1666+
// migrations are safe.
1667+
boxShadow: {
1668+
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
1669+
},
1670+
1671+
extend: {
1672+
// Changes the "before" class definition. 'blur' -> 'blur-sm' is
1673+
// not safe because 'blur' has a custom value.
1674+
//
1675+
// But 'blur-sm' -> 'blur-xs' is safe because 'blur-xs' uses the
1676+
// default value.
1677+
blur: {
1678+
DEFAULT: 'var(--custom-default-blur)',
1679+
},
1680+
1681+
// Changes the "after" class definition. 'rounded' -> 'rounded-sm' is
1682+
// not safe because 'rounded-sm' has a custom value.
1683+
borderRadius: {
1684+
sm: 'var(--custom-rounded-sm)',
1685+
},
1686+
},
1687+
},
1688+
}
1689+
`,
1690+
'index.css': css`
1691+
@tailwind base;
1692+
@tailwind components;
1693+
@tailwind utilities;
1694+
`,
1695+
'index.html': html`
1696+
<div>
1697+
<div class="shadow shadow-sm shadow-xs"></div>
1698+
<div class="blur blur-sm"></div>
1699+
<div class="rounded rounded-sm"></div>
1700+
</div>
1701+
`,
1702+
},
1703+
},
1704+
async ({ fs, exec }) => {
1705+
await exec('npx @tailwindcss/upgrade --force')
1706+
1707+
// Files should not be modified
1708+
expect(await fs.dumpFiles('./*.{js,css,html}')).toMatchInlineSnapshot(`
1709+
"
1710+
--- index.html ---
1711+
<div>
1712+
<div class="shadow shadow-sm shadow-xs"></div>
1713+
<div class="blur blur-xs"></div>
1714+
<div class="rounded rounded-sm"></div>
1715+
</div>
1716+
1717+
--- index.css ---
1718+
@import 'tailwindcss';
1719+
1720+
@theme {
1721+
--shadow-*: initial;
1722+
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
1723+
1724+
--blur: var(--custom-default-blur);
1725+
1726+
--radius-sm: var(--custom-rounded-sm);
1727+
}
1728+
1729+
/*
1730+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
1731+
so we've added these compatibility styles to make sure everything still
1732+
looks the same as it did with Tailwind CSS v3.
1733+
1734+
If we ever want to remove these styles, we need to add an explicit border
1735+
color utility to any element that depends on these defaults.
1736+
*/
1737+
@layer base {
1738+
*,
1739+
::after,
1740+
::before,
1741+
::backdrop,
1742+
::file-selector-button {
1743+
border-color: var(--color-gray-200, currentColor);
1744+
}
1745+
}
1746+
"
1747+
`)
1748+
},
1749+
)
1750+
1751+
test(
1752+
'make suffix-less migrations safe (e.g.: `blur`, `rounded`, `shadow`)',
1753+
{
1754+
fs: {
1755+
'package.json': json`
1756+
{
1757+
"dependencies": {
1758+
"tailwindcss": "^3.4.14",
1759+
"@tailwindcss/upgrade": "workspace:^"
1760+
},
1761+
"devDependencies": {
1762+
"prettier-plugin-tailwindcss": "0.5.0"
1763+
}
1764+
}
1765+
`,
1766+
'tailwind.config.js': js`
1767+
module.exports = {
1768+
content: ['./*.{html,tsx}'],
1769+
}
1770+
`,
1771+
'index.css': css`
1772+
@tailwind base;
1773+
@tailwind components;
1774+
@tailwind utilities;
1775+
`,
1776+
'index.html': html`
1777+
<div class="rounded blur shadow"></div>
1778+
`,
1779+
'example-component.tsx': ts`
1780+
type Star = [
1781+
x: number,
1782+
y: number,
1783+
dim?: boolean,
1784+
blur?: boolean,
1785+
rounded?: boolean,
1786+
shadow?: boolean,
1787+
]
1788+
1789+
function Star({ point: [cx, cy, dim, blur, rounded, shadow] }: { point: Star }) {
1790+
return <svg class="rounded shadow blur" filter={blur ? 'url(…)' : undefined} />
1791+
}
1792+
`,
1793+
},
1794+
},
1795+
async ({ fs, exec }) => {
1796+
await exec('npx @tailwindcss/upgrade --force')
1797+
1798+
// Files should not be modified
1799+
expect(await fs.dumpFiles('./*.{js,css,html,tsx}')).toMatchInlineSnapshot(`
1800+
"
1801+
--- index.html ---
1802+
<div class="rounded-sm blur-sm shadow-sm"></div>
1803+
1804+
--- index.css ---
1805+
@import 'tailwindcss';
1806+
1807+
/*
1808+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
1809+
so we've added these compatibility styles to make sure everything still
1810+
looks the same as it did with Tailwind CSS v3.
1811+
1812+
If we ever want to remove these styles, we need to add an explicit border
1813+
color utility to any element that depends on these defaults.
1814+
*/
1815+
@layer base {
1816+
*,
1817+
::after,
1818+
::before,
1819+
::backdrop,
1820+
::file-selector-button {
1821+
border-color: var(--color-gray-200, currentColor);
1822+
}
1823+
}
1824+
1825+
--- example-component.tsx ---
1826+
type Star = [
1827+
x: number,
1828+
y: number,
1829+
dim?: boolean,
1830+
blur?: boolean,
1831+
rounded?: boolean,
1832+
shadow?: boolean,
1833+
]
1834+
1835+
function Star({ point: [cx, cy, dim, blur, rounded, shadow] }: { point: Star }) {
1836+
return <svg class="rounded-sm shadow-sm blur-sm" filter={blur ? 'url(…)' : undefined} />
1837+
}
1838+
"
1839+
`)
1840+
},
1841+
)

integrations/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,20 @@ export function test(
421421

422422
options.onTestFinished(dispose)
423423

424+
// Make it a git repository, and commit all files
425+
if (only || debug) {
426+
try {
427+
await context.exec('git init', { cwd: root })
428+
await context.exec('git add --all', { cwd: root })
429+
await context.exec('git commit -m "before migration"', { cwd: root })
430+
} catch (error: any) {
431+
console.error(error)
432+
console.error(error.stdout?.toString())
433+
console.error(error.stderr?.toString())
434+
throw error
435+
}
436+
}
437+
424438
return await testCallback(context)
425439
},
426440
)

packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,26 @@ export function migrateAtApply({
3434
return [...variants, utility].join(':')
3535
})
3636

37-
// If we have a valid designSystem and config setup, we can run all
38-
// candidate migrations on each utility
39-
params = params.map((param) => migrateCandidate(designSystem, userConfig, param))
40-
41-
atRule.params = params.join('').trim()
37+
return async () => {
38+
// If we have a valid designSystem and config setup, we can run all
39+
// candidate migrations on each utility
40+
params = await Promise.all(
41+
params.map(async (param) => await migrateCandidate(designSystem, userConfig, param)),
42+
)
43+
44+
atRule.params = params.join('').trim()
45+
}
4246
}
4347

4448
return {
4549
postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply',
46-
OnceExit(root) {
47-
root.walkAtRules('apply', migrate)
50+
async OnceExit(root) {
51+
let migrations: (() => void)[] = []
52+
root.walkAtRules('apply', (atRule) => {
53+
migrations.push(migrate(atRule))
54+
})
55+
56+
await Promise.allSettled(migrations.map((m) => m()))
4857
},
4958
}
5059
}

packages/@tailwindcss-upgrade/src/template/codemods/important.ts

Lines changed: 3 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,7 @@ import type { Config } from 'tailwindcss'
22
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
33
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
44
import { printCandidate } from '../candidates'
5-
6-
const QUOTES = ['"', "'", '`']
7-
const LOGICAL_OPERATORS = ['&&', '||', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
8-
const CONDITIONAL_TEMPLATE_SYNTAX = [
9-
// Vue
10-
/v-else-if=['"]$/,
11-
/v-if=['"]$/,
12-
/v-show=['"]$/,
13-
14-
// Alpine
15-
/x-if=['"]$/,
16-
/x-show=['"]$/,
17-
]
5+
import { isSafeMigration } from '../is-safe-migration'
186

197
// In v3 the important modifier `!` sits in front of the utility itself, not
208
// before any of the variants. In v4, we want it to be at the end of the utility
@@ -46,56 +34,8 @@ export function important(
4634
// with v3 in that it can read `!` in the front of the utility too, we err
4735
// on the side of caution and only migrate candidates that we are certain
4836
// are inside of a string.
49-
if (location) {
50-
let currentLineBeforeCandidate = ''
51-
for (let i = location.start - 1; i >= 0; i--) {
52-
let char = location.contents.at(i)!
53-
if (char === '\n') {
54-
break
55-
}
56-
currentLineBeforeCandidate = char + currentLineBeforeCandidate
57-
}
58-
let currentLineAfterCandidate = ''
59-
for (let i = location.end; i < location.contents.length; i++) {
60-
let char = location.contents.at(i)!
61-
if (char === '\n') {
62-
break
63-
}
64-
currentLineAfterCandidate += char
65-
}
66-
67-
// Heuristic 1: Require the candidate to be inside quotes
68-
let isQuoteBeforeCandidate = QUOTES.some((quote) =>
69-
currentLineBeforeCandidate.includes(quote),
70-
)
71-
let isQuoteAfterCandidate = QUOTES.some((quote) =>
72-
currentLineAfterCandidate.includes(quote),
73-
)
74-
if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
75-
continue nextCandidate
76-
}
77-
78-
// Heuristic 2: Disallow object access immediately following the candidate
79-
if (currentLineAfterCandidate[0] === '.') {
80-
continue nextCandidate
81-
}
82-
83-
// Heuristic 3: Disallow logical operators preceding or following the candidate
84-
for (let operator of LOGICAL_OPERATORS) {
85-
if (
86-
currentLineAfterCandidate.trim().startsWith(operator) ||
87-
currentLineBeforeCandidate.trim().endsWith(operator)
88-
) {
89-
continue nextCandidate
90-
}
91-
}
92-
93-
// Heuristic 4: Disallow conditional template syntax
94-
for (let rule of CONDITIONAL_TEMPLATE_SYNTAX) {
95-
if (rule.test(currentLineBeforeCandidate)) {
96-
continue nextCandidate
97-
}
98-
}
37+
if (location && !isSafeMigration(location)) {
38+
continue nextCandidate
9939
}
10040

10141
// The printCandidate function will already put the exclamation mark in
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
2+
import { expect, test } from 'vitest'
3+
import { legacyClasses } from './legacy-classes'
4+
5+
test.each([
6+
['shadow', 'shadow-sm'],
7+
['shadow-sm', 'shadow-xs'],
8+
['shadow-xs', 'shadow-2xs'],
9+
10+
['inset-shadow', 'inset-shadow-sm'],
11+
['inset-shadow-sm', 'inset-shadow-xs'],
12+
['inset-shadow-xs', 'inset-shadow-2xs'],
13+
14+
['drop-shadow', 'drop-shadow-sm'],
15+
['drop-shadow-sm', 'drop-shadow-xs'],
16+
17+
['rounded', 'rounded-sm'],
18+
['rounded-sm', 'rounded-xs'],
19+
20+
['blur', 'blur-sm'],
21+
['blur-sm', 'blur-xs'],
22+
23+
['blur!', 'blur-sm!'],
24+
['hover:blur', 'hover:blur-sm'],
25+
['hover:blur!', 'hover:blur-sm!'],
26+
27+
['hover:blur-sm', 'hover:blur-xs'],
28+
['blur-sm!', 'blur-xs!'],
29+
['hover:blur-sm!', 'hover:blur-xs!'],
30+
])('%s => %s', async (candidate, result) => {
31+
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
32+
base: __dirname,
33+
})
34+
35+
expect(await legacyClasses(designSystem, {}, candidate)).toEqual(result)
36+
})

0 commit comments

Comments
 (0)