## Summary
This specializes the `.jsonl` and `.ndjson` file extensions so they're
preprocessed like JSON instead of by the standard scanner. This prevents
them from creating thousands of sub machines and reduces scanning time
(see #17125 where this was done for `.json` files).
It seems reasonable to handle new-line delimited JSON files as well
otherwise scanning these files can take quite a long time.
It's quite unlikely that these will contain classes so, alternatively,
these *could* go in the binary extensions list so they get ignored
entirely.
## Test plan
I ran manual tests inside the `oxide` crate against some large-ish JSONL
files (5MB–15MB). These changes bring down scanning time from 2s–3s on
my M3 Max (via `cargo test --release …`) to less than 20ms.
I also ran tests through a full CLI build pipeline on a low-spec linux
box. This change brought scanning time down from ~90s to ~300ms for a
single ~15MB file.
This PR adds a few more canonicalizations for some cases I noticed on
our templates.
When dealing with arbitrary values, and the utility is a "negative"
utility, then we will try to put the `-` inside of the arbitrary value:
```diff
- -left-[9rem]
+ left-[-9rem]
```
The idea is that the arbitrary value is already an escape hatch for when
a value is not available by default. The `-` in front uses an implicit
`calc(<expression> * -1)` which might be confusion if you have an value
like this already.
This also can allow for some further optimizations. For example
```diff
- -mt-[492px]
↓↓↓↓↓↓↓ Into a simpler arbitrary value
+ mt-[-492px]
↓↓↓↓↓↓↓ Into a bare value
+ mt-123
```
This PR also improve the constant folding of calc expressions a bit more
such that nested calc expressions with 2 constants and an unknown can be
folded. Bit of a mouthful, but it allows us to handle this:
```diff
- mt-[calc(-1*calc(-1*var(--foo)))]
↓↓↓↓↓↓↓ The -1 * -1 becomes a no-op
+ mt-[var(--foo)]
↓↓↓↓↓↓↓ Into the shorthand for CSS variables
+ mt-(--foo)
```
Now that we can handle moving the `-` into the arbitrary value, there
are also cases where we can get the `-` _out_ of the arbitrary value:
```diff
- mt-[calc(-1*var(--foo))]
↓↓↓↓↓↓↓ Simplify calc, move `-` to the front
+ -mt-[var(--foo)]
↓↓↓↓↓↓↓ Into the shorthand for CSS variables
+ -mt-(--foo)
```
Another missing piece that this PR adds is the concept of canonicalizing
or normalizing calc expressions. This is a separate step used when
calculating the signature for each utility. This allows us to normalize
`calc(-1*var(--foo))` and `calc(var(--foo)*-1)`. Without this they would
not be considered the same, but not it will.
It's only used when comparing values, it won't unify the actual
arbitrary values with this logic (at least for now).
With the additional constant folding logic and the canonicalization when
comparing signatures it unlocks the necessary power to perform the above
transformations.
## Test plan
1. Existing tests still pass
2. Added additional tests for the constant folding logic
3. Added tests for the canonicalization of calc expressions
4. Added new tests where we move the `-` inside the value, or move the
`-` outside of the arbitrary value.
This are getting a little bit out of hand here, so this is an initial
refactor.
## Test plan
1. All tests are still there
2. All tests are still passing
This PR adds more canonicalization rules for deprecated utilities.
| Before | After |
| --- | --- |
| `overflow-ellipsis` | `text-ellipsis` |
| `start-full` | `inset-s-full` |
| `-start-full` | `-inset-s-full` |
| `start-auto` | `inset-s-auto` |
| `start-px` | `inset-s-px` |
| `-start-px` | `-inset-s-px` |
| `start-8` | `inset-s-8` |
| `-start-8` | `-inset-s-8` |
| `start-123` | `inset-s-123` |
| `-start-123` | `-inset-s-123` |
| `end-full` | `inset-e-full` |
| `-end-full` | `-inset-e-full` |
| `end-auto` | `inset-e-auto` |
| `end-px` | `inset-e-px` |
| `-end-px` | `-inset-e-px` |
| `end-8` | `inset-e-8` |
| `-end-8` | `-inset-e-8` |
| `end-123` | `inset-e-123` |
| `-end-123` | `-inset-e-123` |
In a few cases we already had canonicalization rules, for example
`start-8` where `8` is one of the default suggested spacing scale
values. But this now adds support for positive and negative values that
exceed the default suggested spacing scale as well as some keywords.
## Test plan
1. Existing tests pass
2. Added new tests to ensure these canonicalizations work
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 fixes an issue where `placeholder-*` utilities were reading
values from `--background-color` instead of `--placeholder-color`. In
Tailwind CSS v3, we read from `placeholderColor` which is why we should
use `--placeholder-color` here as well.
f38be227df/src/corePlugins.js (L2317)
That said, this is technically a breaking change in case somebody relies
on `--background-color` for `placeholder` values. But since this is text
related, and most people will rely on the default `--color` values
instead, I think it's safe to change this as-is.
In the unlikely event that somebody _does_ rely on this, then we have 2
options:
1. Guide them to make use of `--placeholder-color` instead (preferred
solution)
2. Re-add `--background-color` after the `--placeholder-color` (band-aid
solution, but might be worth it who knows)
Fixes: #19838
## Test plan
1. Existing tests still pass
2. Verified in the Tailwind CSS v3 codebase that we did read from
`placeholderColor` which in turn reads from `color` by default. Which is
equivalent to `--placeholder-color` and `--color` in Tailwind CSS v4.
This PR adds more declaration expansions such that we can collapse more
utilities.
While testing #19837 I noticed that in my tests some utilities weren't
canonicalized correctly. As part of that PR, we check for
`parsedCandidate.value === null`, which means that a functional utility
without a value is skipped. We do have utilities like that such as
`border` (which is equivalent to `border-1`). But while testing, I
noticed that `border-x border-y` should collapse to `border` but they
didn't. This PR fixes that.
By expanding these properties to their long-form physical properties
(instead of the shorter logical properties) we make the signatures of
utilities a bit bigger, but also more correct such that we can collapse
the physical form into logical utilities.
To make this more concrete, this PR allows for the following
canonicalizations now:
| Input | Output |
| --- | --- |
| `border-t-123 border-r-123 border-b-123 border-l-123` | `border-123` |
| `border-t-1 border-r-1 border-b-1 border-l-1` | `border` |
| `border-t-123 border-b-123` | `border-y-123` |
| `border-l-123 border-r-123` | `border-x-123` |
| `border-t-red-500 border-r-red-500 border-b-red-500 border-l-red-500`
| `border-red-500` |
| `border-t-red-500 border-b-red-500` | `border-y-red-500` |
| `border-l-red-500 border-r-red-500` | `border-x-red-500` |
| `scroll-mt-123 scroll-mr-123 scroll-mb-123 scroll-ml-123` |
`scroll-m-123` |
| `scroll-mt-123 scroll-mb-123` | `scroll-my-123` |
| `scroll-ml-123 scroll-mr-123` | `scroll-mx-123` |
| `scroll-pt-123 scroll-pr-123 scroll-pb-123 scroll-pl-123` |
`scroll-p-123` |
| `scroll-pt-123 scroll-pb-123` | `scroll-py-123` |
| `scroll-pl-123 scroll-pr-123` | `scroll-px-123` |
| `overflow-x-hidden overflow-y-hidden` | `overflow-hidden` |
| `overscroll-x-contain overscroll-y-contain` | `overscroll-contain` |
## Test plan
1. Existing tests pass
2. Added a few more tests to verify that these canonicalizations work
The guard on `dynamicUtilities` restricted root-swapping to named
values, so arbitrary values like `px-[1.2rem] py-[1.2rem]` were never
collapsed into `p-[1.2rem]`.
This is what caused #19835 — in `--stream` mode, the collapse only
happened if an earlier line caused the shorthand to be registered in
`STATIC_UTILITIES_KEY` as a side effect, making the output
non-deterministic. The underlying issue is that arbitrary value collapse
wasn't supported at all.
The fix relaxes the guard from `parsedCandidate.value?.kind !== 'named'`
to `parsedCandidate.value === null`. `cloneCandidate` and
`printCandidate` already handle arbitrary values, so the root-swapping
machinery works without other changes.
The iteration in `dynamicUtilities` is over
`designSystem.utilities.keys('functional')` — a fixed set of roots, not
input-proportional — so the performance cost of including arbitrary
values should be negligible.
Fixes#19835.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
<!--
👋 Hey, thanks for your interest in contributing to Tailwind!
**Please ask first before starting work on any significant new
features.**
It's never a fun experience to have your pull request declined after
investing a lot of time and effort into a new feature. To avoid this
from happening, we request that contributors create a discussion to
first discuss any significant new features.
For more info, check out the contributing guide:
https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md
-->
## Summary
<!--
Provide a summary of the issue and the changes you're making. How does
your change solve the problem?
-->
`@tailwindcss/webpack` currently uses `this.resourcePath` as the cache
key, which ignores the resource query. When the same CSS file is
imported multiple times with different `resourceQuery` values, all of
those imports share a single `CacheEntry`. That means the utilities
discovered for one entry can leak into the CSS output for another entry.
This PR changes the cache key to use `this.resource` (path + query)
instead, while still using `this.resourcePath` for all filesystem work.
## 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.
-->
- `pnpm test:integrations -- webpack/loader.test.ts`
- Confirms all existing webpack loader integration tests pass.
- Confirms the new `@tailwindcss/webpack loader isolates cache by
resource including query` test passes, verifying that two entries
importing the same CSS file with different queries produce isolated
outputs (`dist/a.css` only contains `only-a` / `--color-red-500`, and
`dist/b.css` only contains `only-b` / `--color-blue-500`).
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Change `aliasOnly` from `true` to `false` when calling Vite's resolver
so that the full resolution pipeline runs, including the oxc resolver
responsible for tsconfig path resolution.
When `aliasOnly` was `true`, only the @rollup/plugin-alias plugin ran,
which meant `resolve.tsconfigPaths: true` had no effect on CSS `@import`
or JS `@plugin` resolution in `@tailwindcss/vite`.
Closes#19802.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR fixes an issue where the compiler can crash if it encounters an
invalid codepoint.
When we extract potential candidates from files, it could be that we
encounter values that look like a class or a CSS variable, if it turns
out that it's an invalid CSS variable we can ignore it.
The problem is that sometimes there are escaped values in there that
result in invalid code points crashing the compiler.
This PR fixes that by gracefully handling that and making sure that
invalid code points are replaced by `\uFFFD` as per the spec.
The bug report
(https://github.com/tailwindlabs/tailwindcss/issues/19786) has a clean
example where a piece of text looks like a CSS variable, but contains
invalid code points.
```
--Coding-Projects-CharacterMapper-Master-Workspace\d8819554-4725-4235-9d22-2d0ed572e924
```
Luckily we can fix this today by ignoring the file paths that contain
these strings using `@source not "…";`, but the better way is to
actually fix this.
To solve this, instead of blindly passing numbers to
`String.fromCodePoint`, we will first validate whether it's a valid
codepoint:
1. `0x0000` — `0x10FFFF` (inclusive) is the range of valid code points.
See: https://infra.spec.whatwg.org/#code-point
2. `0xD800` — `0xDBFF` (inclusive) are leading surrogates. See:
https://infra.spec.whatwg.org/#leading-surrogate
3. `0xDC00` — `0xDFFF` (inclusive) are trailing surrogates. See:
https://infra.spec.whatwg.org/#trailing-surrogate
In the code we use the `0xD800` — `0xDFFF` range because the ranges
overlap.
There are various references in the spec to replace surrogates (and
invalid codepoints) with `\uFFFD`. Here is one of them:
https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point
Fixes: https://github.com/tailwindlabs/tailwindcss/issues/19786Fixes: #19801 (this issue talks about a similar invalid code point
issue)
## Test plan
1. Added a regression test where the above string was used as a CSS
variable
2. Added a regression test for the unescape functionality to make sure
that invalid code points and surrogates are replaced by the `\uFFFD`
replacement character.
[ci-all] Just to verify on Windows as well
This PR adds support for canonicalizations for `tracking-*` utilities.
This one is a bit of a funny one, if you take a look at the linked
issue, there is a beautiful table:
| Utility Name | Value | Arbitrary Value | Throws Suggestion |
| - | -: | - | - |
| tracking-tighter | -0.05em | tracking-[-0.05em] | ✗ |
| tracking-tight | -0.025em | tracking-[-0.025em] | ✗ |
| tracking-normal | 0em | tracking-[0em] | ✗ |
| tracking-wide | 0.025em | tracking-[0.025em] | ✗ |
| tracking-wider | 0.05em | tracking-[0.05em] | ✗ |
| tracking-widest | 0.1em | tracking-[0.1em] | ✓ |
It doesn't really make sense to _why_ only the `tracking-widest` one is
properly suggested here. Until you look a little bit closer.
Turns out that `-tracking-tighter` is equivalent to `tracking-wider`,
`-tracking-tight` is equivalent to `tracking-wide` and so on.
The way the canonicalization works internally is by generating a
signature for a given utility class. If two utilities have the exact
same signature, we can consider them the same. In this case
`tracking-widest` and `tracking-[0.1em]` have the same signature.
One of the rules we have internally is that if we find more than one
replacement utility then we don't really know what to do, so we bail.
Because if you get `foo` or `bar`, which one do you pick?
If we refer to this above table again, the moment we want to
canonicalize the `tracking-[-0.05em]` we get two suggestions:
`tracking-tighter` and `-tracking-wider`, since we don't know what to
do, we bail and we don't suggest anything.
So the reason that `tracking-widest` _was_ suggested is just because we
don't have a `-tracking-tightest`.
How do we fix this? Well, since we have `tracking-*` and `-tracking-*`
utilities, I wanted to deprecate the `-tracking-*` ones for named
utilities (where the values come from your theme) because that doesn't
really make sense.
However, we have this exact pattern documented here:
https://tailwindcss.com/docs/letter-spacing#using-negative-values Which
means that I can't just deprecate those utilities.
<img width="723" height="511" alt="image"
src="https://github.com/user-attachments/assets/164b659b-abe9-4f6e-a176-701dd7ea505a"
/>
Instead, I added a different rule which says that if you get multiple
possible replacements, then we prefer the "positive" one, the one
without the `-`. Also added some additional checks to make sure that if
you get `foo`, `-bar`, `baz`, that we also bail because we know that we
should prefer `foo` or `baz` over `-bar`, but we don't know if we should
pick `foo` or `baz`...
This additional rule does solve the original issue, and we already
prefer possible values over negative values in other places (related to
bare values).
Fixes:
https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1558
## Test plan
1. Existing tests pass
2. Added regression tests to make sure that the table from above _does_
get canonicalized correctly into the expected values.
This PR bumps Lightning CSS. The `node/index.js` file in the package
changed, so we had to update the patched version of it as well.
We also had to update the patches for `@parcel/watcher` and
`lightningcss` now that they ship with `detect-libc@2`. Unfortunately,
we still require the patches because we also need to take the
`process.env.PLATFORM_LIBC` into account. Another issue is that bun
needs to be able to statically analyze the `require(…)` calls, so using
the nice `parts.push` approach doesn't really work here.
# Test plan
1. Existing tests should pass
2. Updated some tests to reflect the changes in the generated CSS based
on the version bump.
[ci-all]
This PR fixes a bug in the canonicalization process where if a few
utilities collapse into a smaller one, and the smaller one is part of
the original list, then it results in an empty list.
It will be more clear with an example. Let's say you have this setup:
```
w-[calc(1rem+0.25rem)] h-[calc(1rem+0.25rem)] size-5
```
The first step is that this will result in:
```
w-5 h-5 size-5
```
Then the `w-5 h-5` can turn into `size-5`. But the existing `size-5`,
can also be replaced by the `size-5`.
Internally, when we have a replacement, then we mark all the classes
that can be replaced as "droppable", so they would be dropped from the
list. But in this scenario we also marked `size-5` as droppable,
resulting in an empty list.
If an additional class existed:
```
w-[calc(1rem+0.25rem)] h-[calc(1rem+0.25rem)] size-5 flex
```
The result would be
```
flex
```
Instead of the expected:
```
size-5 flex
```
## Test plan
1. Existing tests pass
2. Added new tests with and without an additional class
This PR adds support for canonicalization of utilities that accept bare
values and exceed the default spacing scale we use for intellisense.
Right now, all utilities are behind functions, so the only way to know
whether something compiles is by compiling a candidate, e.g. `w-8` and
passing it to the utility functions. To help us, we use the intellisense
APIs that we use for suggestions.
Most utilities that accept bare values, have suggestions up until
`*-96`, so `w-96 h-96` would be canonicalized to `size-96`. But the
moment we exceed that, the result stays as-is.
```
→ w-96 h-96
= size-96
→ w-1234 h-1234
= h-1234 w-1234
```
This PR ensures that the last scenario also gets canonicalized to
`size-1234` instead of staying as `h-1234 w-1234`.
```
→ w-96 h-96
= size-96
→ w-1234 h-1234
= size-1234
```
## Test plan
1. Existing tests pass
2. Added new tests for utilities with bare values
[ci-all] just to see if this additional logic doesn't cause timeouts in
CI for WIndows. In my testing this doesn't have a significant impact on
performance at all.
## Summary
- Adds `--stream` flag to `tailwindcss canonicalize` that reads
candidate groups from stdin line by line and writes canonicalized
results to stdout
- Keeps the design system loaded across requests, making it suitable as
a long-running sidecar process
- Empty lines pass through, keeping request/response pairs aligned
## Motivation
Non-JS tools (formatters, editor plugins, etc.) currently have no
lightweight way to canonicalize Tailwind classes. The existing batch
mode works for one-off use, but tools that need to canonicalize
repeatedly pay the cost of loading the design system each time.
With `--stream`, a tool can start `tailwindcss canonicalize --stream`
once and send candidate groups over stdin as needed:
```sh
$ echo -e "py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2" | tailwindcss canonicalize --stream
p-3
m-2
```
Related discussion:
https://github.com/tailwindlabs/tailwindcss/discussions/19736
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Removes unused braces and @types/braces from @tailwindcss/upgrade
package.json as reported in issue #19794
---------
Co-authored-by: CG1AI <sidwu@CG1AIdeMac-mini.local>
Co-authored-by: Robin Malfait <malfait.robin@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
This PR adds support for Vite 8 when using the `@tailwindcss/vite`
package.
From the package's perspective, not a lot had to change, just the `vite`
peer dependency now has an additional `^8.0.0` version range.
Closes: #19789
## Test plan
1. Existing tests pass
2. Manually tested in the `./playgrounds/vite` playground
3. Added Vite 8 integration tests to verify that the plugin works with
Vite 8
<!--
👋 Hey, thanks for your interest in contributing to Tailwind!
**Please ask first before starting work on any significant new
features.**
It's never a fun experience to have your pull request declined after
investing a lot of time and effort into a new feature. To avoid this
from happening, we request that contributors create a discussion to
first discuss any significant new features.
For more info, check out the contributing guide:
https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md
-->
## Summary
- Closes https://github.com/tailwindlabs/tailwindcss/issues/19744
- Closes https://github.com/vitejs/vite-plugin-react/issues/1118
- Closes https://github.com/wakujs/waku/issues/1963
The change in https://github.com/tailwindlabs/tailwindcss/pull/19670
didn't take account for server only modules managed by SSR framework.
Forcing full reload for this path breaks server HMR. This PR added a
check to determine whether the same modified file has associated modules
in a different environment module graph to avoid this.
## 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.
-->
Added an integration test for React router HDR (server loader hmr). This
test fails on main.
Also the local build is tested on `@vitejs/plugin-rsc` CI and confirmed
the fix https://github.com/vitejs/vite-plugin-react/pull/1132
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR fixes an internal source map warning when running tests. To
solve this, we encode the `=` as `\x3d` instead. I'm not 100% sure if
Vitest was hanging on this but it solved the following warning:
```
✓ @tailwindcss/cli src/utils/format-ns.test.ts (21 tests) 3ms
11:43:59 AM [vite] (ssr) Failed to load source map for /Users/robin/github.com/tailwindlabs/tailwindcss/packages/@tailwindcss-node/dist/index.mjs.
Error: An error occurred while trying to read the map file at ${i}
Error: ENOENT: no such file or directory, open '/Users/robin/github.com/tailwindlabs/tailwindcss/packages/@tailwindcss-node/dist/${i}'
at open (node:internal/fs/promises:634:25)
at Object.readFile (node:internal/fs/promises:1238:14)
at extractSourcemapFromFile (file:///Users/robin/github.com/tailwindlabs/tailwindcss/node_modules/.pnpm/vite@7.0.0_@types+node@20.19.1_jiti@2.6.1_lightningcss@1.31.1_patch_hash=tzyxy3asfxcqc7ihroou_7epcep7uhfc7zidzdmxzgvdzwi/node_modules/vite/dist/node/chunks/dep-Bsx9IwL8.js:8349:65)
at loadAndTransform (file:///Users/robin/github.com/tailwindlabs/tailwindcss/node_modules/.pnpm/vite@7.0.0_@types+node@20.19.1_jiti@2.6.1_lightningcss@1.31.1_patch_hash=tzyxy3asfxcqc7ihroou_7epcep7uhfc7zidzdmxzgvdzwi/node_modules/vite/dist/node/chunks/dep-Bsx9IwL8.js:26405:22)
```
## Test plan
1. Existing tests pass
2. Added a failing test to ensure that the source maps are emitted
correctly
## Problem
`canonicalizeCandidates` crashes when called with `collapse: true` and
the candidate list includes utilities whose CSS output contains no
standard declaration properties (only `@property` rules and CSS custom
properties).
This is reproducible with vanilla Tailwind CSS and no custom
configuration:
```js
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], { collapse: true })
// TypeError: X is not iterable
```
```js
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], { collapse: true })
// TypeError: Cannot read properties of null (reading 'has')
```
All shadow utilities (`shadow-sm`, `shadow-md`, `shadow-lg`,
`shadow-xl`) crash when combined with any other utility and `collapse:
true`.
This was discovered via `eslint-plugin-better-tailwindcss`, which calls
`canonicalizeCandidates` with `collapse: true` for its
`enforce-canonical-classes` rule. The crash brings down ESLint entirely.
## Root cause
In `collapseGroup`, the `otherUtilities` array is built by mapping over
each candidate's property values:
```ts
let otherUtilities = candidatePropertiesValues.map((propertyValues) => {
let result: Set<string> | null = null
for (let property of propertyValues.keys()) {
// ... builds result ...
}
return result! // returns null if propertyValues has no keys
})
```
When a utility like `shadow-sm` generates CSS with `@property` rules and
custom property declarations but no standard CSS properties,
`propertyValues.keys()` is empty, the loop never executes, and `result`
stays `null`. The non-null assertion `result!` returns `null` into the
array.
Downstream code then crashes when iterating or calling `.has()` on the
null entry:
```ts
for (let i = 0; i < otherUtilities.length; i++) {
let current = otherUtilities[i] // null
for (let property of current) { // "X is not iterable"
if (other.has(property)) { // "Cannot read properties of null"
```
## Fix
Return an empty `Set` instead of `null` when a utility has no property
keys:
```ts
return result ?? new Set<string>()
```
This is semantically correct: a utility with no standard properties
cannot be linked to or collapsed with any other utility, which is
exactly what an empty Set represents in the linking algorithm. It won't
cause false collapses or suppress valid collapses of other utilities.
## Test plan
- Added test: `collapse does not crash when utilities with no standard
properties are present`
- Verifies `shadow-sm + border`, `shadow-md + p-4`, and `shadow-sm +
shadow-md` don't throw
- Verifies the candidates are returned uncollapsed (correct behavior)
- All 1218 existing tests continue to pass
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR ensures that interpolated expressions in Ruby syntax are
correctly extracted.
The issue was that we ignore comments in Ruby syntax (which start with
`#`). We already made an exception for locals (`<%# locals: … %>`), but
we also need to handle interpolated expressions (`#{ … }`) in the same
way because they are not comments.
Fixes: #19728
## Test plan
1. Existing tests pass
2. Added a regression test for this scenario
3. Tested using the extractor on the given code snippet:
<img width="1461" height="1856" alt="image"
src="https://github.com/user-attachments/assets/b48f7042-ca9d-4133-87ae-4c37c633c073"
/>
Notice that the `w-100` gets extracted now.
When user-controlled candidate values like "constructor" are used as
keys to look up values in plain objects (staticValues, plugin values,
modifiers, config), they can match inherited Object.prototype properties
instead of returning undefined. This caused crashes like "V.map is not
a function" when scanning source files containing strings like
"row-constructor".
Use Object.hasOwn() checks before all user-keyed object lookups in:
- utilities.ts (staticValues lookup)
- plugin-api.ts (values, modifiers, and variant values lookups)
- plugin-functions.ts (get() config traversal function)
Fixes#19721https://claude.ai/code/session_011CYSGw3DLh2Z8xnuyoaCgC
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
## Problem
Tailwind 4.2.0 introduced stricter `@utility` name validation (#19524)
that rejects functional utility names where the root ends with a dash
after stripping the `-*` suffix. This breaks a valid and useful naming
pattern where a double dash separates the CSS property from a value
scale:
```css
@utility border--* {
border-color: --value(--color-border-*, [color]);
}
```
This produces: `border--0`, `border--1`, `border--2`, etc.
The error message is:
> `@utility border--*` defines an invalid utility name. Utilities should
be alphanumeric and start with a lowercase letter.
## Why this pattern matters
The double-dash convention creates a clear visual grammar in class
names. The first segment names the CSS property, and the double dash
separates it from the semantic scale value. In a dense className string
like `border border--0 background--0 content--4`, the scale values (0,
0, 4) are immediately scannable, distinct from the single-dash property
names around them.
This pattern is actively used in production design systems for semantic
color scales (background, content, border, shadow) with values from
0-10.
## Why the restriction is unnecessary
The validation comment states the concern is that `border--*` could
match the bare class `border-` when using default values. However, this
edge case is already handled:
1. **`findRoots` in `candidate.ts`** (line 887) already rejects empty
values: `if (root[1] === '') break`
2. **The Oxide scanner** already extracts double-dash candidates
correctly, as confirmed by existing tests: `("items--center",
vec!["items--center"])`
The candidate parser and scanner both handle this case. The validation
was an overcorrection.
## Changes
- Removed the trailing-dash check from `isValidFunctionalUtilityName` in
`utilities.ts`
- Updated the existing unit test from `['foo--*', false]` to `['foo--*',
true]`
- Added an integration test proving `@utility border--*` compiles
correctly with theme values
## Test results
All 4121 tests pass across the tailwindcss package, including the new
integration test.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
I noticed this when reading the changelog to migrate a project to the
new version. I'm not sure whether retroactively changing the changelog
is considered acceptable behaviour in this project or whether appending
a notice in the next changelog would be better.
Funnily this was actually guessed in #19613, though hard to notice among
the other many false guesses.
## Summary
This PR updates outdated GitHub Action versions by migrating
`actions/download-artifact` from `v6` to `v7` in the following
workflows:
- `.github/workflows/prepare-release.yml`
- `.github/workflows/release.yml`
- `.github/workflows/release-insiders.yml`
## Test plan
The changes will be tested in the CI pipeline of the pull request.
[×] Summary
[×] Test plan
## Summary
This PR makes a few small consistency fixes:
- Fix outdated GitHub links that still point to the old
`tailwindcss/tailwindcss` org and update them to
`tailwindlabs/tailwindcss` (README + contributing docs + PR template).
- Fix punctuation in the contributing guide (“i.e., …”).
- Update two test titles to use “cannot” instead of “can not” for
consistency.
No behavior changes.
## Test plan
- Link check (manual): clicked the updated GitHub links in `README.md`,
`.github/CONTRIBUTING.md`, and `.github/PULL_REQUEST_TEMPLATE.md` to
confirm they resolve correctly.
- (Optional) `pnpm test` — not required since changes are
docs/test-title-only.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
## Summary
fixes
https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/321
This PR fixes an order-sensitive canonicalization bug. This bug caused
issues when running eslint-plugin-better-tailwindcss as the order in
which files are linted in is not consistent. This caused, in some
scenarios, `canonicalizeCandidates(..., { collapse: true,
logicalToPhysical: true, rem: 16 })` to stop collapsing valid
combinations (for example `h-4 + w-4 -> size-4`) after unrelated prior
calls.
To reproduce this issue:
```
# checkout this branch
$ git checkout c/fix-canonicalizeCandidates
# Revert the fix to the current `main` branch
$ git checkout main ./packages/tailwindcss/src/canonicalize-candidates.ts
# Run the tests
$ pnpm run test
```
This should produce a failure like so:
```
FAIL tailwindcss src/canonicalize-candidates.test.ts > regressions > collapse canonicalization is not affected by previous calls
AssertionError: expected [ 'underline', 'h-4', 'w-4' ] to deeply equal [ 'underline', 'size-4' ]
- Expected
+ Received
[
"underline",
- "size-4",
+ "h-4",
+ "w-4",
]
❯ src/canonicalize-candidates.test.ts:1167:66
1165| designSystem.canonicalizeCandidates(['underline', 'mb-4'], options)
1166|
1167| expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
| ^
1168| })
1169| })
```
```
# reset all changes on this branch
git reset --hard
# run the tests again (they should now pass)
pnpm run test
```
The cause of this bug is that the canonicalization caches used
`DefaultMap` in places where lookups were expected to be read-only.
`DefaultMap.get` inserts missing entried, which mutated shared cache
state during intermediate lookups and made later canonicalization
results depend on prior calls.
By replacing the use of `DefaultMap` with a plain `Map`, it avoids
inserting into the map on lookup paths. I've polyfilled
[`Map#getOrInsert`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/getOrInsert)
as it is not widely available yet, and used that where appropriate.
## Test plan
I wrote a test that fails on `main` branch, I then fixed the issue, and
validated that the test now passes.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
<!--depfu-start-->
> 👉 **This PR is queued up to get rebased by Depfu**
<!--depfu-end-->
Here is everything you need to know about this update. Please take a
good look at what changed and the test results before merging this pull
request.
### What changed?
#### ✳️ bun (1.3.7 → 1.3.9) · [Repo](https://github.com/oven-sh/bun)
Sorry, we couldn't find anything useful about this release.
---

[Depfu](https://depfu.com) will automatically keep this PR
conflict-free, as long as you don't add any commits to this branch
yourself. You can also trigger a rebase manually by commenting with
`@depfu rebase`.
<details><summary>All Depfu comment commands</summary>
<blockquote><dl>
<dt>@depfu rebase</dt><dd>Rebases against your default branch and
redoes this update</dd>
<dt>@depfu recreate</dt><dd>Recreates this PR, overwriting any edits
that you've made to it</dd>
<dt>@depfu merge</dt><dd>Merges this PR once your tests are passing and
conflicts are resolved</dd>
<dt>@depfu cancel merge</dt><dd>Cancels automatic merging of this
PR</dd>
<dt>@depfu close</dt><dd>Closes this PR and deletes the branch</dd>
<dt>@depfu reopen</dt><dd>Restores the branch and reopens this PR (if
it's closed)</dd>
<dt>@depfu pause</dt><dd>Ignores all future updates for this dependency
and closes this PR</dd>
<dt>@depfu pause [minor|major]</dt><dd>Ignores all future minor/major
updates for this dependency and closes this PR</dd>
<dt>@depfu resume</dt><dd>Future versions of this dependency will
create PRs again (leaves this PR as is)</dd>
</dl></blockquote>
</details>
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Here is everything you need to know about this update. Please take a
good look at what changed and the test results before merging this pull
request.
### What changed?
#### ✳️ @types/bun (1.3.7 → 1.3.9) ·
[Repo](https://github.com/DefinitelyTyped/DefinitelyTyped)
Sorry, we couldn't find anything useful about this release.
---

[Depfu](https://depfu.com) will automatically keep this PR
conflict-free, as long as you don't add any commits to this branch
yourself. You can also trigger a rebase manually by commenting with
`@depfu rebase`.
<details><summary>All Depfu comment commands</summary>
<blockquote><dl>
<dt>@depfu rebase</dt><dd>Rebases against your default branch and
redoes this update</dd>
<dt>@depfu recreate</dt><dd>Recreates this PR, overwriting any edits
that you've made to it</dd>
<dt>@depfu merge</dt><dd>Merges this PR once your tests are passing and
conflicts are resolved</dd>
<dt>@depfu cancel merge</dt><dd>Cancels automatic merging of this
PR</dd>
<dt>@depfu close</dt><dd>Closes this PR and deletes the branch</dd>
<dt>@depfu reopen</dt><dd>Restores the branch and reopens this PR (if
it's closed)</dd>
<dt>@depfu pause</dt><dd>Ignores all future updates for this dependency
and closes this PR</dd>
<dt>@depfu pause [minor|major]</dt><dd>Ignores all future minor/major
updates for this dependency and closes this PR</dd>
<dt>@depfu resume</dt><dd>Future versions of this dependency will
create PRs again (leaves this PR as is)</dd>
</dl></blockquote>
</details>
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Here is everything you need to know about this update. Please take a
good look at what changed and the test results before merging this pull
request.
### What changed?
#### ✳️ enhanced-resolve (5.18.4 → 5.19.0) ·
[Repo](https://github.com/webpack/enhanced-resolve)
<details>
<summary>Release Notes</summary>
<h4><a
href="https://github.com/webpack/enhanced-resolve/releases/tag/v5.19.0">5.19.0</a></h4>
<blockquote><h3 dir="auto">Features</h3>
<ul dir="auto">
<li>Added TsconfigPathsPlugin (replacement for <code
class="notranslate">tsconfig-paths-webpack-plugin</code>) .</li>
</ul></blockquote>
<p><em>Does any of this look wrong? <a
href="feedback">Please
let us know.</a></em></p>
</details>
<details>
<summary>Commits</summary>
<p><a
href="e8a6ac9fdd...24ba233554">See
the full diff on Github</a>. The new version differs by 4 commits:</p>
<ul>
<li><a
href="24ba233554"><code>chore(release):
5.19.0</code></a></li>
<li><a
href="10dc3bc8b8"><code>chore:
update eslint config webpack (#471)</code></a></li>
<li><a
href="0d6c6c0fd9"><code>feat:
added TsconfigPathsPlugin (replacement for
tsconfig-paths-webpack-plugin) (#463)</code></a></li>
<li><a
href="958aa89219"><code>chore:
migrate to npm from yarn (#469)</code></a></li>
</ul>
</details>
---

[Depfu](https://depfu.com) will automatically keep this PR
conflict-free, as long as you don't add any commits to this branch
yourself. You can also trigger a rebase manually by commenting with
`@depfu rebase`.
<details><summary>All Depfu comment commands</summary>
<blockquote><dl>
<dt>@depfu rebase</dt><dd>Rebases against your default branch and
redoes this update</dd>
<dt>@depfu recreate</dt><dd>Recreates this PR, overwriting any edits
that you've made to it</dd>
<dt>@depfu merge</dt><dd>Merges this PR once your tests are passing and
conflicts are resolved</dd>
<dt>@depfu cancel merge</dt><dd>Cancels automatic merging of this
PR</dd>
<dt>@depfu close</dt><dd>Closes this PR and deletes the branch</dd>
<dt>@depfu reopen</dt><dd>Restores the branch and reopens this PR (if
it's closed)</dd>
<dt>@depfu pause</dt><dd>Ignores all future updates for this dependency
and closes this PR</dd>
<dt>@depfu pause [minor|major]</dt><dd>Ignores all future minor/major
updates for this dependency and closes this PR</dd>
<dt>@depfu resume</dt><dd>Future versions of this dependency will
create PRs again (leaves this PR as is)</dd>
</dl></blockquote>
</details>
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Here is everything you need to know about this update. Please take a
good look at what changed and the test results before merging this pull
request.
### What changed?
#### ✳️ @types/react (19.2.10 → 19.2.14) ·
[Repo](https://github.com/DefinitelyTyped/DefinitelyTyped)
Sorry, we couldn't find anything useful about this release.
---

[Depfu](https://depfu.com) will automatically keep this PR
conflict-free, as long as you don't add any commits to this branch
yourself. You can also trigger a rebase manually by commenting with
`@depfu rebase`.
<details><summary>All Depfu comment commands</summary>
<blockquote><dl>
<dt>@depfu rebase</dt><dd>Rebases against your default branch and
redoes this update</dd>
<dt>@depfu recreate</dt><dd>Recreates this PR, overwriting any edits
that you've made to it</dd>
<dt>@depfu merge</dt><dd>Merges this PR once your tests are passing and
conflicts are resolved</dd>
<dt>@depfu cancel merge</dt><dd>Cancels automatic merging of this
PR</dd>
<dt>@depfu close</dt><dd>Closes this PR and deletes the branch</dd>
<dt>@depfu reopen</dt><dd>Restores the branch and reopens this PR (if
it's closed)</dd>
<dt>@depfu pause</dt><dd>Ignores all future updates for this dependency
and closes this PR</dd>
<dt>@depfu pause [minor|major]</dt><dd>Ignores all future minor/major
updates for this dependency and closes this PR</dd>
<dt>@depfu resume</dt><dd>Future versions of this dependency will
create PRs again (leaves this PR as is)</dd>
</dl></blockquote>
</details>
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>