[@tailwindcss/upgrade] Don’t migrate inline style properties (#19918)
Prevent the upgrade tool from rewriting CSS properties inside inline `style` attributes. This fixes cases like `style="flex-grow: 1"` being changed to `style="grow: 1"` and adds regression tests. --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
73f3a6a743
commit
ba667400e8
3 changed files with 139 additions and 17 deletions
|
|
@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Canonicalization: add parentheses when removing whitespace from arbitrary values would hurt readability ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986))
|
||||
- Canonicalization: preserve the original unit in arbitrary values instead of normalizing to base units (e.g. `-mt-[20in]` → `mt-[-20in]`, not `mt-[-1920px]`) ([#19988](https://github.com/tailwindlabs/tailwindcss/pull/19988))
|
||||
- Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#19991](https://github.com/tailwindlabs/tailwindcss/pull/19991))
|
||||
- Upgrade: don’t migrate inline `style` attributes ([#19918](https://github.com/tailwindlabs/tailwindcss/pull/19918))
|
||||
|
||||
## [4.2.4] - 2026-04-21
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ describe('is-safe-migration', async () => {
|
|||
[`<div v-show="shadow"></div>\n`, 'shadow'],
|
||||
[`<div x-if="shadow"></div>\n`, 'shadow'],
|
||||
[`<div style={{filter: 'drop-shadow(30px 10px 4px #4444dd)'}}/>\n`, 'shadow'],
|
||||
[`<div style="flex-grow: 1"></div>\n`, 'flex-grow'],
|
||||
[`<div style='flex-shrink: 0'></div>\n`, 'flex-shrink'],
|
||||
[`<div style=" flex-shrink: 0"></div>\n`, 'flex-shrink'],
|
||||
[`<div style="\nflex-shrink: 0\n"></div>\n`, 'flex-shrink'],
|
||||
|
||||
// Next.js Image placeholder cases
|
||||
[`<Image placeholder="blur" src="/image.jpg" />`, 'blur'],
|
||||
|
|
|
|||
|
|
@ -66,6 +66,38 @@ export function isSafeMigration(
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Inline `style="..."` attributes can contain CSS property names that look
|
||||
// like valid utility candidates, such as `flex-grow`.
|
||||
{
|
||||
let ranges = inlineStyleAttributeValueRanges.get(location.contents)
|
||||
|
||||
for (let i = 0; i < ranges.length; i += 2) {
|
||||
let start = ranges[i]
|
||||
let end = ranges[i + 1]
|
||||
|
||||
if (location.start >= start && location.end <= end) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let [candidate] = parseCandidate(rawCandidate, designSystem)
|
||||
|
||||
// If we can't parse the candidate, then it's not a candidate at all. However,
|
||||
|
|
@ -123,23 +155,6 @@ export function isSafeMigration(
|
|||
}
|
||||
}
|
||||
|
||||
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: Require the candidate to be inside quotes
|
||||
let isQuoteBeforeCandidate = isMiddleOfString(currentLineBeforeCandidate)
|
||||
let isQuoteAfterCandidate = isMiddleOfString(currentLineAfterCandidate)
|
||||
|
|
@ -218,6 +233,14 @@ const BACKSLASH = 0x5c
|
|||
const DOUBLE_QUOTE = 0x22
|
||||
const SINGLE_QUOTE = 0x27
|
||||
const BACKTICK = 0x60
|
||||
const TAB = 0x09
|
||||
const NEWLINE = 0x0a
|
||||
const FORM_FEED = 0x0c
|
||||
const CARRIAGE_RETURN = 0x0d
|
||||
const SPACE = 0x20
|
||||
const SLASH = 0x2f
|
||||
const EQUALS = 0x3d
|
||||
const GREATER_THAN = 0x3e
|
||||
|
||||
function isMiddleOfString(line: string): boolean {
|
||||
let currentQuote: number | null = null
|
||||
|
|
@ -248,3 +271,97 @@ function isMiddleOfString(line: string): boolean {
|
|||
|
||||
return currentQuote !== null
|
||||
}
|
||||
|
||||
const inlineStyleAttributeValueRanges = new DefaultMap((source: string) => {
|
||||
let ranges: number[] = []
|
||||
let offset = 0
|
||||
|
||||
while (true) {
|
||||
let tagStart = source.indexOf('<', offset)
|
||||
if (tagStart === -1) return ranges
|
||||
|
||||
let tagEnd = source.indexOf('>', tagStart + 1)
|
||||
if (tagEnd === -1) return ranges
|
||||
|
||||
offset = tagEnd + 1
|
||||
|
||||
for (let i = tagStart + 1; i < tagEnd; i++) {
|
||||
let char = source.charCodeAt(i)
|
||||
|
||||
if (
|
||||
char === SPACE ||
|
||||
char === TAB ||
|
||||
char === NEWLINE ||
|
||||
char === CARRIAGE_RETURN ||
|
||||
char === FORM_FEED
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
let start = i
|
||||
while (i < tagEnd) {
|
||||
let char = source.charCodeAt(i)
|
||||
if (
|
||||
char === SPACE ||
|
||||
char === TAB ||
|
||||
char === NEWLINE ||
|
||||
char === CARRIAGE_RETURN ||
|
||||
char === FORM_FEED ||
|
||||
char === EQUALS ||
|
||||
char === GREATER_THAN ||
|
||||
char === SLASH
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
let attribute = source.slice(start, i).toLowerCase()
|
||||
if (attribute !== 'style' && attribute !== ':style') continue
|
||||
|
||||
while (i < tagEnd) {
|
||||
let char = source.charCodeAt(i)
|
||||
if (
|
||||
char !== SPACE &&
|
||||
char !== TAB &&
|
||||
char !== NEWLINE &&
|
||||
char !== CARRIAGE_RETURN &&
|
||||
char !== FORM_FEED
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
if (source[i] !== '=') continue
|
||||
|
||||
i++
|
||||
while (i < tagEnd) {
|
||||
let char = source.charCodeAt(i)
|
||||
if (
|
||||
char !== SPACE &&
|
||||
char !== TAB &&
|
||||
char !== NEWLINE &&
|
||||
char !== CARRIAGE_RETURN &&
|
||||
char !== FORM_FEED
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
let quote = source[i]
|
||||
if (quote !== '"' && quote !== "'") continue
|
||||
|
||||
let valueStart = i + 1
|
||||
let valueEnd = source.indexOf(quote, valueStart)
|
||||
if (valueEnd === -1 || valueEnd > tagEnd) break
|
||||
|
||||
ranges.push(valueStart, valueEnd)
|
||||
i = valueEnd
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue