Commit graph

6661 commits

Author SHA1 Message Date
Robin Malfait
f302fce815
Fix canonicalization resulting in empty list (#19812)
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
2026-03-17 13:00:31 +01:00
Robin Malfait
bb2f170514
Improve canonicalization for bare values exceeding default spacing scale suggestions (#19809)
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.
2026-03-16 22:44:29 +01:00
Aaron Tinio
aaaefe8b5d
Add --stream flag to canonicalize subcommand (#19796)
## 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>
2026-03-16 15:00:11 +00:00
SidWuChou
d24b1127bc
fix: remove unused braces dependencies (#19797)
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>
2026-03-15 21:10:54 +00:00
Robin Malfait
faa5e8849b
Cleanup inconsistencies related to (regex) escapes (#19804)
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
2026-03-15 22:04:58 +01:00
Robin Malfait
a4be983865
increase timeout of canonicalization tests 2026-03-12 21:08:06 +01:00
Robin Malfait
59b0329f85
Add support for Vite 8 in @tailwindcss/vite (#19790)
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
2026-03-12 20:54:34 +01:00
Robin Malfait
dfc449c9ff
slightly re-word CHANGELOG 2026-03-12 19:50:16 +01:00
Hiroshi Ogawa
bf441a799f
fix(vite): skip full reload for server only modules scanned by client css (#19745)
<!--

👋 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>
2026-03-12 18:49:41 +00:00
Robin Malfait
6b54dd8630
Fix internal source map warnings (#19788)
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
2026-03-12 12:21:00 +01:00
Robin Malfait
ad9fdef005
drop unnecessary test 2026-03-11 20:35:35 +01:00
Robin Malfait
e96909accd
Add tailwindcss canonicalize sub-command (#19783)
This PR adds a new `tailwindcss canonicalize` sub-command.
2026-03-11 20:30:21 +01:00
Robin Malfait
d5717f2307
run prettier 2026-03-11 19:59:38 +01:00
Kirk Ouimet
51aa9d799c
fix(canonicalize): handle utilities with empty property maps in collapse (#19727)
## 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>
2026-03-10 16:48:07 +01:00
Robin Malfait
c586bd6a94
Canonicalize calc(var(--spacing)*…) expressions into --spacing(…) (#19769)
This PR canonicalizes usages of `calc(var(--spacing)*…)` to
`--spacing(…)` when used in arbitrary values.

Some examples:

| Before | After |
| --- | --- |
| `pt-[min(20%,calc(var(--spacing)*8))]` | `pt-[min(20%,--spacing(8))]`
|
| `pt-[min(20%,calc(var(--spacing)*var(--other)))]` |
`pt-[min(20%,--spacing(var(--other)))]` |
| `pt-[calc(var(--spacing)*8)]` | `pt-8` |
| `pt-[calc(var(--spacing)*var(--other))]` |
`pt-[--spacing(var(--other))]` |
| `[padding-top:min(20%,calc(var(--spacing)*8))]` |
`pt-[min(20%,--spacing(8))]` |
| `[padding-top:min(20%,calc(var(--spacing)*var(--other)))]` |
`pt-[min(20%,--spacing(var(--other)))]` |
| `[padding-top:calc(var(--spacing)*8)]` | `pt-8` |
| `[padding-top:calc(var(--spacing)*var(--other))]` |
`pt-[--spacing(var(--other))]` |


## Test plan

1. Existing tests pass
2. Added new tests
2026-03-08 20:19:35 +01:00
Robin Malfait
bf2e2fe08a
Extract classes from interpolated expressions in Ruby (#19730)
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.
2026-02-26 12:23:50 +01:00
Adam Wathan
9ded4a23de
Guard object lookups against inherited prototype properties (#19725)
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 #19721

https://claude.ai/code/session_011CYSGw3DLh2Z8xnuyoaCgC

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-02-25 15:16:09 +00:00
Robin Malfait
097f982d7a
update changelog 2026-02-23 13:46:49 +01:00
Robin Malfait
1dce64ee7e
4.2.1 (#19714) 2026-02-23 11:45:12 +01:00
Robin Malfait
58d1fe3948
Fix missing extracted classes in mdx files (#19711) 2026-02-22 13:02:05 +01:00
Robin Malfait
1a973557ed
update webpack readme to be a bit more consistent with other readmes 2026-02-20 12:04:41 +01:00
Robin Malfait
14083ad9aa
use simpler tailwindcss import 2026-02-20 12:03:25 +01:00
Kirk Ouimet
d15d92ca60
Allow trailing dash in functional utility names (#19696)
## 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>
2026-02-19 14:40:37 +00:00
ginnyTheCat
7a54d15f72
Correct CHANGELOG to mention inset not inline (#19700)
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.
2026-02-19 14:05:37 +01:00
Robin Malfait
1b16411919
4.2.0 (#19695)
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2026-02-18 15:45:06 +01:00
Pádraic Slattery
198af9c3ae
chore: Update outdated GitHub Actions versions (#19577)
## 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
2026-02-18 12:27:13 +00:00
Pavan Shinde
6118f4f6a7
Fix/misc docs and tests (#19652)
## 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>
2026-02-18 12:23:48 +00:00
Cameron
5a4a7eba3a
fix(canonicalize): prevent collapse cache pollution across calls (#19675)
## 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>
2026-02-18 12:16:59 +00:00
depfu[bot]
d520e1f571
Update bun 1.3.7 → 1.3.9 (patch) (#19678)
<!--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
Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[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>
2026-02-18 12:00:11 +01:00
depfu[bot]
acf8dd0bf0
Update @types/bun 1.3.7 → 1.3.9 (patch) (#19689)
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
Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[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>
2026-02-18 11:59:30 +01:00
depfu[bot]
d9c4cd8c7c
Update enhanced-resolve 5.18.4 → 5.19.0 (minor) (#19658)
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
Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[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>
2026-02-18 11:59:11 +01:00
depfu[bot]
fb42a3ff9e
Update semver 7.7.3 → 7.7.4 (patch) (#19667)
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?




#### ✳️ semver (7.7.3 → 7.7.4) ·
[Repo](https://github.com/npm/node-semver) ·
[Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)



<details>
<summary>Release Notes</summary>
<h4><a
href="https://github.com/npm/node-semver/releases/tag/v7.7.4">7.7.4</a></h4>

<blockquote><h2 dir="auto">
<a
href="https://bounce.depfu.com/github.com/npm/node-semver/compare/v7.7.3...v7.7.4">7.7.4</a>
(2026-01-16)</h2>
<h3 dir="auto">Bug Fixes</h3>
<ul dir="auto">
<li>
<a
href="a29faa5f33"><code
class="notranslate">a29faa5</code></a> <a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/835">#835</a>
cli: pass options to semver.valid() for loose version validation (<a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/835">#835</a>)
(<a
href="https://bounce.depfu.com/github.com/mldangelo">@mldangelo</a>)</li>
</ul>
<h3 dir="auto">Documentation</h3>
<ul dir="auto">
<li>
<a
href="1d28d5e82d"><code
class="notranslate">1d28d5e</code></a> <a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/836">#836</a>
fix typos and update -n CLI option documentation (<a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/836">#836</a>)
(<a
href="https://bounce.depfu.com/github.com/mldangelo">@mldangelo</a>)</li>
</ul>
<h3 dir="auto">Dependencies</h3>
<ul dir="auto">
<li>
<a
href="120968b767"><code
class="notranslate">120968b</code></a> <a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/840">#840</a>
<code class="notranslate">@npmcli/template-oss@4.29.0</code> (<a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/840">#840</a>)</li>
</ul>
<h3 dir="auto">Chores</h3>
<ul dir="auto">
<li>
<a
href="44d7130c60"><code
class="notranslate">44d7130</code></a> <a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/824">#824</a>
bump @npmcli/eslint-config from 5.1.0 to 6.0.0 (<a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/824">#824</a>)
(<a
href="https://bounce.depfu.com/github.com/dependabot">@dependabot</a>[bot])</li>
<li>
<a
href="70735767b6"><code
class="notranslate">7073576</code></a> <a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/820">#820</a>
reorder parameters in invalid-versions.js test (<a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/820">#820</a>)
(<a href="https://bounce.depfu.com/github.com/reggi">@reggi</a>)</li>
<li>
<a
href="5816d4cfd6"><code
class="notranslate">5816d4c</code></a> <a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/829">#829</a>
bump @npmcli/template-oss from 4.28.0 to 4.28.1 (<a
href="https://bounce.depfu.com/github.com/npm/node-semver/pull/829">#829</a>)
(<a
href="https://bounce.depfu.com/github.com/dependabot">@dependabot</a>[bot],
<a
href="https://bounce.depfu.com/github.com/npm-cli-bot">@npm-cli-bot</a>)</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="a25789b09b...5993c2e42b">See
the full diff on Github</a>. The new version differs by 10 commits:</p>
<ul>
<li><a
href="5993c2e42b"><code>chore:
release 7.7.4 (#839)</code></a></li>
<li><a
href="120968b767"><code>deps:
@npmcli/template-oss@4.29.0 (#840)</code></a></li>
<li><a
href="a29faa5f33"><code>fix(cli):
pass options to semver.valid() for loose version validation
(#835)</code></a></li>
<li><a
href="1d28d5e82d"><code>docs:
fix typos and update -n CLI option documentation (#836)</code></a></li>
<li><a
href="5816d4cfd6"><code>chore:
bump @npmcli/template-oss from 4.28.0 to 4.28.1 (#829)</code></a></li>
<li><a
href="ab9e28a582"><code>chore:
bump @npmcli/template-oss from 4.27.1 to 4.28.0 (#827)</code></a></li>
<li><a
href="44d7130c60"><code>chore:
bump @npmcli/eslint-config from 5.1.0 to 6.0.0 (#824)</code></a></li>
<li><a
href="70735767b6"><code>chore:
reorder parameters in invalid-versions.js test (#820)</code></a></li>
<li><a
href="16a35f5df4"><code>chore:
bump @npmcli/template-oss from 4.26.0 to 4.27.1 (#823)</code></a></li>
<li><a
href="3a3459da45"><code>chore:
bump @npmcli/template-oss from 4.25.1 to 4.26.0 (#818)</code></a></li>
</ul>
</details>












---
![Depfu
Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[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>
2026-02-18 11:58:49 +01:00
depfu[bot]
06b6473fcc
Update @types/react 19.2.10 → 19.2.14 (patch) (#19681)
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
Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[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>
2026-02-18 11:58:33 +01:00
Adam Wathan
d0a5612872
Add mauve, olive, mist, and taupe color palettes (#19627)
## Summary

This PR adds four new neutral color palettes to Tailwind CSS: `mauve`,
`olive`, `mist`, and `taupe`. Each palette includes 11 shades (50, 100,
200, 300, 400, 500, 600, 700, 800, 900, 950) defined using the OKLch
color space for perceptually uniform color transitions.

These new palettes expand the available neutral color options beyond the
existing stone, slate, gray, zinc, and neutral palettes, providing
designers with more nuanced choices for different design systems and
brand aesthetics.

The changes include:
- Added color definitions to `packages/tailwindcss/src/compat/colors.ts`
- Added corresponding CSS custom properties to
`packages/tailwindcss/theme.css`

## Test plan

To verify these changes:
1. Build the project and ensure no compilation errors occur
2. Verify that the new color palettes are available in the Tailwind
config
3. Test that utilities like `bg-mauve-500`, `text-olive-600`,
`border-mist-300`, and `ring-taupe-400` work correctly
4. Confirm that the colors render with proper OKLch values in the
browser

https://claude.ai/code/session_01QhC1ZYkW3pCd55WbbqYBNV

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-02-18 11:52:35 +01:00
Pavan Shinde
d9fff9f595
docs: update package README CI badge to main (#19692)
## Summary
Update the GitHub Actions CI status badge URLs in `packages/*/README.md`
to track `branch=main` instead of `branch=next`. This matches the root
README, keeps the badges consistent across the repo, and ensures the
displayed CI status reflects the default branch.

## Test plan
Docs-only change.
Verified the updated badge URLs in the modified `packages/*/README.md`
files use `ci.yml?branch=main`.
2026-02-18 11:52:07 +01:00
Rózsa Zoltán
ed52d3e6c9
feat: handle backslash in @utility name (#19626)
Resolves #19607

<!--

👋 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/tailwindcss/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?

-->

I believe there is no obstacle to simply ignoring backslashes in the
name. This way, various validators - which are not aware of Tailwind
CSS's specific syntax (which allows the `/` character to be used
directly in utility names) - can still be bypassed using backslashes.
For example, instead of `@utility push-1/2`, one could use `@utility
push-1\/2`, while the end result would be identical.

## 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.

-->

I took a previous utility test as a baseline and extended it with
backslashes, and I expect the same result in the output as in the
original test case:

*
https://github.com/rozsazoltan/tailwindcss/blob/main/packages/tailwindcss/src/utilities.test.ts#L28553-L28567
(the original test case I started from)
*
https://github.com/rozsazoltan/tailwindcss/blob/feat/handle-blackslash-in-utility-name/packages/tailwindcss/src/index.test.ts#L4640-L4654
(the current PR's test)

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-02-17 21:12:20 +00:00
Andrey Viktorov
bc6e4b85bf
Fallback to config.createResolver for client and ssr environments in @tailwindcss/vite (#19679)
<!--

👋 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/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md

-->

## Summary

Sometimes even if Vite Envrionment API is available, some plugins are
still override `config.createResolver` function to inject own aliases

Since technically `config.createResolver` was only [properly
deprecated](https://github.com/vitejs/vite/pull/20031) in Vite 7.0.0,
it's still a valid(-ish) to do so, even if it wasn't ever officially
supported

Vite already handles this in its internal css resolvers, but not exposes
the code to do so as part of public API, so I've copied and adapted it

Fixes #19677

## Test plan

Tested by copying built package into my repro from the issue, also ran
vite integration tests

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-02-17 19:59:57 +00:00
Robin Malfait
095ff96ba3
Improve performance in bigger projects (#19632)
This PR improves the performance of Oxide when scanning large codebases.

The `Oxide` API, looks something like this:

```ts
let scanner = new Scanner({ sources })
let candidates = scanner.scan() // Found candidates
let files = scanner.files // Scanned files
let globs = scanner.globs // Scanned globs
```

The `files` and `globs` are used to tell PostCSS, Vite, webpack etc
which files to watch for changes.
The `.scan()` operation extracts the candidates from the source files.
You can think of these as potential Tailwind CSS classes.

In all these scenarios we have to walk the file system and find files
that match the `sources`.

### 1. Prevent multiple file system walks

The first big win came from the fact that accessing `.files` after a
`.scan()` also does an entire walk of the file system (for the given
`sources`), which is unnecessary because we just walked the file system.

This is something that's not really an issue in smaller codebases
because we have `mtime` tracking. We don't re-scan a file if its `mtime`
hasn't changed since the last scan. However, in large codebases with
thousands of files, even walking the file system to check `mtime`s can
be expensive.

### 2. Use parallel file system walking

Another big win is to use a parallel file system walker instead of a
synchronous one. The big problem here is that the parallel build has
20ms-50ms of overhead which is noticeable on small codebases. We don't
really know if you have a small or big codebase ahead of time, so maybe
some kind of hint in the future would be useful.

So the solution I settled on right now is to use a synchronous walker
for the initial scan, and then switch to a parallel walker for
subsequent scans (think dev mode). This gives us the best of both
worlds: fast initial scan on small codebases, and fast re-scans on large
codebases.

Caveat: if you use the `@tailwindcss/cli` we know exactly which files
changed so we can just re-scan those files directly without walking the
file system at all. But in `@tailwindcss/postcss` we don't know which
files changed, so we have to walk the file system to check `mtime`s.

While this improvement is nice, it resulted in an annoying issue related
to `mtime` tracking. Since the parallel walker processes files in
parallel, the `mtime` was typed as `Arc<Mutex<FxHashMap<PathBuf,
SystemTime>>>` so to avoid locking, I decided to only walk the files
here and collect their paths. Then later we check the `mtime` to know
whether to re-scan them or not.

Initially I just removed the `mtime` tracking altogether. But it did
have an impact when actually extracting candidates from those files, so
I added it back later.

### 3. Delaying work

I was still a bit annoyed by the fact that we had to track `mtime`
values for every file. This seems like annoying overhead, especially
when doing a single build (no dev mode).

So the trick I applied here is to only start tracking `mtime` values
after the initial scan.

This means that, in dev mode, we would do this:

1. Walk entire file system to track files.
2. On a subsequent scan, walk entire file system (again) and start
tracking `mtime` values. This time, we use the parallel walker instead
of the synchronous one.
3. On further scans, only re-scan files whose `mtime` has changed

The trade-off here is that on the second scan we always re-scan all
files, even if they haven't changed. Since this typically only happens
in dev mode, I think this is an acceptable trade-off especially if the
initial build is therefor faster this way.

### 3. Small wins

There are also a few small wins in here that I would like to mention but
that are less significant:

1. Pre-computed normalized `source` patterns instead of in every walker
filter call.
2. Tried to avoid some allocations in various places. For example the
`pre_process_input` always called `content.to_vec()` which allocates.
Instead we now accept an owned `Vec<u8>` so we don't have to call
`.to_vec()` in the default case (in my testing, this is ~92% of the time
in the codebases I checked).
3. Made the `Cursor` struct smaller, which is used a lot during
candidate extraction.

### Benchmarks

Now for the fun stuff, the benchmarks!

<details>

<summary>The code for the benchmarks</summary>

```ts
import path from 'node:path'
import { bench, boxplot, do_not_optimize, run, summary } from 'mitata'

import { Scanner as ScannerPr } from '/path/to/repo/with/pr/branch/tailwindcss/crates/node'
import { Scanner as ScannerMain } from '/path/to/repo/with/main/branch/tailwindcss/crates/node'

let base = '/path/to/some/codebase'
let sources = [{ base, pattern: '**/*', negated: false }]

// Verify the results are the same before benchmarking
let scannerPr = new ScannerPr({ sources })
let scannerMain = new ScannerMain({ sources })

{
  let aCandidates = scannerPr.scan()
  let bCandidates = scannerMain.scan()

  if (aCandidates.length !== bCandidates.length) {
    throw new Error(`Mismatch in candidate count: ${aCandidates.length} vs ${bCandidates.length}`)
  }

  for (let i = 0; i < aCandidates.length; i++) {
    if (aCandidates[i] !== bCandidates[i]) {
      throw new Error(`Mismatch in candidate at index ${i}: ${aCandidates[i]} vs ${bCandidates[i]}`)
    }
  }

  let aFiles = scannerPr.files
  let bFiles = scannerMain.files

  if (aFiles.length !== bFiles.length) {
    throw new Error(`Mismatch in file count: ${aFiles.length} vs ${bFiles.length}`)
  }

  for (let i = 0; i < aFiles.length; i++) {
    if (aFiles[i] !== bFiles[i]) {
      throw new Error(`Mismatch in file at index ${i}: ${aFiles[i]} vs ${bFiles[i]}`)
    }
  }

  console.log('Scanned', aFiles.length, 'files')
  console.log('Extracted', aCandidates.length, 'candidates')
  console.log('Base =', base)
  console.log()
}

summary(() => {
  boxplot(() => {
    bench('PR (build, .scan()))', function* () {
      yield {
        [0]() {
          return new ScannerPr({ sources })
        },
        bench(scanner: ScannerPr) {
          do_not_optimize(scanner.scan())
        },
      }
    })

    bench('main (build, .scan()))', function* () {
      yield {
        [0]() {
          return new ScannerMain({ sources })
        },
        bench(scanner: ScannerMain) {
          do_not_optimize(scanner.scan())
        },
      }
    })
  })
})

summary(() => {
  boxplot(() => {
    bench('PR (build, .scan() + .files)', function* () {
      yield {
        [0]() {
          return new ScannerPr({ sources })
        },
        bench(scanner: ScannerPr) {
          do_not_optimize(scanner.scan())
          do_not_optimize(scanner.files)
        },
      }
    })

    bench('main (build, .scan() + .files)', function* () {
      yield {
        [0]() {
          return new ScannerMain({ sources })
        },
        bench(scanner: ScannerMain) {
          do_not_optimize(scanner.scan())
          do_not_optimize(scanner.files)
        },
      }
    })
  })
})

summary(() => {
  boxplot(() => {
    bench('PR (watch, .scan()))', function* () {
      yield {
        bench() {
          do_not_optimize(scannerPr.scan())
        },
      }
    })

    bench('main (watch, .scan()))', function* () {
      yield {
        bench() {
          do_not_optimize(scannerMain.scan())
        },
      }
    })
  })
})

summary(() => {
  boxplot(() => {
    bench('PR (watch, .scan() + .files)', function* () {
      yield {
        bench() {
          do_not_optimize(scannerPr.scan())
          do_not_optimize(scannerPr.files)
        },
      }
    })

    bench('main (watch, .scan() + .files)', function* () {
      yield {
        bench() {
          do_not_optimize(scannerMain.scan())
          do_not_optimize(scannerMain.files)
        },
      }
    })
  })
})

await run()
```

</details>

#### tailwindcss.com codebase

```
Scanned 462 files
Extracted 13200 candidates
Base = /Users/robin/github.com/tailwindlabs/tailwindcss.com

clk: ~3.09 GHz
cpu: Apple M1 Max
runtime: bun 1.3.3 (arm64-darwin)
```

In these benchmarks the `PR` one is consistently faster than `main`.
It's not by a lot but that's mainly because the codebase itself isn't
that big. It is a codebase with _a lot_ of candidates though, but not
that many files.

The candidate extraction was already pretty fast, so the wins here
mainly come from avoiding re-walking the file system when accessing
`.files`, and from delaying `mtime` tracking until after the initial
scan.

**Single initial build**:

It's not a lot, but it's a bit faster. This is due to avoiding tracking
the `mtime` values initially and making some small optimizations related
to the struct size and allocations.

```
benchmark                     avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------- -------------------------------
PR (build, .scan()))            22.87 ms/iter  23.28 ms               █
                        (21.49 ms … 25.68 ms)  23.98 ms     ▂  ▂   ▂  █  ▂▂
                      (832.00 kb …   2.69 mb)   1.41 mb ▆▆▆▆█▆▆█▁▆▆█▁▁█▁▆██▁▆

main (build, .scan()))          25.67 ms/iter  26.12 ms █   █      █
                        (24.54 ms … 27.74 ms)  27.06 ms █   █ █    ███
                      (432.00 kb …   2.78 mb) 996.00 kb ██▁████▁█▁████▁█▁▁█▁█

                               ┌                                            ┐
                               ╷    ┌─────┬──┐     ╷
          PR (build, .scan())) ├────┤     │  ├─────┤
                               ╵    └─────┴──┘     ╵
                                                        ╷  ┌─────┬──┐       ╷
        main (build, .scan()))                          ├──┤     │  ├───────┤
                                                        ╵  └─────┴──┘       ╵
                               └                                            ┘
                               21.49 ms           24.28 ms           27.06 ms

summary
  PR (build, .scan()))
   1.12x faster than main (build, .scan()))
```


**Single initial build + accessing `.files`**:

We don't have to re-walk the entire file system even if we're just
dealing with ~462 scanned files.

```
benchmark                     avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------- -------------------------------
PR (build, .scan() + .files)    22.54 ms/iter  22.99 ms █      ▂
                        (21.41 ms … 25.86 ms)  24.26 ms █ ▅  ▅▅█▅  ▅▅
                      (368.00 kb …   2.05 mb) 853.00 kb █▇█▇▇████▇▇██▁▁▇▁▁▇▁▇

main (build, .scan() + .files)  32.15 ms/iter  32.17 ms  █  ▂
                        (30.78 ms … 36.22 ms)  35.75 ms ▅█ ▅█  ▅
                      (400.00 kb …   2.45 mb) 952.00 kb ██▁██▇▇█▇▁▁▁▁▁▁▁▁▁▁▁▇

                               ┌                                            ┐
                               ╷┌──┬┐   ╷
  PR (build, .scan() + .files) ├┤  │├───┤
                               ╵└──┴┘   ╵
                                                            ╷┌───┬          ╷
main (build, .scan() + .files)                              ├┤   │──────────┤
                                                            ╵└───┴          ╵
                               └                                            ┘
                               21.41 ms           28.58 ms           35.75 ms

summary
  PR (build, .scan() + .files)
   1.43x faster than main (build, .scan() + .files)
```


**Watch/dev mode, only scanning**:

This now switches to the parallel walker, but since it's not a super big
codebase we don't see a huge win here yet.

```
benchmark                     avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------- -------------------------------
PR (watch, .scan()))             6.85 ms/iter   7.22 ms   █▄
                          (6.34 ms … 7.94 ms)   7.91 ms  ▄██▃
                      ( 64.00 kb … 688.00 kb) 452.82 kb ▃████▆▂▂▁▂▁▂▁▁▁▅█▆▃▅▃

main (watch, .scan()))           7.92 ms/iter   8.08 ms   █ █  ▃ █▃▃
                          (7.41 ms … 8.71 ms)   8.68 ms   █▆█▆▃█████
                      (  0.00  b …  64.00 kb)  19.20 kb ▆▄██████████▆▁▆▆█▄▄▄▆

                               ┌                                            ┐
                               ╷  ┌──────┬──────┐            ╷
          PR (watch, .scan())) ├──┤      │      ├────────────┤
                               ╵  └──────┴──────┘            ╵
                                                    ╷    ┌───┬──┐           ╷
        main (watch, .scan()))                      ├────┤   │  ├───────────┤
                                                    ╵    └───┴──┘           ╵
                               └                                            ┘
                               6.34 ms            7.51 ms             8.68 ms

summary
  PR (watch, .scan()))
   1.16x faster than main (watch, .scan()))
```

**Watch/dev mode, scanning + accessing `.files`**:

Again we avoid re-walking the entire file system when accessing
`.files`.

```
benchmark                     avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------- -------------------------------
PR (watch, .scan() + .files)    12.10 ms/iter  12.74 ms █ █     █ █ ▃▃▃
                        (10.69 ms … 13.89 ms)  13.81 ms █ █▂▂▂ ▇█▂█▂███▇
                      (128.00 kb …  10.73 mb)   5.23 mb █▆████▁█████████▆▆▆▆▆

main (watch, .scan() + .files)  14.44 ms/iter  14.74 ms   █
                        (13.93 ms … 15.33 ms)  15.18 ms   ███▅ █ ▅   ▅
                      ( 16.00 kb …  80.00 kb)  39.51 kb █▅████▁███▅▁█████▅▁▅▅

                               ┌                                            ┐
                               ╷      ┌──────┬──────┐         ╷
  PR (watch, .scan() + .files) ├──────┤      │      ├─────────┤
                               ╵      └──────┴──────┘         ╵
                                                               ╷ ┌───┬──┐   ╷
main (watch, .scan() + .files)                                 ├─┤   │  ├───┤
                                                               ╵ └───┴──┘   ╵
                               └                                            ┘
                               10.69 ms           12.93 ms           15.18 ms

summary
  PR (watch, .scan() + .files)
   1.19x faster than main (watch, .scan() + .files)
```

#### Synthetic 5000 files codebase

Based on the instructions from #19616 I created a codebase with 5000
files. Each file contains a `flex` class and a unique class like
`content-['/path/to/file']` to ensure we have a decent amount of unique
candidates.

You can test the script yourself by running this:
```
mkdir -p fixtures/app-5000/src/components/{auth,dashboard,settings,profile,notifications,messages,search,navigation,footer,sidebar}/sub{001..500} && for dir in fixtures/app-5000/src/components/*/sub*; do echo "export const Component = () => <div className=\"flex content-['$dir']\">test</div>" > "$dir/index.tsx"; done && find fixtures/app-5000/src/components -type f | wc -lc
```

```
Scanned 5000 files
Extracted 5005 candidates
Base = /Users/robin/github.com/RobinMalfait/playground/scanner-benchmarks/fixtures/app-5000

clk: ~3.08 GHz
cpu: Apple M1 Max
runtime: bun 1.3.3 (arm64-darwin)
```

**Single initial build**:

As expected not a super big win here because it's a single build. But
there is a noticeable improvement.

```
benchmark                     avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------- -------------------------------
PR (build, .scan()))           217.27 ms/iter 211.97 ms              █
                      (205.99 ms … 289.53 ms) 214.33 ms ▅      ▅ ▅▅▅▅█▅▅    ▅
                      (  3.34 mb …   4.25 mb)   3.72 mb █▁▁▁▁▁▁█▁███████▁▁▁▁█

main (build, .scan()))         249.26 ms/iter 239.88 ms               █
                      (231.51 ms … 381.66 ms) 241.01 ms ▅    ▅ ▅▅  ▅  █▅  ▅▅▅
                      (  4.22 mb …   4.78 mb)   4.49 mb █▁▁▁▁█▁██▁▁█▁▁██▁▁███

                               ┌                                            ┐
                               ╷    ┌─────╷──┬
          PR (build, .scan())) ├────┤     ┤  │
                               ╵    └─────╵──┴
                                                                ╷   ┌───────╷
        main (build, .scan()))                                  ├───┤       ┤
                                                                ╵   └───────╵
                               └                                            ┘
                               205.99 ms         223.50 ms          241.01 ms

summary
  PR (build, .scan()))
   1.15x faster than main (build, .scan()))
```

**Single initial build + accessing `.files`**:

Now things are getting interesting. Almost a 2x speedup by avoiding
re-walking the file system when accessing `.files`.

```
benchmark                     avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------- -------------------------------
PR (build, .scan() + .files)   216.35 ms/iter 214.53 ms █ █ █
                      (211.00 ms … 242.64 ms) 221.45 ms █ █▅█ ▅▅      ▅     ▅
                      (  2.97 mb …   4.47 mb)   3.97 mb █▁███▁██▁▁▁▁▁▁█▁▁▁▁▁█

main (build, .scan() + .files) 414.79 ms/iter 406.05 ms     ██
                      (396.72 ms … 542.30 ms) 413.69 ms ▅   ██▅ ▅▅ ▅ ▅      ▅
                      (  5.19 mb …   6.03 mb)   5.63 mb █▁▁▁███▁██▁█▁█▁▁▁▁▁▁█

                               ┌                                            ┐
                               ┌┬╷
  PR (build, .scan() + .files) ││┤
                               └┴╵
                                                                        ╷┌──╷
main (build, .scan() + .files)                                          ├┤  ┤
                                                                        ╵└──╵
                               └                                            ┘
                               211.00 ms         312.34 ms          413.69 ms

summary
  PR (build, .scan() + .files)
   1.92x faster than main (build, .scan() + .files)
```

**Watch/dev mode, only scanning**:

This is where we see bigger wins because now we're using the parallel
walker.

```
benchmark                     avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------- -------------------------------
PR (watch, .scan()))            76.26 ms/iter  77.41 ms                     █
                        (73.56 ms … 79.02 ms)  77.81 ms ▅   ▅  ▅ ▅▅ ▅▅▅   ▅ █
                      (  2.53 mb …   5.52 mb)   3.06 mb █▁▁▁█▁▁█▁██▁███▁▁▁█▁█

main (watch, .scan()))         166.71 ms/iter 165.14 ms █  █
                      (161.49 ms … 198.26 ms) 168.99 ms █ ▅█ ▅▅▅  ▅  ▅      ▅
                      (  1.08 mb …   2.72 mb)   1.24 mb █▁██▁███▁▁█▁▁█▁▁▁▁▁▁█

                               ┌                                            ┐
                               ╷┬┐
          PR (watch, .scan())) ├││
                               ╵┴┘
                                                                        ╷┌─┬╷
        main (watch, .scan()))                                          ├┤ │┤
                                                                        ╵└─┴╵
                               └                                            ┘
                               73.56 ms          121.28 ms          168.99 ms

summary
  PR (watch, .scan()))
   2.19x faster than main (watch, .scan()))
```

**Watch/dev mode, scanning + accessing `.files`**:

This is the biggest win of them all because we have all the benefits
combined:

1. Avoiding re-walking the file system when accessing `.files`
2. Using the parallel walker for faster file system walking

```
benchmark                     avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------- -------------------------------
PR (watch, .scan() + .files)    84.04 ms/iter  84.84 ms            █
                        (80.96 ms … 87.53 ms)  87.27 ms ▅▅   ▅▅ ▅▅ █▅ ▅     ▅
                      ( 15.42 mb …  31.34 mb)  22.16 mb ██▁▁▁██▁██▁██▁█▁▁▁▁▁█

main (watch, .scan() + .files) 338.59 ms/iter 353.89 ms   █
                      (321.87 ms … 378.43 ms) 358.70 ms   █   █
                      (  2.39 mb …   2.45 mb)   2.42 mb ███▁▁██▁▁▁▁▁▁▁▁▁▁██▁█

                               ┌                                            ┐
                               ┬┐
  PR (watch, .scan() + .files) ││
                               ┴┘
                                                                      ┌──┬─┐╷
main (watch, .scan() + .files)                                        │  │ ├┤
                                                                      └──┴─┘╵
                               └                                            ┘
                               80.96 ms          219.83 ms          358.70 ms

summary
  PR (watch, .scan() + .files)
   4.03x faster than main (watch, .scan() + .files)
```


## Test plan

1. All existing tests still pass
2. All public APIs remain the same
3. In the benchmarks I'm sharing, I first verify that the candidates
returned and the files returned are the same before and after the
change.
4. Benchmarked against real codebases, and against a synthetic large
codebase (5000 files).

Fixes: #19616
2026-02-17 20:15:20 +01:00
Arsher. {Zach}
f212b0fc22
fix: restore full page reload for watched external files on Vite 7.1+ (#19670)
# PR: Fix @source file changes not triggering full page reload on Vite
7.1+

## Description
This PR addresses issue #19637 where template files (PHP, HTML, Blade,
etc.) watched via the `@source` directive fail to trigger a full page
reload when using Vite 7.1 or newer.

## Root Cause
Vite 7.1 introduced the Environment API, which supersedes the legacy
WebSocket API for HMR. Specifically:
- `server.ws.send` is deprecated/ignored for certain external file
updates in favor of `server.hot.send`.
- The `@tailwindcss/vite` plugin currently collects `ViteDevServer`
instances but lacks a `handleHotUpdate` hook to explicitly trigger
reloads for non-module files added via `addWatchFile`.

## Changes
- Implemented a `handleHotUpdate` hook in the `@tailwindcss/vite`
plugin.
- The hook identifies changes to files that are not part of the standard
Vite module graph (e.g., `.php`, `.html`) but are watched by Tailwind.
- Triggers a `full-reload` using the new `server.hot.send` API if
available (Vite 7.1+), with a fallback to `server.ws.send` for backward
compatibility.

## Verification
- Reproduced the issue in a standalone Vite 7.1.0 project using a mock
plugin with the legacy API.
- Confirmed that the browser fails to reload upon editing a watched
`.php` file.
- Verified that migrating to `server.hot.send` restores the expected
reload behavior.

[ci-all]

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-02-17 20:04:17 +01:00
Robin Malfait
6eb3b32434
Allow multiples of .25 in aspect-* fractions (#19688)
This PR reduces the restrictions of the `aspect-*` utilities when
dealing with fractional values.

Up until now, the numbers had to be positive integers, so `aspect-1/2`
was valid, but `aspect-8.5/11` was not.

This PR allows for any multiple of `.25` as a valid value, so
`aspect-8.5/11` is now valid, but `aspect-8.3/11` is not, this will
still require `aspect-[8.3/11]` arbitrary value syntax to be valid.

This behavior of allowing multiples of `.25` is consistent with other
utilities that handle bare values such as `w-2.5`.

## Test plan

1. Existing tests pass
2. Added a test for `aspect-8.5/11`

Fixes: #19663
Closes: #19680, #19669
2026-02-17 19:32:31 +01:00
Benjamin Bock
d74f8b8392
Add '.jj' to ignored content directories (#19687)
Ignore .jj directory, just like .git, .hg, .svn.

https://www.jj-vcs.dev/latest/

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-02-17 17:09:59 +00:00
Pavan Shinde
99a7c602f2
docs: fix GitHub links to tailwindlabs org (#19686)
## Summary 
Fix outdated GitHub links that still point to
`github.com/tailwindcss/tailwindcss` (old org). This updates references
across the README, contributing guide, and PR template to the canonical
`tailwindlabs/tailwindcss` URLs to avoid redirects/404s and keep repo
metadata consistent

## Test Plan
This change is docs-only.
2026-02-17 13:06:49 +01:00
depfu[bot]
257dc581ba
Update @vitejs/plugin-react 5.1.2 → 5.1.3 (patch) (#19651)
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?




#### ✳️ @​vitejs/plugin-react (5.1.2 → 5.1.3) ·
[Repo](https://github.com/vitejs/vite-plugin-react) ·
[Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
















---
![Depfu
Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[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>
2026-02-09 12:20:45 -05:00
depfu[bot]
4614f5eaa7
Update autoprefixer 10.4.23 → 10.4.24 (patch) (#19642)
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?




#### ✳️ autoprefixer (10.4.23 → 10.4.24) ·
[Repo](https://github.com/postcss/autoprefixer) ·
[Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)



<details>
<summary>Release Notes</summary>
<h4><a
href="https://github.com/postcss/autoprefixer/releases/tag/10.4.24">10.4.24</a></h4>

<blockquote><ul dir="auto">
<li>Made Autoprefixer a little faster (by <a
href="https://bounce.depfu.com/github.com/Cherry">@Cherry</a>).</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="212ba3c189...36692c25e7">See
the full diff on Github</a>. The new version differs by 3 commits:</p>
<ul>
<li><a
href="36692c25e7"><code>Release
10.4.24 version</code></a></li>
<li><a
href="67df014ae1"><code>Update
dependencies</code></a></li>
<li><a
href="032440ee4a"><code>perf:
reduce array allocations (#1542)</code></a></li>
</ul>
</details>












---
![Depfu
Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[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>
2026-02-09 12:20:25 -05:00
Pavan Shinde
8ed67bf551
Fix Tailwind CSS package README GitHub links (#19644)
<!--

👋 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/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md

-->

## Summary
These package READMEs referenced the wrong GitHub org
(tailwindcss/tailwindcss) and outdated branches (master/next) for common
project links.
Update them to point at tailwindlabs/tailwindcss on main for releases,
license, discussions, and contributing docs.
<!--

Provide a summary of the issue and the changes you're making. How does
your change solve the problem?

-->

## Test plan
Docs-only change: No test required.
<!--

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.

-->
2026-02-07 19:26:38 -05:00
Pavan Shinde
e73c2472fd
Fix README links to use main branch (#19641)
<!--

👋 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/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md

-->

## Summary
The README still referenced the `next` branch for the CI badge and
contributing docs link, but the `next` branch no longer exists upstream.
This updates those URLs to point to `main` so the badge and contributing
docs link work correctly.
<!--

Provide a summary of the issue and the changes you're making. How does
your change solve the problem?

-->

## Test plan
N/A, docs only change 
<!--

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.

-->
2026-02-06 16:51:07 +01:00
Pavan Shinde
ddb47e84da
Fix README license badge link (#19638)
<!--

👋 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/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md

-->

## Summary
The README license badge linked to a non-existent `master` branch (and
the wrong org).
This updates the link to the current location:
tailwindlabs/tailwindcss/blob/main/LICENSE.
<!--

Provide a summary of the issue and the changes you're making. How does
your change solve the problem?

-->

## Test plan
- Opened `README.md` and clicked the license badge/link to confirm it
resolves to the LICENSE file on the `main` branch.
<!--

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.

-->
2026-02-05 10:59:10 -05:00
Robin Malfait
23d01bc474
only notify Discord once
If we're in a matrix, and one of the jobs fail, then we only want to
notify Discord once instead of for each failure.
2026-02-04 13:08:20 +01:00
Robin Malfait
1638f35c3a
Bump dependencies (#19608)
This PR bumps a bunch of dependencies. This also moves a few
dependencies that we use in multiple packages to the pnpm catalog.

Closes: #19603, #19604, #19576, #19575, #19573, #19565, #19547, #19546,
#19545, #19609, #19581, #19620, #19619

- #19603
- #19604
- #19576
- #19575
- #19573
- #19565
- #19547
- #19546
- #19545
- #19609
- #19581
- https://github.com/tailwindlabs/tailwindcss/pull/19620 
- #19620
- #19619

## Test Plan

All tests in CI should still pass. [ci-all]
2026-02-04 12:38:50 +01:00
Robin Malfait
df96ea5eba
Fix infinite loop when using @variant inside @custom-variant that points to another @custom-variant (#19633)
This PR fixes an infinite loop when you use a `@variant` inside of a
`@custom-variant`, where the `@variant` used is another
`@custom-variant`.

The issue stems from the fact that a `@custom-variant` can use a `@slot`
that we have to replace with the proper AST nodes. However in this
setup, the AST nodes will include a `@slot` node as well, which causes
us to replace the `@slot` again, and so on, causing an infinite loop.

```css
@custom-variant a {
  @slot;
}

@custom-variant b {
  @variant a {
    @slot;
  }
}
```

The solution here is to replace the `@slot` nodes and then skip walking
the nodes that were just inserted. This does mean that we end up with a
`@slot` node in the final AST but that's not a real issue because that
will get replaced later when handling the next `@custom-variant`.

## Test plan

1. Existing tests still pass
2. Added a regression test to ensure that the infinite loop does not
happen anymore
3. Added additional tests to ensure that the behavior is correct

Thanks @wongjn for your initial debugging help and providing a test case
as well!

Fixes: #19618
2026-02-02 14:42:31 +01:00