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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- 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))
- _Upgrade (experimental)_: Ensure it's safe to migrate `blur`, `rounded`, or `shadow` ([#14979](https://github.com/tailwindlabs/tailwindcss/pull/14979))
- _Upgrade (experimental)_: Do not rename classes using custom defined theme values ([#14976](https://github.com/tailwindlabs/tailwindcss/pull/14976))

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

Expand Down
199 changes: 198 additions & 1 deletion integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'vitest'
import { candidate, css, html, js, json, test } from '../utils'
import { candidate, css, html, js, json, test, ts } from '../utils'

test(
'error when no CSS file with @tailwind is used',
Expand Down Expand Up @@ -1642,3 +1642,200 @@ test(
expect(pkg.devDependencies['prettier-plugin-tailwindcss']).not.toEqual('0.5.0')
},
)

test(
'only migrate legacy classes when it is safe to do so',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3.4.14",
"@tailwindcss/upgrade": "workspace:^"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "0.5.0"
}
}
`,
'tailwind.config.js': js`
module.exports = {
content: ['./*.html'],
theme: {
// Overrides the default boxShadow entirely so none of the
// migrations are safe.
boxShadow: {
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
},

extend: {
// Changes the "before" class definition. 'blur' -> 'blur-sm' is
// not safe because 'blur' has a custom value.
//
// But 'blur-sm' -> 'blur-xs' is safe because 'blur-xs' uses the
// default value.
blur: {
DEFAULT: 'var(--custom-default-blur)',
},

// Changes the "after" class definition. 'rounded' -> 'rounded-sm' is
// not safe because 'rounded-sm' has a custom value.
borderRadius: {
sm: 'var(--custom-rounded-sm)',
},
},
},
}
`,
'index.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'index.html': html`
<div>
<div class="shadow shadow-sm shadow-xs"></div>
<div class="blur blur-sm"></div>
<div class="rounded rounded-sm"></div>
</div>
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade --force')

// Files should not be modified
expect(await fs.dumpFiles('./*.{js,css,html}')).toMatchInlineSnapshot(`
"
--- index.html ---
<div>
<div class="shadow shadow-sm shadow-xs"></div>
<div class="blur blur-xs"></div>
<div class="rounded rounded-sm"></div>
</div>

--- index.css ---
@import 'tailwindcss';

@theme {
--shadow-*: initial;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);

--blur: var(--custom-default-blur);

--radius-sm: var(--custom-rounded-sm);
}

/*
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);
}
}
"
`)
},
)

test(
'make suffix-less migrations safe (e.g.: `blur`, `rounded`, `shadow`)',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3.4.14",
"@tailwindcss/upgrade": "workspace:^"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "0.5.0"
}
}
`,
'tailwind.config.js': js`
module.exports = {
content: ['./*.{html,tsx}'],
}
`,
'index.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'index.html': html`
<div class="rounded blur shadow"></div>
`,
'example-component.tsx': ts`
type Star = [
x: number,
y: number,
dim?: boolean,
blur?: boolean,
rounded?: boolean,
shadow?: boolean,
]

function Star({ point: [cx, cy, dim, blur, rounded, shadow] }: { point: Star }) {
return <svg class="rounded shadow blur" filter={blur ? 'url(…)' : undefined} />
}
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade --force')

// Files should not be modified
expect(await fs.dumpFiles('./*.{js,css,html,tsx}')).toMatchInlineSnapshot(`
"
--- index.html ---
<div class="rounded-sm blur-sm shadow-sm"></div>

--- index.css ---
@import 'tailwindcss';

/*
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);
}
}

--- example-component.tsx ---
type Star = [
x: number,
y: number,
dim?: boolean,
blur?: boolean,
rounded?: boolean,
shadow?: boolean,
]

function Star({ point: [cx, cy, dim, blur, rounded, shadow] }: { point: Star }) {
return <svg class="rounded-sm shadow-sm blur-sm" filter={blur ? 'url(…)' : undefined} />
}
"
`)
},
)
14 changes: 14 additions & 0 deletions integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,20 @@ export function test(

options.onTestFinished(dispose)

// Make it a git repository, and commit all files
if (only || debug) {
try {
await context.exec('git init', { cwd: root })
await context.exec('git add --all', { cwd: root })
await context.exec('git commit -m "before migration"', { cwd: root })
} catch (error: any) {
console.error(error)
console.error(error.stdout?.toString())
console.error(error.stderr?.toString())
throw error
}
}

return await testCallback(context)
},
)
Expand Down
23 changes: 16 additions & 7 deletions packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,26 @@ export function migrateAtApply({
return [...variants, utility].join(':')
})

// If we have a valid designSystem and config setup, we can run all
// candidate migrations on each utility
params = params.map((param) => migrateCandidate(designSystem, userConfig, param))

atRule.params = params.join('').trim()
return async () => {
// If we have a valid designSystem and config setup, we can run all
// candidate migrations on each utility
params = await Promise.all(
params.map(async (param) => await migrateCandidate(designSystem, userConfig, param)),
)

atRule.params = params.join('').trim()
}
}

return {
postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply',
OnceExit(root) {
root.walkAtRules('apply', migrate)
async OnceExit(root) {
let migrations: (() => void)[] = []
root.walkAtRules('apply', (atRule) => {
migrations.push(migrate(atRule))
})

await Promise.allSettled(migrations.map((m) => m()))
},
}
}
66 changes: 3 additions & 63 deletions packages/@tailwindcss-upgrade/src/template/codemods/important.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,7 @@ import type { Config } from 'tailwindcss'
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { printCandidate } from '../candidates'

const QUOTES = ['"', "'", '`']
const LOGICAL_OPERATORS = ['&&', '||', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
const CONDITIONAL_TEMPLATE_SYNTAX = [
// Vue
/v-else-if=['"]$/,
/v-if=['"]$/,
/v-show=['"]$/,

// Alpine
/x-if=['"]$/,
/x-show=['"]$/,
]
import { isSafeMigration } from '../is-safe-migration'

// In v3 the important modifier `!` sits in front of the utility itself, not
// before any of the variants. In v4, we want it to be at the end of the utility
Expand Down Expand Up @@ -46,56 +34,8 @@ export function important(
// with v3 in that it can read `!` in the front of the utility too, we err
// on the side of caution and only migrate candidates that we are certain
// are inside of a string.
if (location) {
let currentLineBeforeCandidate = ''
for (let i = location.start - 1; i >= 0; i--) {
let char = location.contents.at(i)!
if (char === '\n') {
break
}
currentLineBeforeCandidate = char + currentLineBeforeCandidate
}
let currentLineAfterCandidate = ''
for (let i = location.end; i < location.contents.length; i++) {
let char = location.contents.at(i)!
if (char === '\n') {
break
}
currentLineAfterCandidate += char
}

// Heuristic 1: Require the candidate to be inside quotes
let isQuoteBeforeCandidate = QUOTES.some((quote) =>
currentLineBeforeCandidate.includes(quote),
)
let isQuoteAfterCandidate = QUOTES.some((quote) =>
currentLineAfterCandidate.includes(quote),
)
if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
continue nextCandidate
}

// Heuristic 2: Disallow object access immediately following the candidate
if (currentLineAfterCandidate[0] === '.') {
continue nextCandidate
}

// Heuristic 3: Disallow logical operators preceding or following the candidate
for (let operator of LOGICAL_OPERATORS) {
if (
currentLineAfterCandidate.trim().startsWith(operator) ||
currentLineBeforeCandidate.trim().endsWith(operator)
) {
continue nextCandidate
}
}

// Heuristic 4: Disallow conditional template syntax
for (let rule of CONDITIONAL_TEMPLATE_SYNTAX) {
if (rule.test(currentLineBeforeCandidate)) {
continue nextCandidate
}
}
if (location && !isSafeMigration(location)) {
continue nextCandidate
}

// The printCandidate function will already put the exclamation mark in
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { expect, test } from 'vitest'
import { legacyClasses } from './legacy-classes'

test.each([
['shadow', 'shadow-sm'],
['shadow-sm', 'shadow-xs'],
['shadow-xs', 'shadow-2xs'],

['inset-shadow', 'inset-shadow-sm'],
['inset-shadow-sm', 'inset-shadow-xs'],
['inset-shadow-xs', 'inset-shadow-2xs'],

['drop-shadow', 'drop-shadow-sm'],
['drop-shadow-sm', 'drop-shadow-xs'],

['rounded', 'rounded-sm'],
['rounded-sm', 'rounded-xs'],

['blur', 'blur-sm'],
['blur-sm', 'blur-xs'],

['blur!', 'blur-sm!'],
['hover:blur', 'hover:blur-sm'],
['hover:blur!', 'hover:blur-sm!'],

['hover:blur-sm', 'hover:blur-xs'],
['blur-sm!', 'blur-xs!'],
['hover:blur-sm!', 'hover:blur-xs!'],
])('%s => %s', async (candidate, result) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})

expect(await legacyClasses(designSystem, {}, candidate)).toEqual(result)
})
Loading