Fixes#20328.
## What changed
When `@tailwindcss/upgrade` is invoked from a subpackage of a workspace
(e.g. `pnpm --filter …` or `cd packages/foo && pnpm exec upgrade`), it
traverses `../../node_modules/…` and applies its v3 → v4 migration
transform (`@tailwind utilities;` → `@import 'tailwindcss/utilities'
layer(utilities);`) to files inside the installed `tailwindcss` package
itself — a self-referential import that later detonates as an infinite
CSS-resolution loop and reads like a "cache corruption" bug (matches the
report in #19726 / #20328).
Root cause: `globby`'s `isGitIgnored` only walks `.gitignore` files at
or below its `cwd`. When invoked from a subpackage, the workspace-root
`.gitignore` (which almost always lists `node_modules`) is never
consulted, and `../../node_modules/…` paths come back as **not**
ignored.
The fix anchors `isGitIgnored` at the git repository root instead of the
subpackage:
- New helper: `gitRoot(cwd)` in `src/utils/git.ts` — thin wrapper over
`git rev-parse --show-toplevel`.
- `analyze(stylesheets, { base })` in `src/codemods/css/analyze.ts` now
calls `isGitIgnored({ cwd: gitRoot(base) ?? base })`. Falls back to the
previous behavior when not inside a git repository or when `git` is
unavailable.
## Test
Added an integration test that reproduces the exact scenario: a pnpm
workspace with a subpackage that imports `tailwindcss/utilities.css`,
upgrade run from the subpackage, then asserts
`packages/css/node_modules/tailwindcss/utilities.css` still holds the
pristine `@tailwind utilities;` directive.
Verified the test fails on `main` and passes with this change.
The 415 existing upgrade unit tests still pass. Two npm-related snapshot
failures in `upgrade-errors.test.ts` (`half-upgraded v3 project to v4
(bun|npm)`) pre-exist this change — they're about newer npm's `npm
notice run` output that isn't in the snapshots, unrelated to what this
PR touches.
---
Edit by @RobinMalfait: going to run on all [ci-all] so we can make sure
it works on Windows as well.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR fixes a crash while using `npx @tailwindcss/upgrade` when
migrating classes with no body inside of an `@layer utilities`.
When running `npx @tailwindcss/upgrade`, one thing we do is migrate the
CSS from:
```css
@layer utilities {
.foo {
color: red;
}
}
```
To:
```css
@utility foo {
color: red;
}
```
We already have some logic that leaves non-classe (IDs, attribute
selectors, ...) alone. But if we are migrating a class that has no body,
then we will migrate that as well:
```css
@layer utilities {
.empty {
}
}
```
Is turned into:
```css
@utility empty {
}
```
But later in the migration process this will result in an error because
a `@utility` has to have at least _some_ nodes.
Ideally, you don't even have CSS that has empty rules since it doesn't
have any effect in the browser (except of making your CSS file bigger),
but it could be that you don't have control over this file, so a fix is
still valid.
This PR solves that by leaving those rules alone, and keep them in an
`@layer utilities`.
Fixes: #20204
## Test plan
1. Added a dedicated test for this usecase
This PR cleans up some old stale test that has been skipped from the
beginning.
While this test wants to prove that migrating from Tailwind CSS v3 to
Tailwind CSS v4 can handle the `#{!important}` SCSS notation, it doesn't
prove that we can migrate an entire SCSS project. SCSS has much more
special syntax and we never supported that.
Let's get rid of this test that's doing nothing at the moment.
Especially since we don't want to migrate SCSS projects. Enabling this
might result in the false sense that we _do_ support SCSS migrations
which is not the case.
Closes: #20106
This PR is a small improvement of the current `walk` implementation
where we will expose the `index` and the `siblings` on the current
context.
During a walk, we walk over objects that contain a `nodes: []` field.
The `ctx.parent` that already exists is a reference to the parent node,
but `ctx.siblings` is a reference to the `ctx.parent.nodes`.
The `ctx.index` is the index of the current node we are walking in the
`siblings` array. This way we can prevent the awkward
`ctx.parent?.nodes.indexOf(node)` which is a bit silly because we
already know the nodes we're walking and its index...
The `ctx.parent` can be `null`, but the `ctx.siblings` will never be
`null`, this can be seen in a situation like this:
```ts
let ast: AstNode = [nodeA, nodeB]
walk(ast, (node, ctx) => {
if (node === nodeA) {
ctx.parent === null; // Because there is no parent
ctx.siblings === ast; // Because that's the current list we're looping over
// Before this PR, we would have to do something like:
let siblings = ctx.parent?.nodes ?? ast
}
})
```
In the above example, the `ast` is a separately variable, but if this
was inlined, we would run into some issues:
```ts
walk([nodeA, nodeB], (node, ctx) => {
if (node === nodeA) {
ctx.parent === null; // Because there is no parent
ctx.siblings === ast; // Because that's the current list we're looping over
// At this point, there is no way to get to the `[nodeA, nodeB]` list
// without moving it to a variable first.
}
})
```
So, this PR doesn't change much, the additional information we track is
already known information that is now exposed to the caller of the
`walk` function.
In this PR we did update some usages and got rid of some awkward
`ctx.parent?.nodes ?? []` and `ctx.parent.nodes.indexOf(…)` usages.
## Test plan
- All tests still pass as expected
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 PR bumps dependencies in all the packages, typically just bumping
to the latest patch release.
Closes: #19936Closes: #19917Closes: #19899Closes: #19897Closes: #19845Closes: #19832Closes: #19967Closes: #19968
## Test plan
1. All tests still pass
2. All integration tests still pass
[ci-all] to verify Linux, Windows and macOS
This PR is an attempt to make the upgrade tooling more stable.
### TL;DR
1. When migrating from Tailwind CSS v3 → Tailwind CSS v4, only migrate
files listed in the `config.content` instead of relying on v4's auto
content detection feature
2. Skip writing files that have not been changed
3. Write changed files in a safe way: first write to a temporary file,
then rename the file atomically
4. Never migrate files that are git ignored, even if they are listed in
the `config.content` file
5. Always ignore `.env` and `.env.*` files when scanning for files. Most
people will have this in their `.gitignore` file, but if not, then this
is a fallback mechanism.
---
Looking at the #18972 issue, it looks like some people are running into
weird situations where some of the contents is just gone.
I have never been able to reproduce this on my own devices and in my own
projects unfortunately. But there is definitely _something_ happening
that's not right that people are running into.
Therefore, this PR is an attempt to fix what I think _might_ be wrong,
but I'm not 100% sure if these fixes are enough, or if something else is
still happening here.
This builds on top of the #19779 PR which has some small fixes, but is
incomplete to make this work.
### What's happening
Looking at some of the comments, it looks like a few things are
happening such as:
1. The upgrade tool is emptying out my files — it looks like these are
only happening if you ctrl+c while the process is taking a while. It
could be that a lot of files are being checked and therefore the tooling
looks like its stuck.
6. The upgrade tool is looking at files it shouldn't look at — in
Tailwind CSS v4 we have this concept of the auto-content detection. This
means that we will look at any plain text file that is not git ignored.
### Fixes
#### Emptying out files
The files being emptied looks like it's because how `fs.writeFile`
behaves by default. It opens the file handle with the `w` flag, which
will first truncate the file before writing the new contents. This is
not a single atomic operation, so a killed process in the middle will
cause invalid state.
When we migrate your template files, everything is happening in promises
to migrate things at the same time. When a lot of files are being
scanned, truncating might have happened already before we write the new
content. Since we migrate a bunch of files in parallel, a ctrl+c could
cause data loss in multiple files.
To mitigate this, I switched to an alternative way of writing files.
1. First, we do some quick checks where if the contents didn't change we
just bail out immediately. Files that don't include Tailwind CSS classes
won't change, and therefore we don't need to override these files with
the same contents.
2. When the migrated contents is empty, we bail out as well. I'm 100%
sure that this is not the spot where the "emptying out" happens, I still
believe it happens in the `writeFile` itself, but added it just in case.
7. Next, I introduced a safe write, where we first write to a temporary
file in the same folder. We could write it to `/tmp`, but then we can't
guarantee that we are on the same file system.
If we ctrl+c at this stage, then the worst case scenario is that you
have additional temporary files in your project, but your original files
are still there.
Once that file was written, we will use the atomic `fs.rename`. This
should be atomic as long as we are on the same file system, so either
the rename didn't happen yet, or it completed.
I added an integration test for this, but I had to change the
`writeFile` implementation slightly. In the test, we will truncate the
file first, after that we will write the new contents. This is so that
we have enough time to kill the current process and allows us to verify
that we didn't clear out the file. Again, this is a hacky way of testing
this, just because I can't reproduce this issue myself, let alone
reproduce it reliable in a CI environment.
Note: we are also using `realpath` to make sure that we are updating the
real file. Otherwise, if we were dealing with a symlinked file, we would
override the symlink with a "hard" copy instead.
#### Touching files that should not be touched
During the migration, we rely on the Tailwind CSS v4 auto detection
logic which means that it will scan any plain text file that is not git
ignored. Therefore changes to php files could happen because in theory
they could contain Tailwind CSS classes.
To solve this, when migrating from Tailwind CSS v3 to Tailwind CSS v4,
we will _only_ take the sources into account that were listed in the
`config.content` array. Since this was a requirement in Tailwind CSS v3,
it should be safe to rely on this array.
Additionally, this will make sure that we are dealing with way fewer
files to migrate as well.
On top of that, files that match the patterns in the content array that
are git ignored will also be skipped. This is to prevent that we mutate
files in `node_modules` for example.
In one of the comments I read that `.env` files were emptied out. In
most cases people will have these files gitignored but I explicitly
added `.env` and `.env.*` as files to never ever touch by default when
scanning.
Last but not least, this also updates the output a little bit of the
upgrade tool in case we skip content files (because of git ignore) and
if we changed a file.
<img width="1122" height="1376" alt="image"
src="https://github.com/user-attachments/assets/318fdbbf-e319-4c7e-9648-ee9283842624"
/>
- "Git ignored folder, skipping: `./node_modules`": this is because the
content array looks like this while the `node_modules` are being
ignored:
<img width="1090" height="398" alt="image"
src="https://github.com/user-attachments/assets/7d694720-5671-47ec-bb2e-f24c5f2c4248"
/>
- "Migrated
`./resources/views/vendor/filament-panels/components/logo.blade.php`":
this is because **I** made a change to showcase this feature.
Fixes: #18972Closes: #19779
### Test plan
1. Existing tests still pass
1. Added a dedicated integration test to ensure that we only take
`config.content` into account when migrating from Tailwind CSS v3 to
Tailwind CSS v4 projects.
1. Added a dedicated integration test to make sure that files listed in
`config.content` that are also git ignored, will still be skipped.
1. Added a dedicated integration test to ensure that when `writeFile` is
cancelled mid-write that our old files are still present.
1. Added a dedicated integration test to ensure that we ignore `.env`
and `.env.*` files even if you didn't git ignore them.
[ci-all] To verify on Windows
---------
Co-authored-by: Sami <sychocouldy@gmail.com>
This PR does some generic cleanup to the codebase.
I'm playing with oxfmt and oxlint and noticed some unnecessary escapes.
Might add these dependencies to the project later (and rolldown for
building). But baby steps for now.
## Test plan
1. All tests should still pass
Fixes#19345
In v3 the `ringColor.DEFAULT` option was used as the default color for
ring utilities (when it was defined). This currently gets translated as
`--ring-color` but that doesn't work this way in v4. Instead it should
translate to `--default-ring-color` and *not* `--ring-color`.
I've also tweaked the upgrade tool to handle this properly as well.
This PR bumps Vitest from v2 to v4. As far as I know we don't use any
Vitest specific features in our tests, but had to upgrade the
`vitest.workspace.ts` file to a `vitest.config.ts` file instead.
The only features we use are the typical `describe`, `it`, `test`, and
`expect` functions.
The only other part we use is `vi.spyOn` and `vi.fn` but those didn't
change in API either.
The test shards were removed to prevent errors. Not all suites have
enough files / tests to be broken up into 3 parts so Vitest now errors
when that happens.
### Test plan
1. All tests should pass in CI.
2. All integration tests should pass in CI.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
The Bun build issue was caused by Turborepo passing through
`USERPROFILE` from the env. Probably need to file a bug with Bun:
```
Works:
env: USERPROFILE: undefined
CWD: `D:\a\tailwindcss\tailwindcss\packages\@tailwindcss-standalone`
Fails:
env: USERPROFILE: "C:\Users\runneradmin"
CWD: `D:\a\tailwindcss\tailwindcss\packages\@tailwindcss-standalone`
```
This PR drastically improves the memory usage when performing
canonicalization if you swap out the underlying DesignSystem often. This
will be most noticeable in Intellisense.
The big issue we had is that we used module scoped Map objects where we
cache data based on the DesignSystem. If you then create new design
systems (often), then the cache would just keep growing and growing.
This PR solves that by essentially storing all the caches on the Design
System itself. This way, when you throw away a Design System, all the
caches go with it.
Another approach would've been to use a WeakMap, but then we would have
to make sure that no strong references to the DesignSystem exist
anywhere else, otherwise we would still have the same memory issues.
Note: make sure to go commit by commit and use `?w=1` to ignore
whitespace changes.
## Test plan
1. All existing tests pass
Not super sure how to test this as part of the test suite without making
it slow But I also don't think that's super necessary either. Here is an
experiment I did where I introduce 5 design systems:
<img width="1326" height="274" alt="image"
src="https://github.com/user-attachments/assets/817025e3-0f5b-44be-949b-54ed08f5b3fb"
/>
On the current `main` branch, this looks like:
<img width="619" height="69" alt="image"
src="https://github.com/user-attachments/assets/588ae99b-c978-4c01-bfd1-5cc0725723a8"
/>
In this PR, the memory usage looks like:
<img width="512" height="56" alt="image"
src="https://github.com/user-attachments/assets/0052ad21-7b99-4edf-8a14-8ccef52362db"
/>
The memory usage is stable, but to actually prove that we can still
track multiple design systems, let's track them all in a `Set` so
garbage collection cannot get rid of the unused design system objects:
<img width="847" height="230" alt="image"
src="https://github.com/user-attachments/assets/5f044927-3d53-4c15-8145-78eb2b4d6d54"
/>
Now we're sort of back to the current situation on `main`:
<img width="507" height="53" alt="image"
src="https://github.com/user-attachments/assets/868c0238-8646-41ce-8151-e0ef6dd17d64"
/>
This PR generalizes the `walk` implementations we have. What's important
here is that we currently have multiple `walk` implementations, one for
the AST, one for the `SelectorParser`, one for the `ValueParser`.
Sometimes, we also need to go up the tree in a depth-first manner. For
that, we have `walkDepth` implementations.
The funny thing is, all these implementations are very very similar,
even the kinds of trees are very similar. They are just objects with
`nodes: []` as children.
So this PR introduces a generic `walk` function that can work on all of
these trees.
There are also some situations where you need to go down and back up the
tree. For this reason, we added an `enter` and `exit` phase:
```ts
walk(ast, {
enter(node, ctx) {},
exit(node, ctx) {},
})
```
This means that you don't need to `walk(ast)` and later `walkDepth(ast)`
in case you wanted to do something _after_ visiting all nodes.
The API of these walk functions also slightly changed to fix some
problems we've had before. One is the `replaceWith` function. You could
technically call it multiple times, but that doesn't make sense so
instead you always have to return an explicit `WalkAction`. The
possibilities are:
```ts
// The ones we already had
WalkAction.Continue // Continue walking as normal, the default behavior
WalkAction.Skip // Skip walking the `nodes` of the current node
WalkAction.Stop // Stop the entire walk
// The new ones
WalkAction.Replace(newNode) // Replace the current node, and continue walking the new node(s)
WalkAction.ReplaceSkip(newNode) // Replace the current node, but don't walk the new node(s)
WalkAction.ReplaceStop(newNode) // Replace the current node, but stop the entire walk
```
To make sure that we can walk in both directions, and to make sure we
have proper control over when to walk which nodes, the `walk` function
is implemented in an iterative manner using a stack instead of
recursion.
This also means that a `WalkAction.Stop` or `WalkAction.ReplaceStop`
will immediately stop the walk, without unwinding the entire call stack.
Some notes:
- The CSS AST does have `context` nodes, for this we can build up the
context lazily when we need it. I added a `cssContext(ctx)` that gives
you an enhanced context including the `context` object that you can read
information from.
- The second argument of the `walk` function can still be a normal
function, which is equivalent to `{ enter: fn }`.
Let's also take a look at some numbers. With this new implementation,
each `walk` is roughly ~1.3-1.5x faster than before. If you look at the
memory usage (especially in Bun) we go from `~2.2GB` peak memory usage,
to `~300mb` peak memory usage.
Some benchmarks on small and big trees (M1 Max):
<img width="2062" height="1438" alt="image"
src="https://github.com/user-attachments/assets/5ec8c22a-9de8-4e08-869a-18c0d30eb7e8"
/>
<img width="2062" height="1246" alt="image"
src="https://github.com/user-attachments/assets/e89d4b8e-29ca-4aee-8fd2-b7c043d3bbf4"
/>
We also ran some benchmarks on @thecrypticace's M3 Max:
<img width="1598" height="1452" alt="image"
src="https://github.com/user-attachments/assets/3b06b6fe-2497-4f24-a428-1a0e2af3896a"
/>
In node the memory difference isn't that big, but the performance itself
is still better:
<img width="2034" height="1586" alt="image"
src="https://github.com/user-attachments/assets/ef28ae14-b53e-4912-9621-531f3b02898f"
/>
In summary:
1. Single `walk` implementation for multiple use cases
2. Support for `enter` and `exit` phases
3. New `WalkAction` possibilities for better control
4. Overall better performance
5. ... and lower memory usage
## Test plan
1. All tests still pass (but had to adjust some of the APIs if `walk`
was used inside tests).
2. Added new tests for the `walk` implementation
3. Ran local benchmarks to verify the performance improvements
The main goal of this PR was to support canonicalization of zero like
values. We essentially want to canonicalize `-mt-0` as `mt-0`, but also
`mt-[0px]`, `mt-[0rem]`, and other length-like units to just `mt-0`.
To do this, we had to handle 2 things:
1. We introduced some more constant folding, including making `0px` and
`0rem` fold to `0`. We only do this for length units. We also normalize
`-0`, `+0`, `-0.0` and so on to `0`.
2. While pre-computing utilities in our lookup table, we make sure that
we prefer `mt-0` over `-mt-0` if both result in the same signature.
Moved some of the constant folding logic into its own function and added
a bunch of separate tests for it.
## Test plan
Added more unit tests where we normalize different zero-like values to
`0`.
Running the canonicalization logic:
```js
designSystem.canonicalizeCandidates([
'-m-0',
'-m-[-0px]',
'-m-[-0rem]',
'-m-[0px]',
'-m-[0rem]',
'm-0',
'm-[-0px]',
'm-[-0rem]',
'm-[0px]',
'm-[0rem]',
'm-[calc(var(--spacing)*0)]',
'm-[--spacing(0)]',
'm-[--spacing(0.0)]',
'm-[+0]',
'm-[-0]',
'-m-[-0]',
'-m-[+0]',
]) // → ['m-0']
```
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This PR fixes an issue when loading (nested) colors from a config file
and later referencing it via the `theme(…)` function in CSS.
Given a config like this:
```js
module.exports = {
theme: {
colors: {
foo: 'var(--foo-foo)',
'foo-bar': 'var(--foo-foo-bar)',
},
},
}
```
We internally map this into the design system. The issue here is that
the `foo` and `foo-bar` are overlapping and it behaves more like this:
```js
{
foo: {
DEFAULT: 'var(--foo-foo)',
bar: 'var(--foo-foo-bar)'
},
}
```
So while we can easily resolve `colors.foo-bar`, the `colors.foo` would
result in the object with a `DEFAULT` key. This PR solves that by using
the `DEFAULT` key if we end up with an object that has it.
If you end up resolving an object (`theme(colors)`) then the behavior is
unchanged.
## Test plan
1. Added a test based on the config in the issue (which failed before
this fix).
2. Also simplified the test case after identifying the problem (with the
`DEFAULT` key).
Fixes: #19091
This PR improves the performance of when we need to clone some AST
nodes. We have a few places where we clone `Candidate`, `Variant` and
CSS `AST` nodes.
Right now we use `structuredClone`, which works, but it is a generic
solution. However, we do know the exact structure of these AST nodes, so
we can write specialized clone functions that are much faster.
## Test plan
1. All the tests still pass with this change
2. The performance is better:
```
cloneCandidate - src/candidate.bench.ts > Candidate cloning
1.72x faster than cloneCandidate (spread)
74.03x faster than structuredClone
cloneAstNode() - src/ast.bench.ts > Cloning AST nodes
1.15x faster than cloneAstNode (with spread)
33.54x faster than structuredClone()
```
Ready for review, but should be merged after #19059
This PR introduces a new `canonicalizeCandidates` function on the
internal Design System.
The big motivation to moving this to the core `tailwindcss` package is
that we can use this in various places:
- The Raycast extension
- The VS Code extension / language server
- 3rd party tools that use the Tailwind CSS design system APIs
> This PR looks very big, but **I think it's best to go over the changes
commit by commit**. Basically all of these steps already existed in the
upgrade tool, but are now moved to our core `tailwindcss` package.
Here is a list of all the changes:
- Added a new `canonicalizeCandidates` function to the design system
- Moved various migration steps to the core package. I inlined them in
the same file and because of that I noticed a specific pattern (more on
this later).
- Moved `printCandidate` tests to the `tailwindcss` package
- Setup tests for `canonicalizeCandidates` based on the existing tests
in the upgrade tool.
I noticed that all the migrations followed a specific pattern:
1. Parse the raw candidate into a `Candidate[]` AST
2. In a loop, try to migrate the `Candidate` to a new `Candidate` (this
often handled both the `Candidate` and its `Variant[]`)
3. If something changed, print the new `Candidate` back to a string, and
pass it to the next migration step.
While this makes sense in isolation, we are doing a lot of repeated work
by parsing, modifying, and printing the candidate multiple times. This
let me to introduce the `big refactor` commit. This changes the steps
to:
1. Up front, parse the raw candidate into a `Candidate[]` _once_.
2. Strip the variants and the important marker from the candidate. This
means that each migration step only has to deal with the base `utility`
and not care about the variants or the important marker. We can
re-attach these afterwards.
3. Instead of a `rawCandidate: string`, each migration step receives an
actual `Candidate` object (or a `Variant` object).
4. I also split up the migration steps for the `Candidate` and the
`Variant[]`.
All of this means that there is a lot less work that needs to be done.
We can also cache results between migrations. So `[@media_print]:flex`
and `[@media_print]:block` will result in `print:flex` and `print:block`
respectively, but the `[@media_print]` part is only migrated once across
both candidates.
One migration step relied on the `postcss-selector-parser` package to
parse selectors and attribute selectors. I didn't want to introduce a
package just for this, so instead used our own `SelectorParser` in the
migration and wrote a small `AttributeSelectorParser` that can parse the
attribute selector into a little data structure we can work with
instead.
If we want, we can split this PR up into smaller pieces, but since the
biggest chunk is moving existing code around, I think it's fairly doable
to review as long as you go commit by commit.
---
With this new API, we can turn:
```
[
'bg-red-500',
'hover:bg-red-500',
'[@media_print]:bg-red-500',
'hover:[@media_print]:bg-red-500',
'bg-red-500/100',
'hover:bg-red-500/100',
'[@media_print]:bg-red-500/100',
'hover:[@media_print]:bg-red-500/100',
'bg-[var(--color-red-500)]',
'hover:bg-[var(--color-red-500)]',
'[@media_print]:bg-[var(--color-red-500)]',
'hover:[@media_print]:bg-[var(--color-red-500)]',
'bg-[var(--color-red-500)]/100',
'hover:bg-[var(--color-red-500)]/100',
'[@media_print]:bg-[var(--color-red-500)]/100',
'hover:[@media_print]:bg-[var(--color-red-500)]/100',
'bg-(--color-red-500)',
'hover:bg-(--color-red-500)',
'[@media_print]:bg-(--color-red-500)',
'hover:[@media_print]:bg-(--color-red-500)',
'bg-(--color-red-500)/100',
'hover:bg-(--color-red-500)/100',
'[@media_print]:bg-(--color-red-500)/100',
'hover:[@media_print]:bg-(--color-red-500)/100',
'bg-[color:var(--color-red-500)]',
'hover:bg-[color:var(--color-red-500)]',
'[@media_print]:bg-[color:var(--color-red-500)]',
'hover:[@media_print]:bg-[color:var(--color-red-500)]',
'bg-[color:var(--color-red-500)]/100',
'hover:bg-[color:var(--color-red-500)]/100',
'[@media_print]:bg-[color:var(--color-red-500)]/100',
'hover:[@media_print]:bg-[color:var(--color-red-500)]/100',
'bg-(color:--color-red-500)',
'hover:bg-(color:--color-red-500)',
'[@media_print]:bg-(color:--color-red-500)',
'hover:[@media_print]:bg-(color:--color-red-500)',
'bg-(color:--color-red-500)/100',
'hover:bg-(color:--color-red-500)/100',
'[@media_print]:bg-(color:--color-red-500)/100',
'hover:[@media_print]:bg-(color:--color-red-500)/100',
'[background-color:var(--color-red-500)]',
'hover:[background-color:var(--color-red-500)]',
'[@media_print]:[background-color:var(--color-red-500)]',
'hover:[@media_print]:[background-color:var(--color-red-500)]',
'[background-color:var(--color-red-500)]/100',
'hover:[background-color:var(--color-red-500)]/100',
'[@media_print]:[background-color:var(--color-red-500)]/100',
'hover:[@media_print]:[background-color:var(--color-red-500)]/100'
]
```
Into their canonicalized form:
```
[
'bg-red-500',
'hover:bg-red-500',
'print:bg-red-500',
'hover:print:bg-red-500'
]
```
The list is also unique, so we won't end up with `bg-red-500 bg-red-500`
twice.
While the canonicalization itself is fairly fast, we still pay a **~1s**
startup cost for some migrations (once, and cached for the entire
lifetime of the design system). I would like to keep improving the
performance and the kinds of migrations we do, but I think this is a
good start.
The cost we pay is for:
1. Generating a full list of all possible utilities based on the
`getClassList` suggestions API.
2. Generating a full list of all possible variants.
The canonicalization step for this list takes **~2.9ms** on my machine.
Just for fun, if you use the `getClassList` API for intellisense that
generates all the suggestions and their modifiers, you get a list of
**263788** classes. If you canonicalize all of these, it takes
**~500ms** in total. So roughly **~1.9μs** per candidate.
This new API doesn't result in a performance difference for normal
Tailwind CSS builds.
The other potential concern is file size of the package. The generated
`tailwindcss.tgz` file changed like this:
```diff
- 684652 bytes (684.65 kB)
+ 749169 bytes (749.17 kB)
```
So the package increased by ~65 kB which I don't think is the end of the
world, but it is important for the `@tailwindcss/browser` build which we
don't want to grow unnecessarily. For this reason we remove some of the
code for the design system conditionally such that you don't pay this
cost in an environment where you will never need this API.
The `@tailwindcss/browser` build looks like this:
```shell
`dist/index.global.js 255.14 KB` (main)
`dist/index.global.js 272.61 KB` (before this change)
`dist/index.global.js 252.83 KB` (after this change, even smaller than on `main`)
```
### 1. Handling React className migration
The PR fixes an issue when migrating React components to tailwind v4
with the migration tool, that the first class after `className="` is
ignored.
For example, when migrating
```JSX
<div className="shadow"></div>
```
`shadow` will not be migrated to `shadow-sm` .
This is because in `is-safe-migration.ts`, it tests the line before
candidate with regex `/(?<!:?class)=['"]$/`. This basically skips the
migration for anything like `foo="shadow"`, with only exception for Vue
(eg. `class="shadow"`).
The PR changes the regex from
```regex
/(?<!:?class)=['"]$/
````
to
```regex
/(?<!:?class|className)=['"]$/
```
which essentially adds a new exception specifically for React's
`className="shadow"` case.
### 2. Removing redundant rules
Besides, I found that several other rules in
`CONDITIONAL_TEMPLATE_SYNTAX` being redundant since they are already
covered by the rule above, so I removed them. If we prefer the previous
explicit approach, I can revert it.
## Test plan
<!--
Explain how you tested your changes. Include the exact commands that you
used to verify the change works and include screenshots/screen
recordings of the update behavior in the browser if applicable.
-->
Tests added for both the Vue and React classes to prevent false negative
cases.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This PR fixes an issue where sometimes people try to run the upgrade
tool, reset the changes and then try again.
If this happens, then the `package.json` and/or your lock file will
point to the old Tailwind CSS v3 version, but the actual installed
version will be v4.
This will also cause the upgrade tool to now upgrade from v4 to v4,
which is not what most people want if they were trying to upgrade from
v3 to v4. This in turn will cause some issues because now we won't try
to migrate the config file, or v3-specific classes that also exist in v4
but are only safe to upgrade from v3 to v4.
This PR uses `npm ls tailwindcss` to determine the actual installed
version. This command already errors if there is a mismatch between the
installed version and the version in `package.json` or the lock file.
This also happens to work in pnpm and bun projects (added integration
tests for these).
If for whatever reason we can't determine the expected version, we fall
back to the old behavior of just upgrading. In this scenario, the
changes introduced in
https://github.com/tailwindlabs/tailwindcss/pull/19026 will at least
give you a hint of what version was actually installed.
### Test plan
1. Tested it in a v3 project where I performed the following steps:
1. Run the upgrade tool in full (`npx tailwindcss-upgrade`)
2. Reset the changes (`git reset --hard && git clean -df`)
1. Run the upgrade tool again
This resulted in the following output: <img width="1059" height="683"
alt="image"
src="https://github.com/user-attachments/assets/1d2ea2d1-b602-4631-958f-cc21eb8a633f"
/>
2. Added some integration tests to make sure this also works in pnpm,
bun and normal npm projects.
[ci-all]
This PR adds a bit more information when running the upgrade tool to
know what version of Tailwind CSS you're upgrading from. This will help
users and maintainers when things go wrong.
Will have another PR up soon that errors when the Tailwind CSS version
in package.json and node_modules don't match.
### Test plan
Ran this one one of our older projects and saw the version logged
correctly.
<img width="1055" height="363" alt="image"
src="https://github.com/user-attachments/assets/5cbf4c52-ea0f-42c8-bd55-5bae2ed511de"
/>
This PR improves the upgrade tool for shadcn/ui projects where the
`variant = "outline"` is incorrectly migrated to `variant =
"outline-solid"`.
This PR also handles a few more cases:
```ts
// As default argument
function Button({ variant = "outline", ...props }: ButtonProps) { }
// With different kinds of quotes (single, double, backticks)
function Button({ variant = 'outline', ...props }: ButtonProps) { }
// Regardless of whitespace
function Button({ variant="outline", ...props }: ButtonProps) { }
// In JSX
<Button variant="outline" />
// With different quotes and using JavaScript expressions
<Button variant={'outline'} />
// As an object property
buttonVariants({ variant: "outline" })
```
This PR is a follow up of #18815 and #18816, but this time let's migrate
the `supports` theme keys.
Let's imagine you have the following Tailwind CSS v3 configuration:
```ts
export default {
content: ['./src/**/*.html'],
theme: {
extend: {
supports: {
// Automatically handled by bare values (using CSS variable as the value)
foo: 'foo: var(--foo)', // parentheses are optional
bar: '(bar: var(--bar))',
// Not automatically handled because names differ
baz: 'qux: var(--foo)',
// ^^^ ^^^ ← different names
// Custom
grid: 'display: grid',
},
},
},
}
```
Then we would generate the following Tailwind CSS v4 CSS:
```css
@custom-variant supports-baz {
@supports (qux: var(--foo)) {
@slot;
}
}
@custom-variant supports-grid {
@supports (display: grid) {
@slot;
}
}
```
Notice how we didn't generate a custom variant for `data-foo` or
`data-bar` because those are automatically handled by bare values.
I also went with the longer form of `@custom-variant`, we could use the
single selector approach, but that felt less clear to me.
```css
@custom-variant supports-baz (@supports (qux: var(--foo)));
@custom-variant supports-grid (@supports (display: grid));
```
---------
Co-authored-by: Jordan Pittman <thecrypticace@gmail.com>
This PR is similar to and a follow up of #18815, but this time to
migrate the `data` theme keys.
Let's imagine you have the following Tailwind CSS v3 configuration:
```ts
export default {
content: ['./src/**/*.html'],
theme: {
extend: {
data: {
// Automatically handled by bare values
foo: 'foo',
// ^^^ ^^^ ← same names
// Not automatically handled by bare values
bar: 'baz',
// ^^^ ^^^ ← different names
// Completely custom
checked: 'ui~="checked"',
},
},
},
}
```
Then we would generate the following Tailwind CSS v4 CSS:
```css
@custom-variant data-bar (&[data-baz]);
@custom-variant data-checked (&[data-ui~="checked"]);
```
Notice how we didn't generate a custom variant for `data-foo` because
those are automatically handled by bare values.
This PR migrates `aria` theme keys when migrating from Tailwind CSS v3
to v4.
While working on improving some of the error messages to get more
insights into why migrating the JS file changed
(https://github.com/tailwindlabs/tailwindcss/pull/18808), I ran into an
issue where I couldn't think of a good comment to why `aria` theme keys
were not being migrated. (Internally we have `aria` "blocked").
So instead of figuring out a good error message..., I just went ahead
and added the migration for `aria` theme keys.
Let's imagine you have the following Tailwind CSS v3 configuration:
```ts
export default {
content: ['./src/**/*.html'],
theme: {
extend: {
aria: {
// Built-in (not really, but visible because of intellisense)
busy: 'busy="true"',
// Automatically handled by bare values
foo: 'foo="true"',
// ^^^ ^^^ ← same names
// Not automatically handled by bare values because names differ
bar: 'baz="true"',
// ^^^ ^^^ ← different names
// Completely custom
asc: 'sort="ascending"',
desc: 'sort="descending"',
},
},
},
}
```
Then we would generate the following Tailwind CSS v4 CSS:
```css
@custom-variant aria-bar (&[aria-baz="true"]);
@custom-variant aria-asc (&[aria-sort="ascending"]);
@custom-variant aria-desc (&[aria-sort="descending"]);
```
Notice how we didn't generate a custom variant for `aria-busy` or
`aria-foo` because those are automatically handled by bare values.
We could also emit a comment near the CSS to warn about the fact that
`@custom-variant` will always be sorted _after_ any other built-in
variants.
This could result in slightly different behavior, or different order of
classes when using `prettier-plugin-tailwindcss`.
I don't know how important this is, because before this PR we would just
use `@config './tailwind.config.js';`.
Edit: when using the `@config` we override `aria` and extend it, which
means that it would be in the expected order 🤔
---------
Co-authored-by: Jordan Pittman <thecrypticace@gmail.com>
This PR fixes 2 false-positives when running the upgrade tool on a
Tailwind CSS v3 project converting it to a Tailwind CSS v4 project.
The issue occurs around migrations with short simple names that have a
meaning outside if Tailwind CSS, e.g. `blur` and `outline`.
This PR fixes 2 such cases:
1. The `addEventListener` case:
```js
document.addEventListener('blur', handleBlur)
```
We do this by special casing the `addEventListener(` case and making
sure the first argument to `addEventListener` is never migrated.
2. A JavaScript variable with default value:
```js
function foo({ foo = "bar", outline = true, baz = "qux" }) {
// ...
}
```
The bug is relatively subtle here, but it has actually nothing to do
with `outline` itself, but rather the fact that some quote character
came before and after it on the same line...
One of our heuristics for determining if a migration on these small
words is safe, is to ensure that the candidate is inside of a string.
Since we didn't do any kind of quote balancing, we would consider the
`outline` to be inside of a string, even though it is not.
So to actually solve this, we do some form of quote balancing to ensure
that it's _not_ inside of a string in this case.
Additionally, this PR also introduces a small refactor to the
`is-safe-migration.test.ts` file where we now use a `test.each` to
ensure that failing tests in the middle don't prevent the rest of the
tests from running.
### Test plan
1. Added dedicated tests for the cases mentioned in the issue (#18675).
2. Added a few more tests with various forms of whitespace.
Fixes: #18675
Fixes#18400
In v3 when you used `important: true` it did not affect `@apply`.
However, in v4 it does and there's no way to make it *not*. This is
definitely a bug and would be unexpected for users coming from v3 who
use `@apply` and `important` together.
Basically, the following code, along with the detected utility `flex` in
source files…
```css
@import 'tailwindcss/utilities' important;
.flex-explicitly-important {
@apply flex!;
}
.flex-not-important {
@apply flex;
}
```
… would output this:
```css
.flex {
display: flex !important;
}
.flex-explicitly-important {
display: flex !important;
}
.flex-not-important {
display: flex !important;
}
```
But it's expected that `@apply` doesn't consider the "global" important
state. This PR addresss this problem and now the output is this:
```css
.flex {
display: flex !important;
}
.flex-explicitly-important {
display: flex !important;
}
.flex-not-important {
display: flex; /* this line changed */
}
```
If you want to mark a utility as important in `@apply` you can still use
`!` after the utility to do so as shown above.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This pull request contains a couple of minor documentation fixes.
- Corrected a typo from `predicable` to `predictable` in a comment for
`DarkModeStrategy`.
- Applied minor formatting to a comment in a test file.
These changes help improve code clarity and maintainability.
Description:
This pull request corrects minor typos in comments and improves clarity
in two files:
- Fixes a typo in a comment within migrate-js-config.ts ("migrateable" →
"migratable").
- Refines a comment in wasm.test.ts for better readability.
No functional code changes are included.
## Summary
- Fixes a typo: "a arbitrary" → "an arbitrary" in a comment/description.
## Details
- This is a documentation-only change. No code logic is affected.
## Test Plan
- N/A (doc-only)
Co-authored-by: 中野 博文 <hirofumi0082@gmail.com>
This PR fixes an issue where the `blur` in `wire:model.blur="…"` was
incorrectly migrated. We solved it by marking `wire:…` as an unsafe
region (`…` can be anything but whitespace).
Fixes: #18187
## Test plan
Added a test with this use case
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This PR adds some improvements to the upgrade tool where it can now also
migrate negative arbitrary values to negative bare values.
We already had support for the positive version of this:
```diff
- mb-[32rem]
+ mb-128
```
But now it can also handle negative values:
```diff
- mb-[-32rem]
+ -mb-128
```
The only tricky part here is that we had to hoist the `-` sign. Before
this PR, we were actually generating `mb--128` and that is invalid so it
was thrown out.
## Test plan
1. Added a test to ensure that the negative values are correctly
transformed.
This PR fixes 2 issues with the migration tool where certain classes
weren't migrated. This PR fixes those 2 scenarios:
### Scenario 1
When you have an arbitrary opacity modifier that doesn't use `%`, but is
just a number typically between `0` and `1` then this was not converted
to the bare value equivalent before.
E.g.:
```html
<div class="bg-[#f00]/[0.16]"></dv>
```
Will now be converted to:
```html
<div class="bg-[#f00]/16"></dv>
```
### Scenario 2
Fixes a bug when a CSS function was used in a fallback value in the CSS
variable shorthand syntax. In that case we didn't migrate the class to
the new syntax.
This was because we assumed that a `(` was found, that we are dealing
with a CSS function.
E.g.:
```html
<div class="w-[--spacing(1)]"></div>
^ This indicates a CSS function, we should not be
converting this to `w-(--spacing(1))`
```
But if a function was used as a fallback value, for example:
```html
<div class="bg-[--my-color,theme(colors.red.500)]"></dv>
```
Then we also didn't migrate it, but since the function call is in the
fallback, we can still migrate it.
Will now properly be converted to:
```html
<div class="bg-(--my-color,var(--color-red-500))"></dv>
```
## Test plan
1. Added a test for the first case
2. Added a test for the second case
3. Also added an integration-like test that runs all the migration steps
to make sure that the `theme(…)` in the fallback also gets updated to
`var(…)`. This one caught an issue because the `var(…)` wasn't handling
prefixes correctly.
I was testing to upgrade tool on various random projects just to see how
it behaves. Then I noticed an odd migration...
This PR fixes an issue where the upgrade tool accidentally migrated
classes such as `mt-[0px]` to `-mt-[0px]`. The reason for this is
because we are trying to find a replacement, and the computed signature
for both of them are exactly the same.
- `mt-[0px]` translates to:
```css
.x {
margin-top: 0px;
}
```
- `-mt-[0px]` translates to:
```css
.x {
margin-top: calc(0px * -1);
}
```
Which in turn translates to
```css
.x {
margin-top: 0px;
}
```
Notice that this is `0px`, not `-0px`.
Internally we use the roots of functional utilities to find
replacements. For intellisense purposes we typically show negative
versions before positive versions. This then means that we will try
`-mt-*` before `mt-*`. Because of the signature above, the `mt-[0px]`
was translated into `-mt-[0px]`.
We could solve this in a few ways. The first thing we can try is to make
sure that the signature is not the same and that `-mt-[0px]` actually
translates into `-0px` not `0px`.
This would solve our problem of the accidental migration. However, if we
_just_ sort the functional utilities roots such that the positive
versions exist before negative version and rely on the fact that
`-mt-[0px]` has the same signature. Then it also means that by doing
that we can migrate `-mt-[0px]` into `mt-[0px]` which is even better
because it's the same result and shorter.
## Test plan
1. Added a test to verify that `mt-[0px]` does not get migrated to
`-mt-[0px]`.
2. Added a test to verify that `-mt-[0px]` does get migrated to
`mt-[0px]`.
This PR adds an initial version for deprecated utilities. Right now it's
hardcoded to just the `order-none` utility.
This means that `order-0` and `order-[0]` will not be migrated to
`order-none` anymore. We did that automatically because we prefer named
utilities over bare values and arbitrary values.
With this PR, `order-none` is ignored.
Similarly, `order-none` will be migrated to `order-0` instead (defined
in a separate migration for deprecated values). Made it a new migration
instead of using the legacy migration because there all utilities still
exist, but are defined differently (e.g.: `shadow`, `shadow-sm`,
`shadow-xs`).
This PR is also an initial version, it doesn't add any form of
`deprecated` flag or feature on a per-utility implementation basis. This
therefor has the side effect that if you have a custom `order-none`
defined, that it will also be ignored during migrations.
## Test plan
1. Added tests to ensure the `order-0` is not migrated to `order-none`
2. Added tests to ensure `order-none` is migrated to `order-0` (if it's
safe to do so, the signature is still computed to ensure the output is
the same).
3. Ran this on the Tailwind Plus codebase and ensured that `order-0` is
not migrated to `order-none` and that `order-none` is migrated to
`order-0`.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
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 PR fixes an issue where an error such as:
<img width="1702" alt="image"
src="https://github.com/user-attachments/assets/4e6f75c7-3182-4497-939e-96cff08c55ae"
/>
Will be thrown during the upgrade process. This can happen when you are
using `pnpm` and your CSS file includes a `@import "tailwindcss";`. In
this scenario, `tailwindcss` will be loaded from a shared `.pnpm` folder
outside of the current working directory.
In this case, we are also not interested in migrating _that_ file, but
we also don't want the upgrade process to just crash.
I didn't see an option to ignore errors like this, so wrapped it in a
try/catch instead.
It also fixes another issue where if you are using a pnpm workspace and
run the upgrade tool from the root, then it throws you an error that you
cannot add dependencies to the workspace root unless `-w` or
`--workspace-root` flags are passed.
For this, we disable the check entirely using the
`--ignore-workspace-root-check` flag. If we always used the
`--workspace-root` flag, then the dependencies would always be added to
the root, regardless of where you are running the script from which is
not what we want.
## Test plan
Before:
<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/78246876-3eb6-4539-a557-d3d366f1b3a3"
/>
After:
<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/a65e4421-d7c5-4d83-b35d-934708543e25"
/>
Before:
<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/53772661-2c4a-4212-84d9-a556a0ad320f"
/>
After:
<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/5bfaf20e-34b8-44fd-9b59-e72d36738879"
/>
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
This PR makes the migrations for templates much faster. To make this
work, I also had to move things around a bit (so you might want to check
this PR commit by commit). I also solved an issue by restructuring the
code.
### Performance
For starters, we barely applied any caching when migrating candidates
from α to β. The problem with this is that in big projects the same
candidates will appear _everywhere_, so caching is going to be useful
here.
One of the reasons why we didn't do any caching is that some migrations
were checking if a migration is actually safe to do. To do this, we were
checking the `location` (the location of the candidate in the template).
Since this location is unique for each template, caching was not
possible.
So the first order of business was to hoist the `isSafeMigration` check
up as the very first thing we do in the migration.
If we do this first, then the only remaining code relies on the
`DesignSystem`, `UserConfig` and `rawCandidate`.
In a project, the `DesignSystem` and `UserConfig` will be the same
during the migration, only the `rawCandidate` will be different which
means that we can move all this logic in a good old `DefaultMap` and
cache the heck out of it.
Running the numbers on our Tailwind Plus repo, this results in:
```
Total seen candidates: 2 211 844
Total migrated candidates: 7 775
Cache hits: 1 575 700
```
That's a lot of work we _don't_ have to do. Looking at the timings, the
template migration step goes from ~45s to ~10s because of this.
Another big benefit of this is that this makes migrations _actually_
safe. Before we were checking if a migration was safe to do in specific
migrations. But other migrations were still printing the candidate which
could still result in an unsafe migration.
For example when migrating the `blur` and the `shadow` classes, the
`isSafeMigration` was used. But if the input was `!flex` then the safety
check wasn't even checked in this specific migration.
### Safe migrations
Also made some changes to the `isSafeMigration` logic itself. We used to
start by checking the location, but thinking about the problem again,
the actual big problem we were running into is classes that are short
like `blur`, and `shadow` because they could be used in other contexts
than a Tailwind CSS class.
Inverting this logic means that more specific Tailwind CSS classes will
very likely _not_ cause any issues at all.
For example:
- If you have variants: `hover:focus:flex`
- If you have arbitrary properties: `[color:red]`
- If you have arbitrary values: `bg-[red]`
- If you have a modifier: `bg-red-500/50`
- If you have a `-` in the name: `bg-red-500`
Even better if we can't parse a candidate at all, we can skip the
migrations all together.
This brings us to the issue in #17974, one of the issues was already
solved by just hoisting the `isSafeMigration`. But to make the issue was
completely solved I also made sure that in Vue attributes like
`:active="…"` are also considered unsafe (note: `:class` is allowed).
Last but not least, in case of the `!duration` that got replaced with
`duration!` was solved by verifying that the candidate actually produces
valid CSS. We can compute the signature for this class.
The reason this wasn't thrown away earlier is because we can correctly
parse `duration` but `duration` on its own doesn't exist,
`duration-<number>` does exist as a functional utility which is why it
parsed in the first place.
Fixes: #17974
## Test plan
1. Ran the tool on our Tailwind UI Templates repo to compare the new
output with the "old" behavior and there were no differences in output.
2. Ran the tool on our Tailwind Plus repo, and the template migration
step went from ~45s to ~10s.
3. Added additional tests to verify the issues in #17974 are fixed.
[ci-all] let's run this on all CI platforms...
Fixes#16156
## Summary
This PR adds a new 3 -> 4 template migration that changes the casing of
in both utility values and modifier values from camelCase to kebab-case
to match the updated CSS variable names.
## Test plan
- Added integration test, see the diff in the PR.
This PR improves the upgrade tool by also migrating bare values to named
values defined in the `@theme`.
Recently we shipped some updates dat allowed us to migrate arbitrary
values (with square brackets), but we didn't migrate bare values yet.
That means that in this example:
```html
<div class="aspect-[16/9]"></div>
<div class="aspect-16/9"></div>
```
We migrated this to:
```html
<div class="aspect-video"></div>
<div class="aspect-16/9"></div>
```
With this change, we will also try and migrate the bare value to a named
value. So this example:
```html
<div class="aspect-[16/9]"></div>
<div class="aspect-16/9"></div>
```
Now becomes:
```html
<div class="aspect-video"></div>
<div class="aspect-video"></div>
```
## Test plan
1. Added unit tests for the new functionality.
2. Ran this on a local project
Before:
<img width="432" alt="image"
src="https://github.com/user-attachments/assets/ce1adfbd-7be1-4062-bea5-66368f748e44"
/>
After:
<img width="382" alt="image"
src="https://github.com/user-attachments/assets/a385c94c-4e4c-4e1c-ac73-680c56ac4081"
/>
When passing `gitignore: true` to globby it will start a search for all
.gitignore files, this initial search includes node_modules making it
hang forever for large monorepos with many files inside node_modules