Skip to content

Commit 95c4877

Browse files
Upgrade: Migrate spacing scale (#14905)
This PR adds migrations for the recent changes to the `--spacing` scale done in #12263. There are a few steps that we do to ensure we have the best upgrade experience: - If you are overwriting the `spacing` theme with custom values, we now check if the new values are multiplies of the default spacing scale. When they are, we can safely remove the overwrite. - If you are extending the `spacing` theme, we will unset the default `--spacing` scale and only use the values you provided. - Any `theme()` function calls are replaced with `calc(var(--spacing) * multiplier)` unless the values are extending the default scale. One caveat here is for `theme()` key which can not be replaced with `var()` (e.g. in `@media` attribute positions). These will not be able to be replaced with `calc()` either so the following needs to stay unmigrated: ```css @media (max-width: theme(spacing.96)) { .foo { color: red; } } ``` ## Test plan We are mainly testing two scenarios: The JS config _extends_ the `spacing` namespace and the JS config _overwrites_ the `spacing` namespace. For both cases we have added an integration test each to ensure this works as expected. The test contains a mixture of keys (some of it matching the default multiples, some don't, some have different scales, and some use non-numeric identifiers). In addition to asserting on the created CSS `@theme`, we also ensure that `theme()` calls are properly replaced. --------- Co-authored-by: Adam Wathan <[email protected]> Co-authored-by: Adam Wathan <[email protected]>
1 parent 28e46ba commit 95c4877

File tree

7 files changed

+385
-47
lines changed

7 files changed

+385
-47
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- _Upgrade (experimental)_: Rename `drop-shadow` to `drop-shadow-sm` and `drop-shadow-sm` to `drop-shadow-xs` ([#14875](https://github.com/tailwindlabs/tailwindcss/pull/14875))
2020
- _Upgrade (experimental)_: Rename `rounded` to `rounded-sm` and `rounded-sm` to `rounded-xs` ([#14875](https://github.com/tailwindlabs/tailwindcss/pull/14875))
2121
- _Upgrade (experimental)_: Rename `blur` to `blur-sm` and `blur-sm` to `blur-xs` ([#14875](https://github.com/tailwindlabs/tailwindcss/pull/14875))
22+
- _Upgrade (experimental)_: Migrate `theme()` usage and JS config files to use the new `--spacing` multiplier where possible ([#14905](https://github.com/tailwindlabs/tailwindcss/pull/14905))
2223

2324
### Fixed
2425

integrations/upgrade/js-config.test.ts

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,4 +1415,260 @@ describe('border compatibility', () => {
14151415
`)
14161416
},
14171417
)
1418+
1419+
test(
1420+
'migrates extended spacing keys',
1421+
{
1422+
fs: {
1423+
'package.json': json`
1424+
{
1425+
"dependencies": {
1426+
"@tailwindcss/upgrade": "workspace:^"
1427+
}
1428+
}
1429+
`,
1430+
'tailwind.config.ts': ts`
1431+
import { type Config } from 'tailwindcss'
1432+
1433+
export default {
1434+
content: ['./src/**/*.html'],
1435+
theme: {
1436+
extend: {
1437+
spacing: {
1438+
2: '0.5rem',
1439+
4.5: '1.125rem',
1440+
5.5: '1.375em', // Units are different from --spacing scale
1441+
13: '3.25rem',
1442+
100: '100px',
1443+
miami: '1337px',
1444+
},
1445+
},
1446+
},
1447+
} satisfies Config
1448+
`,
1449+
'src/input.css': css`
1450+
@tailwind base;
1451+
@tailwind components;
1452+
@tailwind utilities;
1453+
1454+
.container {
1455+
width: theme(spacing.2);
1456+
width: theme(spacing[4.5]);
1457+
width: theme(spacing[5.5]);
1458+
width: theme(spacing[13]);
1459+
width: theme(spacing[100]);
1460+
width: theme(spacing.miami);
1461+
}
1462+
`,
1463+
'src/index.html': html`
1464+
<div
1465+
class="[width:theme(spacing.2)]
1466+
[width:theme(spacing[4.5])]
1467+
[width:theme(spacing[5.5])]
1468+
[width:theme(spacing[13])]
1469+
[width:theme(spacing[100])]
1470+
[width:theme(spacing.miami)]"
1471+
></div>
1472+
`,
1473+
},
1474+
},
1475+
async ({ exec, fs }) => {
1476+
await exec('npx @tailwindcss/upgrade')
1477+
1478+
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
1479+
"
1480+
--- src/index.html ---
1481+
<div
1482+
class="[width:calc(var(--spacing)*2)]
1483+
[width:calc(var(--spacing)*4.5)]
1484+
[width:var(--spacing-5_5)]
1485+
[width:calc(var(--spacing)*13)]
1486+
[width:var(--spacing-100)]
1487+
[width:var(--spacing-miami)]"
1488+
></div>
1489+
1490+
--- src/input.css ---
1491+
@import 'tailwindcss';
1492+
1493+
@theme {
1494+
--spacing-100: 100px;
1495+
--spacing-5_5: 1.375em;
1496+
--spacing-miami: 1337px;
1497+
}
1498+
1499+
/*
1500+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
1501+
so we've added these compatibility styles to make sure everything still
1502+
looks the same as it did with Tailwind CSS v3.
1503+
1504+
If we ever want to remove these styles, we need to add an explicit border
1505+
color utility to any element that depends on these defaults.
1506+
*/
1507+
@layer base {
1508+
*,
1509+
::after,
1510+
::before,
1511+
::backdrop,
1512+
::file-selector-button {
1513+
border-color: var(--color-gray-200, currentColor);
1514+
}
1515+
}
1516+
1517+
/*
1518+
Form elements have a 1px border by default in Tailwind CSS v4, so we've
1519+
added these compatibility styles to make sure everything still looks the
1520+
same as it did with Tailwind CSS v3.
1521+
1522+
If we ever want to remove these styles, we need to add \`border-0\` to
1523+
any form elements that shouldn't have a border.
1524+
*/
1525+
@layer base {
1526+
input:where(:not([type='button'], [type='reset'], [type='submit'])),
1527+
select,
1528+
textarea {
1529+
border-width: 0;
1530+
}
1531+
}
1532+
1533+
.container {
1534+
width: calc(var(--spacing) * 2);
1535+
width: calc(var(--spacing) * 4.5);
1536+
width: var(--spacing-5_5);
1537+
width: calc(var(--spacing) * 13);
1538+
width: var(--spacing-100);
1539+
width: var(--spacing-miami);
1540+
}
1541+
"
1542+
`)
1543+
},
1544+
)
1545+
1546+
test(
1547+
'retains overwriting spacing scale',
1548+
{
1549+
fs: {
1550+
'package.json': json`
1551+
{
1552+
"dependencies": {
1553+
"@tailwindcss/upgrade": "workspace:^"
1554+
}
1555+
}
1556+
`,
1557+
'tailwind.config.ts': ts`
1558+
import { type Config } from 'tailwindcss'
1559+
1560+
export default {
1561+
content: ['./src/**/*.html'],
1562+
theme: {
1563+
spacing: {
1564+
2: '0.5rem',
1565+
4.5: '1.125rem',
1566+
5.5: '1.375em',
1567+
13: '3.25rem',
1568+
100: '100px',
1569+
miami: '1337px',
1570+
},
1571+
},
1572+
} satisfies Config
1573+
`,
1574+
'src/input.css': css`
1575+
@tailwind base;
1576+
@tailwind components;
1577+
@tailwind utilities;
1578+
1579+
.container {
1580+
width: theme(spacing.2);
1581+
width: theme(spacing[4.5]);
1582+
width: theme(spacing[5.5]);
1583+
width: theme(spacing[13]);
1584+
width: theme(spacing[100]);
1585+
width: theme(spacing.miami);
1586+
}
1587+
`,
1588+
'src/index.html': html`
1589+
<div
1590+
class="[width:theme(spacing.2)]
1591+
[width:theme(spacing[4.5])]
1592+
[width:theme(spacing[5.5])]
1593+
[width:theme(spacing[13])]
1594+
[width:theme(spacing[100])]
1595+
[width:theme(spacing.miami)]"
1596+
></div>
1597+
`,
1598+
},
1599+
},
1600+
async ({ exec, fs }) => {
1601+
await exec('npx @tailwindcss/upgrade')
1602+
1603+
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
1604+
"
1605+
--- src/index.html ---
1606+
<div
1607+
class="[width:var(--spacing-2)]
1608+
[width:var(--spacing-4_5)]
1609+
[width:var(--spacing-5_5)]
1610+
[width:var(--spacing-13)]
1611+
[width:var(--spacing-100)]
1612+
[width:var(--spacing-miami)]"
1613+
></div>
1614+
1615+
--- src/input.css ---
1616+
@import 'tailwindcss';
1617+
1618+
@theme {
1619+
--spacing-*: initial;
1620+
--spacing-2: 0.5rem;
1621+
--spacing-13: 3.25rem;
1622+
--spacing-100: 100px;
1623+
--spacing-4_5: 1.125rem;
1624+
--spacing-5_5: 1.375em;
1625+
--spacing-miami: 1337px;
1626+
}
1627+
1628+
/*
1629+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
1630+
so we've added these compatibility styles to make sure everything still
1631+
looks the same as it did with Tailwind CSS v3.
1632+
1633+
If we ever want to remove these styles, we need to add an explicit border
1634+
color utility to any element that depends on these defaults.
1635+
*/
1636+
@layer base {
1637+
*,
1638+
::after,
1639+
::before,
1640+
::backdrop,
1641+
::file-selector-button {
1642+
border-color: var(--color-gray-200, currentColor);
1643+
}
1644+
}
1645+
1646+
/*
1647+
Form elements have a 1px border by default in Tailwind CSS v4, so we've
1648+
added these compatibility styles to make sure everything still looks the
1649+
same as it did with Tailwind CSS v3.
1650+
1651+
If we ever want to remove these styles, we need to add \`border-0\` to
1652+
any form elements that shouldn't have a border.
1653+
*/
1654+
@layer base {
1655+
input:where(:not([type='button'], [type='reset'], [type='submit'])),
1656+
select,
1657+
textarea {
1658+
border-width: 0;
1659+
}
1660+
}
1661+
1662+
.container {
1663+
width: var(--spacing-2);
1664+
width: var(--spacing-4_5);
1665+
width: var(--spacing-5_5);
1666+
width: var(--spacing-13);
1667+
width: var(--spacing-100);
1668+
width: var(--spacing-miami);
1669+
}
1670+
"
1671+
`)
1672+
},
1673+
)
14181674
})

packages/@tailwindcss-upgrade/src/migrate-js-config.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import {
1212
} from '../../tailwindcss/src/compat/apply-config-to-theme'
1313
import { keyframesToRules } from '../../tailwindcss/src/compat/apply-keyframes-to-theme'
1414
import { resolveConfig, type ConfigFile } from '../../tailwindcss/src/compat/config/resolve-config'
15-
import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types'
15+
import type { ResolvedConfig, ThemeConfig } from '../../tailwindcss/src/compat/config/types'
1616
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
1717
import type { DesignSystem } from '../../tailwindcss/src/design-system'
1818
import { escape } from '../../tailwindcss/src/utils/escape'
19+
import { isValidSpacingMultiplier } from '../../tailwindcss/src/utils/infer-data-type'
1920
import { findStaticPlugins, type StaticPluginOptions } from './utils/extract-static-plugins'
2021
import { info } from './utils/renderer'
2122

@@ -101,6 +102,8 @@ async function migrateTheme(
101102
Array.from(replacedThemeKeys.entries()).map(([key]) => [key, false]),
102103
)
103104

105+
removeUnnecessarySpacingKeys(designSystem, resolvedConfig, replacedThemeKeys)
106+
104107
let prevSectionKey = ''
105108
let css = '\n@tw-bucket theme {\n'
106109
css += `\n@theme {\n`
@@ -317,3 +320,42 @@ function patternSourceFiles(source: { base: string; pattern: string }): string[]
317320
scanner.scan()
318321
return scanner.files
319322
}
323+
324+
function removeUnnecessarySpacingKeys(
325+
designSystem: DesignSystem,
326+
resolvedConfig: ResolvedConfig,
327+
replacedThemeKeys: Set<string>,
328+
) {
329+
// We want to keep the spacing scale as-is if the user is overwriting
330+
if (replacedThemeKeys.has('spacing')) return
331+
332+
// Ensure we have a spacing multiplier
333+
let spacingScale = designSystem.theme.get(['--spacing'])
334+
if (!spacingScale) return
335+
336+
let [spacingMultiplier, spacingUnit] = splitNumberAndUnit(spacingScale)
337+
if (!spacingMultiplier || !spacingUnit) return
338+
339+
if (spacingScale && !replacedThemeKeys.has('spacing')) {
340+
for (let [key, value] of Object.entries(resolvedConfig.theme.spacing ?? {})) {
341+
let [multiplier, unit] = splitNumberAndUnit(value as string)
342+
if (multiplier === null) continue
343+
344+
if (!isValidSpacingMultiplier(key)) continue
345+
if (unit !== spacingUnit) continue
346+
347+
if (parseFloat(multiplier) === Number(key) * parseFloat(spacingMultiplier)) {
348+
delete resolvedConfig.theme.spacing[key]
349+
designSystem.theme.clearNamespace(escape(`--spacing-${key.replaceAll('.', '_')}`), 0)
350+
}
351+
}
352+
}
353+
}
354+
355+
function splitNumberAndUnit(value: string): [string, string] | [null, null] {
356+
let match = value.match(/^([0-9.]+)(.*)$/)
357+
if (!match) {
358+
return [null, null]
359+
}
360+
return [match[1], match[2]]
361+
}

0 commit comments

Comments
 (0)