Improve performance of upgrade tool (#18068)

This PR improves the performance of the upgrade tool due to a regression
introduced by https://github.com/tailwindlabs/tailwindcss/pull/18057

Essentially, we had to make sure that we are not in `<style>…</style>`
tags because we don't want to migrate declarations in there such as
`flex-shrink: 0;`

The issue with this approach is that we checked _before_ the candidate
if a `<style` cold be found and if we found an `</style>` tag after the
candidate.

We would basically do this check for every candidate that matches.

Running this on our Tailwind UI codebase, this resulted in a bit of a
slowdown:

```diff
- Before: ~13s
+  After: ~5m 39s
```

... quite the difference.

This is because we have a snapshot file that contains ~650k lines of
code. Looking for `<style>` and `</style>` tags in a file that large is
expensive, especially if we do it a lot.

I ran some numbers and that file contains ~1.8 million candidates.

Anyway, this PR fixes that by doing a few things:

1. We will compute the `<style>` and `</style>` tag positions only once
per file and cache it. This allows us to re-use this work for every
candidate that needs it.
2. We track the positions, which means that we can simply check if a
candidate's location is within any of 2 start and end tags. If so, we
skip it.

Running the numbers now gets us to:

```diff
- Before: ~5m 39s
+  After: ~9s
```

Much better!

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
Robin Malfait 2025-05-19 16:41:40 +02:00 committed by GitHub
parent 71fb9cdf59
commit a42251cc29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 45 additions and 10 deletions

View File

@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Upgrade: Do not migrate declarations that look like candidates in `<style>` blocks ([#18057](https://github.com/tailwindlabs/tailwindcss/pull/18057))
- Upgrade: Do not migrate declarations that look like candidates in `<style>` blocks ([#18057](https://github.com/tailwindlabs/tailwindcss/pull/18057), [18068](https://github.com/tailwindlabs/tailwindcss/pull/18068))
- Upgrade: Improve `pnpm` workspaces support ([#18065](https://github.com/tailwindlabs/tailwindcss/pull/18065))
## [4.1.7] - 2025-05-15

View File

@ -1,5 +1,6 @@
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import * as version from '../../utils/version'
const QUOTES = ['"', "'", '`']
@ -44,16 +45,23 @@ export function isSafeMigration(
// Whitespace before the candidate
location.contents[location.start - 1]?.match(/\s/) &&
// A colon followed by whitespace after the candidate
location.contents.slice(location.end, location.end + 2)?.match(/^:\s/) &&
// A `<style` block is present before the candidate
location.contents.slice(0, location.start).includes('<style') &&
// `</style>` is present after the candidate
location.contents.slice(location.end).includes('</style>')
location.contents.slice(location.end, location.end + 2)?.match(/^:\s/)
) {
return false
// Compute all `<style>` ranges once and cache it for the current files
let ranges = styleBlockRanges.get(location.contents)
for (let i = 0; i < ranges.length; i += 2) {
let start = ranges[i]
let end = ranges[i + 1]
// Check if the candidate is inside a `<style>` block
if (location.start >= start && location.end <= end) {
return false
}
}
}
let [candidate] = Array.from(parseCandidate(rawCandidate, designSystem))
let [candidate] = parseCandidate(rawCandidate, designSystem)
// If we can't parse the candidate, then it's not a candidate at all. However,
// we could be dealing with legacy classes like `tw__flex` in Tailwind CSS v3
@ -62,10 +70,10 @@ export function isSafeMigration(
// So let's only skip if we couldn't parse and we are not in Tailwind CSS v3.
//
if (!candidate && version.isGreaterThan(3)) {
return true
return false
}
// Parsed a candidate succesfully, verify if it's a valid candidate
// Parsed a candidate successfully, verify if it's a valid candidate
else if (candidate) {
// When we have variants, we can assume that the candidate is safe to migrate
// because that requires colons.
@ -168,3 +176,30 @@ export function isSafeMigration(
return true
}
// Assumptions:
// - All `<style` tags appear before the next `</style>` tag
// - All `<style` tags are closed with `</style>`
// - No nested `<style>` tags
const styleBlockRanges = new DefaultMap((source: string) => {
let ranges: number[] = []
let offset = 0
while (true) {
let startTag = source.indexOf('<style', offset)
if (startTag === -1) return ranges
offset = startTag + 1
// Ensure the style looks like:
// - `<style>` (closed)
// - `<style …>` (with attributes)
if (!source[startTag + 6].match(/[>\s]/)) continue
let endTag = source.indexOf('</style>', offset)
if (endTag === -1) return ranges
offset = endTag + 1
ranges.push(startTag, endTag)
}
})