diff --git a/CHANGELOG.md b/CHANGELOG.md index 96844410fd78..0b1d8ba5829f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - _Upgrade (experimental)_: Do not migrate legacy classes with custom values ([#14976](https://github.com/tailwindlabs/tailwindcss/pull/14976)) +- _Upgrade (experimental)_: Ensure it's safe to migrate `blur`, `rounded`, or `shadow` ([#14979](https://github.com/tailwindlabs/tailwindcss/pull/14979)) ## [4.0.0-alpha.33] - 2024-11-11 diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index dcaf96e93a92..bc1450b5240c 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -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', @@ -1747,3 +1747,95 @@ test( `) }, ) + +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` +
+ `, + '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 + } + `, + }, + }, + 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 --- + + + --- 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 + } + " + `) + }, +) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/important.ts b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts index f9a8e2723c76..7955e8412945 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/important.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts @@ -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 @@ -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 diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/legacy-classes.ts b/packages/@tailwindcss-upgrade/src/template/codemods/legacy-classes.ts index 39c9fa47c842..e697b1125dfb 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/legacy-classes.ts @@ -5,6 +5,7 @@ import type { Config } from 'tailwindcss' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' import { printCandidate } from '../candidates' +import { isSafeMigration } from '../is-safe-migration' const __filename = url.fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -56,6 +57,11 @@ export async function legacyClasses( designSystem: DesignSystem, _userConfig: Config, rawCandidate: string, + location?: { + contents: string + start: number + end: number + }, ): Promise