Skip to content

Commit c7d368b

Browse files
authored
Do not migrate declarations in <style> blocks (#18057)
This PR improves the upgrade tool by making sure that we don't migrate CSS declarations in `<style>…</style>` blocks. We do this by making sure that: 1. We detect a declaration, the current heuristic is that the candidate is: - Preceded by whitespace - Followed by a colon and whitespace ```html <style> .foo { flex-shrink: 0; ^ ^^ } </style> ``` 2. We are in a `<style>…</style>` block ```html <style> ^^^^^^ .foo { flex-shrink: 0; } </style> ^^^^^^^^ ``` The reason we have these 2 checks is because just relying on the first heuristic alone, also means that we will not be migrating keys in JS objects, because they typically follow the same structure: ```js let classes = { flex: 0, ^ ^^ } ``` Another important thing to note is that we can't just ignore anything in between `<style>…</style>` blocks, because you could still be using `@apply` that we _do_ want to migrate. Last but not least, the first heuristics is not perfect either. If you are writing minified CSS then this will likely fail if there is no whitespace around the candidate. But my current assumption is that nobody should be writing minified CSS, and minified CSS will very likely be generated and gitignored. In either situation, replacements in minified CSS will not be any worse than it is today. I'm open to suggestions for better heuristics. ## Test plan 1. Added an integration test that verifies that we do migrate `@apply` and don't migrate the `flex-shrink: 0;` declaration. Fixes: #17975
1 parent d69604e commit c7d368b

File tree

3 files changed

+115
-1
lines changed

3 files changed

+115
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Upgrade: Do not migrate declarations that look like candidates in `<style>` blocks ([#18057](https://github.com/tailwindlabs/tailwindcss/pull/18057))
1113

1214
## [4.1.7] - 2025-05-15
1315

integrations/upgrade/index.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2967,6 +2967,87 @@ test(
29672967
},
29682968
)
29692969

2970+
test(
2971+
'upgrade <style> blocks carefully',
2972+
{
2973+
fs: {
2974+
'package.json': json`
2975+
{
2976+
"dependencies": {
2977+
"tailwindcss": "^4",
2978+
"@tailwindcss/upgrade": "workspace:^"
2979+
}
2980+
}
2981+
`,
2982+
'src/index.vue': html`
2983+
<template
2984+
<div class="!flex"></div>
2985+
</template>
2986+
2987+
<style>
2988+
@reference "./input.css";
2989+
2990+
.foo {
2991+
@apply !bg-red-500;
2992+
}
2993+
2994+
.bar {
2995+
/* Do not upgrade the key: */
2996+
flex-shrink: 0;
2997+
}
2998+
</style>
2999+
`,
3000+
'src/input.css': css`
3001+
@import 'tailwindcss';
3002+
3003+
.foo {
3004+
flex-shrink: 1;
3005+
}
3006+
3007+
.bar {
3008+
@apply !underline;
3009+
}
3010+
`,
3011+
},
3012+
},
3013+
async ({ exec, fs, expect }) => {
3014+
await exec('npx @tailwindcss/upgrade')
3015+
3016+
expect(await fs.dumpFiles('./src/**/*.{css,vue}')).toMatchInlineSnapshot(`
3017+
"
3018+
--- ./src/index.vue ---
3019+
<template
3020+
<div class="flex!"></div>
3021+
</template>
3022+
3023+
<style>
3024+
@reference "./input.css";
3025+
3026+
.foo {
3027+
@apply !bg-red-500;
3028+
}
3029+
3030+
.bar {
3031+
/* Do not upgrade the key: */
3032+
flex-shrink: 0;
3033+
}
3034+
</style>
3035+
3036+
--- ./src/input.css ---
3037+
@import 'tailwindcss';
3038+
3039+
.foo {
3040+
flex-shrink: 1;
3041+
}
3042+
3043+
.bar {
3044+
@apply underline!;
3045+
}
3046+
"
3047+
`)
3048+
},
3049+
)
3050+
29703051
function withBOM(text: string): string {
29713052
return '\uFEFF' + text
29723053
}

packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,37 @@ export function isSafeMigration(
2222
location: { contents: string; start: number; end: number },
2323
designSystem: DesignSystem,
2424
): boolean {
25+
// Ensure we are not migrating a candidate in a `<style>` block. The heuristic
26+
// would be if the candidate is preceded by a whitespace and followed by a
27+
// colon and whitespace.
28+
//
29+
// E.g.:
30+
// ```vue
31+
// <template>
32+
// <div class="foo"></div>
33+
// </template>
34+
//
35+
//
36+
// <style>
37+
// .foo {
38+
// flex-shrink: 0;
39+
// ^ ^^
40+
// }
41+
// </style>
42+
// ```
43+
if (
44+
// Whitespace before the candidate
45+
location.contents[location.start - 1]?.match(/\s/) &&
46+
// A colon followed by whitespace after the candidate
47+
location.contents.slice(location.end, location.end + 2)?.match(/^:\s/) &&
48+
// A `<style` block is present before the candidate
49+
location.contents.slice(0, location.start).includes('<style') &&
50+
// `</style>` is present after the candidate
51+
location.contents.slice(location.end).includes('</style>')
52+
) {
53+
return false
54+
}
55+
2556
let [candidate] = Array.from(parseCandidate(rawCandidate, designSystem))
2657

2758
// If we can't parse the candidate, then it's not a candidate at all. However,

0 commit comments

Comments
 (0)