Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
- Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
- Support environment API in `@tailwindcss/vite` ([#18970](https://github.com/tailwindlabs/tailwindcss/pull/18970))
- Upgrade: Handle `future` and `experimental` config keys ([#19344](https://github.com/tailwindlabs/tailwindcss/pull/19344))

### Added

Expand Down
240 changes: 240 additions & 0 deletions integrations/upgrade/js-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1841,3 +1841,243 @@ describe('border compatibility', () => {
},
)
})

test(
`future and experimental keys are supported`,
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'

module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
future: {
hoverOnlyWhenSupported: true,
respectDefaultRingColorOpacity: true,
disableColorOpacityUtilitiesByDefault: true,
relativeContentPathsByDefault: true,
},
experimental: {
generalizedModifiers: true,
},
theme: {
colors: {
red: {
400: '#f87171',
500: 'red',
},
},
},
plugins: [],
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';

@custom-variant dark (&:where(.dark, .dark *));

@theme {
--color-*: initial;
--color-red-400: #f87171;
--color-red-500: red;
}

/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.

If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)

expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('')
},
)

test(
`unknown future keys dont migrate the config`,
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'

module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
future: {
something: true,
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';

@config '../tailwind.config.ts';

/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.

If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)

expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(`
"--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'

module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
future: {
something: true,
},
} satisfies Config"
`)
},
)

test(
`unknown experimental keys dont migrate the config`,
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'

module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
experimental: {
something: true,
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';

@config '../tailwind.config.ts';

/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.

If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)

expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(`
"--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'

module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
experimental: {
something: true,
},
} satisfies Config"
`)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,8 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
'presets',
'prefix', // Prefix is handled in the dedicated prefix migrator
'corePlugins',
'future',
'experimental',
]

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

// If there are unknown "future" flags we should bail
if (unresolvedConfig.future && unresolvedConfig.future !== 'all') {
let knownFlags = [
'hoverOnlyWhenSupported',
'respectDefaultRingColorOpacity',
'disableColorOpacityUtilitiesByDefault',
'relativeContentPathsByDefault',
]

if (Object.keys(unresolvedConfig.future).some((key) => !knownFlags.includes(key))) {
return false
}
}

// If there are unknown "experimental" flags we should bail
if (unresolvedConfig.experimental && unresolvedConfig.experimental !== 'all') {
let knownFlags = ['generalizedModifiers']

if (Object.keys(unresolvedConfig.experimental).some((key) => !knownFlags.includes(key))) {
return false
}
}

// Only migrate the config file if all top-level theme keys are allowed to be
// migrated
if (theme && typeof theme === 'object') {
Expand Down
1 change: 1 addition & 0 deletions packages/tailwindcss/src/compat/config/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface ResolutionContext {
let minimal: ResolvedConfig = {
blocklist: [],
future: {},
experimental: {},
prefix: '',
important: false,
darkMode: null,
Expand Down
9 changes: 9 additions & 0 deletions packages/tailwindcss/src/compat/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,12 @@ export interface UserConfig {
export interface ResolvedConfig {
future: Record<string, boolean>
}

// `experimental` key support
export interface UserConfig {
experimental?: 'all' | Record<string, boolean>
}

export interface ResolvedConfig {
experimental: Record<string, boolean>
}