Skip to content

Commit 5a8e878

Browse files
Handle future and experimental config keys during upgrade (#19344)
Fixes #19342
1 parent 68337df commit 5a8e878

File tree

5 files changed

+276
-0
lines changed

5 files changed

+276
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
1515
- Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
1616
- Support environment API in `@tailwindcss/vite` ([#18970](https://github.com/tailwindlabs/tailwindcss/pull/18970))
17+
- Upgrade: Handle `future` and `experimental` config keys ([#19344](https://github.com/tailwindlabs/tailwindcss/pull/19344))
1718

1819
### Added
1920

integrations/upgrade/js-config.test.ts

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1841,3 +1841,243 @@ describe('border compatibility', () => {
18411841
},
18421842
)
18431843
})
1844+
1845+
test(
1846+
`future and experimental keys are supported`,
1847+
{
1848+
fs: {
1849+
'package.json': json`
1850+
{
1851+
"dependencies": {
1852+
"tailwindcss": "^3",
1853+
"@tailwindcss/upgrade": "workspace:^"
1854+
}
1855+
}
1856+
`,
1857+
'tailwind.config.ts': ts`
1858+
import { type Config } from 'tailwindcss'
1859+
import defaultTheme from 'tailwindcss/defaultTheme'
1860+
1861+
module.exports = {
1862+
darkMode: 'selector',
1863+
content: ['./src/**/*.{html,js}'],
1864+
future: {
1865+
hoverOnlyWhenSupported: true,
1866+
respectDefaultRingColorOpacity: true,
1867+
disableColorOpacityUtilitiesByDefault: true,
1868+
relativeContentPathsByDefault: true,
1869+
},
1870+
experimental: {
1871+
generalizedModifiers: true,
1872+
},
1873+
theme: {
1874+
colors: {
1875+
red: {
1876+
400: '#f87171',
1877+
500: 'red',
1878+
},
1879+
},
1880+
},
1881+
plugins: [],
1882+
} satisfies Config
1883+
`,
1884+
'src/input.css': css`
1885+
@tailwind base;
1886+
@tailwind components;
1887+
@tailwind utilities;
1888+
`,
1889+
},
1890+
},
1891+
async ({ exec, fs, expect }) => {
1892+
await exec('npx @tailwindcss/upgrade')
1893+
1894+
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
1895+
"
1896+
--- src/input.css ---
1897+
@import 'tailwindcss';
1898+
1899+
@custom-variant dark (&:where(.dark, .dark *));
1900+
1901+
@theme {
1902+
--color-*: initial;
1903+
--color-red-400: #f87171;
1904+
--color-red-500: red;
1905+
}
1906+
1907+
/*
1908+
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
1909+
so we've added these compatibility styles to make sure everything still
1910+
looks the same as it did with Tailwind CSS v3.
1911+
1912+
If we ever want to remove these styles, we need to add an explicit border
1913+
color utility to any element that depends on these defaults.
1914+
*/
1915+
@layer base {
1916+
*,
1917+
::after,
1918+
::before,
1919+
::backdrop,
1920+
::file-selector-button {
1921+
border-color: var(--color-gray-200, currentcolor);
1922+
}
1923+
}
1924+
"
1925+
`)
1926+
1927+
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('')
1928+
},
1929+
)
1930+
1931+
test(
1932+
`unknown future keys dont migrate the config`,
1933+
{
1934+
fs: {
1935+
'package.json': json`
1936+
{
1937+
"dependencies": {
1938+
"tailwindcss": "^3",
1939+
"@tailwindcss/upgrade": "workspace:^"
1940+
}
1941+
}
1942+
`,
1943+
'tailwind.config.ts': ts`
1944+
import { type Config } from 'tailwindcss'
1945+
import defaultTheme from 'tailwindcss/defaultTheme'
1946+
1947+
module.exports = {
1948+
darkMode: 'selector',
1949+
content: ['./src/**/*.{html,js}'],
1950+
future: {
1951+
something: true,
1952+
},
1953+
} satisfies Config
1954+
`,
1955+
'src/input.css': css`
1956+
@tailwind base;
1957+
@tailwind components;
1958+
@tailwind utilities;
1959+
`,
1960+
},
1961+
},
1962+
async ({ exec, fs, expect }) => {
1963+
await exec('npx @tailwindcss/upgrade')
1964+
1965+
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
1966+
"
1967+
--- src/input.css ---
1968+
@import 'tailwindcss';
1969+
1970+
@config '../tailwind.config.ts';
1971+
1972+
/*
1973+
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
1974+
so we've added these compatibility styles to make sure everything still
1975+
looks the same as it did with Tailwind CSS v3.
1976+
1977+
If we ever want to remove these styles, we need to add an explicit border
1978+
color utility to any element that depends on these defaults.
1979+
*/
1980+
@layer base {
1981+
*,
1982+
::after,
1983+
::before,
1984+
::backdrop,
1985+
::file-selector-button {
1986+
border-color: var(--color-gray-200, currentcolor);
1987+
}
1988+
}
1989+
"
1990+
`)
1991+
1992+
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(`
1993+
"--- tailwind.config.ts ---
1994+
import { type Config } from 'tailwindcss'
1995+
import defaultTheme from 'tailwindcss/defaultTheme'
1996+
1997+
module.exports = {
1998+
darkMode: 'selector',
1999+
content: ['./src/**/*.{html,js}'],
2000+
future: {
2001+
something: true,
2002+
},
2003+
} satisfies Config"
2004+
`)
2005+
},
2006+
)
2007+
2008+
test(
2009+
`unknown experimental keys dont migrate the config`,
2010+
{
2011+
fs: {
2012+
'package.json': json`
2013+
{
2014+
"dependencies": {
2015+
"tailwindcss": "^3",
2016+
"@tailwindcss/upgrade": "workspace:^"
2017+
}
2018+
}
2019+
`,
2020+
'tailwind.config.ts': ts`
2021+
import { type Config } from 'tailwindcss'
2022+
import defaultTheme from 'tailwindcss/defaultTheme'
2023+
2024+
module.exports = {
2025+
darkMode: 'selector',
2026+
content: ['./src/**/*.{html,js}'],
2027+
experimental: {
2028+
something: true,
2029+
},
2030+
} satisfies Config
2031+
`,
2032+
'src/input.css': css`
2033+
@tailwind base;
2034+
@tailwind components;
2035+
@tailwind utilities;
2036+
`,
2037+
},
2038+
},
2039+
async ({ exec, fs, expect }) => {
2040+
await exec('npx @tailwindcss/upgrade')
2041+
2042+
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
2043+
"
2044+
--- src/input.css ---
2045+
@import 'tailwindcss';
2046+
2047+
@config '../tailwind.config.ts';
2048+
2049+
/*
2050+
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
2051+
so we've added these compatibility styles to make sure everything still
2052+
looks the same as it did with Tailwind CSS v3.
2053+
2054+
If we ever want to remove these styles, we need to add an explicit border
2055+
color utility to any element that depends on these defaults.
2056+
*/
2057+
@layer base {
2058+
*,
2059+
::after,
2060+
::before,
2061+
::backdrop,
2062+
::file-selector-button {
2063+
border-color: var(--color-gray-200, currentcolor);
2064+
}
2065+
}
2066+
"
2067+
`)
2068+
2069+
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(`
2070+
"--- tailwind.config.ts ---
2071+
import { type Config } from 'tailwindcss'
2072+
import defaultTheme from 'tailwindcss/defaultTheme'
2073+
2074+
module.exports = {
2075+
darkMode: 'selector',
2076+
content: ['./src/**/*.{html,js}'],
2077+
experimental: {
2078+
something: true,
2079+
},
2080+
} satisfies Config"
2081+
`)
2082+
},
2083+
)

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,8 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
411411
'presets',
412412
'prefix', // Prefix is handled in the dedicated prefix migrator
413413
'corePlugins',
414+
'future',
415+
'experimental',
414416
]
415417

416418
if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) {
@@ -425,6 +427,29 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
425427
return false
426428
}
427429

430+
// If there are unknown "future" flags we should bail
431+
if (unresolvedConfig.future && unresolvedConfig.future !== 'all') {
432+
let knownFlags = [
433+
'hoverOnlyWhenSupported',
434+
'respectDefaultRingColorOpacity',
435+
'disableColorOpacityUtilitiesByDefault',
436+
'relativeContentPathsByDefault',
437+
]
438+
439+
if (Object.keys(unresolvedConfig.future).some((key) => !knownFlags.includes(key))) {
440+
return false
441+
}
442+
}
443+
444+
// If there are unknown "experimental" flags we should bail
445+
if (unresolvedConfig.experimental && unresolvedConfig.experimental !== 'all') {
446+
let knownFlags = ['generalizedModifiers']
447+
448+
if (Object.keys(unresolvedConfig.experimental).some((key) => !knownFlags.includes(key))) {
449+
return false
450+
}
451+
}
452+
428453
// Only migrate the config file if all top-level theme keys are allowed to be
429454
// migrated
430455
if (theme && typeof theme === 'object') {

packages/tailwindcss/src/compat/config/resolve-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface ResolutionContext {
3333
let minimal: ResolvedConfig = {
3434
blocklist: [],
3535
future: {},
36+
experimental: {},
3637
prefix: '',
3738
important: false,
3839
darkMode: null,

packages/tailwindcss/src/compat/config/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,12 @@ export interface UserConfig {
105105
export interface ResolvedConfig {
106106
future: Record<string, boolean>
107107
}
108+
109+
// `experimental` key support
110+
export interface UserConfig {
111+
experimental?: 'all' | Record<string, boolean>
112+
}
113+
114+
export interface ResolvedConfig {
115+
experimental: Record<string, boolean>
116+
}

0 commit comments

Comments
 (0)