## 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 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 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.
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
Fixes: #19481
This PR improves the Ruby extractor to better handle strict locals. We
recently introduced skipping comments in the Ruby extractor (PR #19243
for #19239) by ignoring comments that start with `#` until the end of
the line.
Strict locals are implemented like this:
```ruby
<%# locals: (css: "text-amber-600") %>
```
Notice the `#` after the `<%`, we considered this a comment and ignored
it.
This PR changes that behavior slightly where we skip comments that are
preceded by `%`. This means that `<%# anything here _will_ be scanned
%>`. This should solve the strict locals case, and normal comments will
still be skipped.
We can be more strict in the future if needed, but I think that this
should be a good solution for both scenarios.
### Test plan
1. Added a test to ensure we extract candidates in strict locals
2. Added a regression test for issue #19239 where we introduced skipping
comments in the Ruby extractor
3. Other existing tests are still passing
We can also verify the extracted candidates:
(it's subtle, but you can see that the class is being extracted now)
<img width="1187" height="1376" alt="image"
src="https://github.com/user-attachments/assets/74bbfd79-9db4-4a5b-bd8d-25f1565c6bfd"
/>
This PR adds support for Maud templates in Rust.
We already had some pre-processing for Rust but for Leptos `class:`
syntax. This PR now added a dedicated Rust pre-processor that handles
Leptos and Maud syntax.
We only start pre-processing Maud templates if the Rust file includes
the `html!` macro.
## Test plan
Looking at the extractor, you can see that we now do extract the proper
classes in Maud templates:
<img width="1076" height="1856" alt="image"
src="https://github.com/user-attachments/assets/e649e1de-289e-466f-8fab-44a938a47dd5"
/>
Fixes: #18984
This PR fixes an issue where the `'` syntax in ClojureScript was not
handled properly, resulting in missing extracted classes.
This PR now supports the following ClojureScript syntaxes:
```cljs
; Keyword
(print 'text-red-500)
; List
(print '(flex flex-col underline))
; Vector
(print '[flex flex-col underline])
```
### Test plan
1. Added regression tests
2. Verified that we extract classes correctly now in various scenarios:
Top is before, bottom is with this PR:
<img width="1335" height="1862" alt="image"
src="https://github.com/user-attachments/assets/746aa073-25f8-41f8-b71c-ba83a33065aa"
/>
Fixes: #18882
This PR fixes an issue where `.hdr` files were scanned for candidates
even though it's a binary file.
As a workaround today, you could use:
```css
@source not "**/*.hdr";
```
To ignore `.hdr` files, but let's bake it in by default instead.
Fixes: #18733
## Summary
Slang is basically a Slim template language for Crystal language, so the
very same Slim parser works fine.
Slim template: https://github.com/slim-template/slim
Slang template: https://github.com/jeromegn/slang
## Test plan
Create a simple slang file with some tailwind-css and check if the CSS
is being extracted:
```slim
doctype html
html
head
title This is a title
body.min-h-screen
header.stick.top-0.z-10
section.max-w-4xl.mx-auto.p-4.flex.items-center.justify-between
h1.text-3xl.font-medium This is a slang file
```
To test it, get any slim template, rename the extension to .slang
Fixes#17851
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Fixes#18431
The `~W(…)` syntax in Elixir is a _sigil_. It's basically another way to
write strings, arrays of strings, etc… The syntax lets you use a handful
of surrounding brackets like `~W(…)`, `~W[…]`, `~W{…}`, `~W"…"`, etc… to
let you write lists without necessarily having to escape characters.
In v3 our extractor was able to pick these up but in v4 Oxide does not.
I've added a preprocessor for Elixir files so we can modify the code
before our main extractor sees it.
Now things like this: `~W(text-white bg-gray-600)` will get turned into
` ~W text-white bg-gray-600 ` which can easily be processed by our
extractor.
The sigils we support are:
- `~s` / `~S` (strings)
- `~w` / `~W` (word lists)
- `~c` / `~C` (charlists)
We're specifically detecting the use of `(…)`, `[…]`, and `{…}` as using
quotes already works today.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
## Summary
In a form like,
```clojure
(if condition :bg-white :bg-black)
```
`:bg-black` will fail to extract, while `:bg-white` is extracted as
expected. This PR fixes this case, implements more comprehensive
candidate filtering, and supersedes a previous PR.
Having recently submitted a PR for handling another special case with
Clojure keywords (the presence of `:` inside of keywords), I thought it
best to invert the previous strategy: Instead of handling special cases
one by one, consume keywords according to the Clojure reader spec.
Consume nothing else, other than strings.
Because of this, this PR is a tad more invasive rather than additive,
for which I apologize. The strategy is this:
- Strings begin with a `"` and ends with an unescaped `"`. Consume
everything between these delimiters (existing case).
- Keywords begin with `:`, and end with whitespace, or one out of a
small set of specific reserved characters. Everything else is a valid
character in a keyword. Consume everything between these delimiters, and
apply the class splitting previously contained in the outer loop. My
previous special case handling of `:` inside of keywords in #18338 is
now redundant (and is removed), as this is a more general solution.
- Discard _everything else_.
I'm hoping that a strategy that is based on Clojure's definition of
strings and keywords will pre-empt any further issues with edge cases.
Closes#18344.
## Test plan
- Added failing tests.
- `cargo test` -> failure
- Added fix
- `cargo test` -> success
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
## Summary
Taking a shot at fixing my own complaint from #18336.
Added failing test and fix for extracting classes containing a
pseudo-class from Clojure keywords. Eg., `:hover:text` ->
`"hover:text"`. This would previously produce `["hover", "text"]`.
## Test plan
- Add a failing test.
- Verify that it fails with `cargo test`.
- Add fix.
- Verify that `cargo test` has no further complaints.
ATT: @RobinMalfait
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This PR fixes a Haml pre-processing issue where a crash occurs if there
is no trailing `\n` at the end of the file and the code before it was
considered Ruby code.
This happens in situations where Ruby code was used. E.g.:
```
- index = 0
- index += 1
```
In this situation when we see the `-` on the second line, then we will
find the whole indented block and parse it as Ruby code instead. The
block ands at the last `\n`, but since we reach the end of the file,
there is no `\n`. Right now we incorrectly reset the internal cursor to
the last known `\n` position. This means that the `start` of the Ruby
block will be _after_ the `end` of the Ruby block and the Haml parser
will crash.
To solve this, once we reach the end of the file, we don't reset the
cursor to the wrong position.
Fixes:
https://github.com/tailwindlabs/tailwindcss/issues/17379#issuecomment-2910108646
## Test plan
1. Added a regression test that did fail before the fix, and doesn't
anymore
<!--
👋 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
-->
Fixes#18092
## Summary
Using the Svelte preprocessor for Rust files we can support Leptos
`class:` attributes syntax.
## Test plan
```
pnpm t
```
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR fixes an issue in the Clojure pre-processor where candidates
including `.` characters were not extracted correctly.
The solution here is to only replace the `.` with a ` ` when the `.` is
not surrounded by numbers. This means that:
```
:.foo.bar
```
Becomes
```
: foo bar
```
But
```
:.gap-1.5.flex
```
Becomes
```
: gap-1.5 flex
```
This way the `gap-1.5` is correctly extracted.
## Test plan
1. Added a test for this case
2. Tested this in the extractor tool as well. Notice how the `gap-1.5`
is correctly extracted here.
<img width="1247" alt="image"
src="https://github.com/user-attachments/assets/f5dd2600-5c5e-4ad8-88af-4e5be44340f5"
/>
Fixes: 17760
This PR fixes a bug where a class like `header_1` wasn't properly
extracted because we didn't allow an `_` before a number.
This PR fixes that by allowing an `_` before a number.
Fixes: #17960
## Test plan
1. Added a test to verify this works
2. Existing tests work
Used the visualizer tool for this to verify that the `header_1` class is
being extracted:
<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/fdc21602-0e2b-4e4e-92a1-19c4f4f5393f"
/>
This PR improves the debug logging by also adding the provided `@source`
to the log file. It will also print the optimized sources (the ones we
expand and mark as `Auto` or `Pattern`).
In the logs, this will look like this:
```
2025-05-09T11:03:32.906478Z INFO tailwindcss_oxide::scanner: Provided sources:
2025-05-09T11:03:32.906544Z INFO tailwindcss_oxide::scanner: Source: PublicSourceEntry { base: "/Users/robin/github.com/RobinMalfait/spreadsheet", pattern: "**/*", negated: false }
2025-05-09T11:03:32.906589Z INFO tailwindcss_oxide::scanner: Optimized sources:
2025-05-09T11:03:32.906595Z INFO tailwindcss_oxide::scanner: Source: Auto { base: "/Users/robin/github.com/RobinMalfait/spreadsheet" }
```
Or if you have more sources:
```
2025-05-09T11:06:54.767546Z INFO tailwindcss_oxide::scanner: Provided sources:
2025-05-09T11:06:54.767660Z INFO tailwindcss_oxide::scanner: Source: PublicSourceEntry { base: "/Users/robin/github.com/RobinMalfait/spreadsheet", pattern: "**/*", negated: false }
2025-05-09T11:06:54.767987Z INFO tailwindcss_oxide::scanner: Source: PublicSourceEntry { base: "/Users/robin/github.com/RobinMalfait/spreadsheet/app", pattern: "./routes/*.{jsx,tsx}", negated: false }
2025-05-09T11:06:54.767992Z INFO tailwindcss_oxide::scanner: Source: PublicSourceEntry { base: "/Users/robin/github.com/RobinMalfait/spreadsheet/app", pattern: "./utils/*.ts", negated: false }
2025-05-09T11:06:54.768450Z INFO tailwindcss_oxide::scanner: Optimized sources:
2025-05-09T11:06:54.768455Z INFO tailwindcss_oxide::scanner: Source: Auto { base: "/Users/robin/github.com/RobinMalfait/spreadsheet" }
2025-05-09T11:06:54.768459Z INFO tailwindcss_oxide::scanner: Source: Pattern { base: "/Users/robin/github.com/RobinMalfait/spreadsheet/app/routes", pattern: "*.jsx" }
2025-05-09T11:06:54.768462Z INFO tailwindcss_oxide::scanner: Source: Pattern { base: "/Users/robin/github.com/RobinMalfait/spreadsheet/app/routes", pattern: "*.tsx" }
2025-05-09T11:06:54.768466Z INFO tailwindcss_oxide::scanner: Source: Pattern { base: "/Users/robin/github.com/RobinMalfait/spreadsheet/app/utils", pattern: "*.ts" }
```
Closes#15452
This PR adds more directories to the list of ignored content dirs. These
are now handled the same as `node_modules`:
- Contents of this directory are ignored by default to avoid scanning
dependency trees
- Explicit `@import`s inside these folders are now treated as
_external_, bypassing any `.gitignore` files.
The new extensions are:
- Version control systems: `.hg`, `.svn`
- Bundler caches: `.venv`, `venv`, `.yarn`
- Framework caches: `.next`, `.turbo`, `.parcel-cache`, `__pycache__`,
`.svelte-kit`
## Test plan
Verified with the repro of #15452 by renaming the ignored directory to
`.venv` and installing a local tarball:
<img width="1283" alt="Screenshot 2025-05-06 at 13 14 55"
src="https://github.com/user-attachments/assets/3265acbb-e121-47b3-ac6c-e174770f8234"
/>
This PR improves the debugability of the scanner when using `DEBUG=*` by
writing to a `tailwindcss-{pid}.log` file in the current working
directory.
It will include all the tracing information from the scanner. This PR
also introduces `Discovering {path}` and `Reading {path}` logs.
- `Discovering {path}` — this is logged when we are traversing the file
system looking for files. We use the `ignore` crate, and log this
information in the `filter_entry` callback. If a file was already
ignored by `.gitignore` files, this won't show up, but it also means
that we will not read it.
- `Reading {path}` — this is when we are actually reading the file so we
can start extracting potential Tailwind CSS classes.
These will give you some insights in what paths are being scanned, and
if we get stuck, where we get stuck.
Also, we are appending to the file. In the log below, you can already
see that a `tailwindcss-<number>.log` file exists already even though it
didn't exist before running the command. This should make it easier to
debug if we get stuck on a specific file/folder because the file will be
populated with information.
There are a few reasons for appending to a file:
1. There is a lot of output, so spamming the stdout/stderr is not ideal
2. If you run the same command again, after changing your `@source`
directives, you could diff the outputs. (although, the timestamps will
be different)
3. When using `DEBUG=*`, a lot of other tools also output debug
information, so writing to a file should make this better.
<details>
<summary>Example log</summary>
```log
2025-05-06T23:13:45.912292Z INFO scan_sources: tailwindcss_oxide::scanner: enter
2025-05-06T23:13:45.912697Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/out.css"
2025-05-06T23:13:45.912716Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/tailwindcss-61347.log"
2025-05-06T23:13:45.912748Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app"
2025-05-06T23:13:45.912786Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/test"
2025-05-06T23:13:45.912814Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/test/utils.ts"
2025-05-06T23:13:45.912851Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/utils"
2025-05-06T23:13:45.912873Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/utils/flatten.ts"
2025-05-06T23:13:45.912884Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/utils/matrix.ts"
2025-05-06T23:13:45.912893Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/utils/default-map.ts"
2025-05-06T23:13:45.912904Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/tailwind.css"
2025-05-06T23:13:45.912914Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/root.tsx"
2025-05-06T23:13:45.912936Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain"
2025-05-06T23:13:45.912962Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/expression.ts"
2025-05-06T23:13:45.912972Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/walk-ast.ts"
2025-05-06T23:13:45.912995Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature"
2025-05-06T23:13:45.913019Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/parser.test.ts"
2025-05-06T23:13:45.913029Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/validate.test.ts"
2025-05-06T23:13:45.913039Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/tokenizer.ts"
2025-05-06T23:13:45.913048Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/validate.ts"
2025-05-06T23:13:45.913058Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/parser.ts"
2025-05-06T23:13:45.913067Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/tokenizer.test.ts"
2025-05-06T23:13:45.913077Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/ast.ts"
2025-05-06T23:13:45.913086Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/version-control.ts"
2025-05-06T23:13:45.913095Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/tokenizer.ts"
2025-05-06T23:13:45.913105Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/type-checker.test.ts"
2025-05-06T23:13:45.913121Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/evaluation-result.ts"
2025-05-06T23:13:45.913505Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/tmp.test.ts"
2025-05-06T23:13:45.913514Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/version-control.test.ts"
2025-05-06T23:13:45.913523Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/tokenizer.test.ts"
2025-05-06T23:13:45.913531Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/evaluation.ts"
2025-05-06T23:13:45.913554Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions"
2025-05-06T23:13:45.913583Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/sequence.test.ts"
2025-05-06T23:13:45.913592Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/date.ts"
2025-05-06T23:13:45.913601Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/lookup.test.ts"
2025-05-06T23:13:45.913613Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/statistics.ts"
2025-05-06T23:13:45.913622Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/intrinsics.test.ts"
2025-05-06T23:13:45.913631Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/math.ts"
2025-05-06T23:13:45.913640Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/engineering.ts"
2025-05-06T23:13:45.913648Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/types.test.ts"
2025-05-06T23:13:45.913656Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/text.ts"
2025-05-06T23:13:45.913665Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/text.test.ts"
2025-05-06T23:13:45.913673Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/intrinsics.ts"
2025-05-06T23:13:45.913681Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/logic.ts"
2025-05-06T23:13:45.913689Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/date.test.ts"
2025-05-06T23:13:45.913697Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/types.ts"
2025-05-06T23:13:45.913705Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/logic.test.ts"
2025-05-06T23:13:45.913713Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/lookup.ts"
2025-05-06T23:13:45.913720Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/sequence.ts"
2025-05-06T23:13:45.913728Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/engineering.test.ts"
2025-05-06T23:13:45.913741Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/index.ts"
2025-05-06T23:13:45.913749Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/math.test.ts"
2025-05-06T23:13:45.913757Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/statistics.test.ts"
2025-05-06T23:13:45.913783Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/__snapshots__"
2025-05-06T23:13:45.913817Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/spreadsheet.test.ts"
2025-05-06T23:13:45.913826Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/type-checker.ts"
2025-05-06T23:13:45.913833Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/function-utils.ts"
2025-05-06T23:13:45.913841Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/spreadsheet.ts"
2025-05-06T23:13:45.913849Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/expression.test.ts"
2025-05-06T23:13:45.913857Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/evaluation.test.ts"
2025-05-06T23:13:45.913879Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/routes"
2025-05-06T23:13:45.913896Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/app/routes/_index.tsx"
2025-05-06T23:13:45.914172Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/README.md"
2025-05-06T23:13:45.914197Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/public"
2025-05-06T23:13:45.914228Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/public/fonts"
2025-05-06T23:13:45.914268Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/package.json"
2025-05-06T23:13:45.914289Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/scripts"
2025-05-06T23:13:45.914310Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/scripts/generate-documentation.ts"
2025-05-06T23:13:45.914332Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/.github"
2025-05-06T23:13:45.914383Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/.github/workflows"
2025-05-06T23:13:45.914410Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/.github/workflows/ci.yml"
2025-05-06T23:13:45.914420Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/tsconfig.json"
2025-05-06T23:13:45.914455Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/vite.config.ts"
2025-05-06T23:13:45.914486Z INFO scan_sources: tailwindcss_oxide::scanner: Discovering "/Users/robin/github.com/RobinMalfait/spreadsheet/biome.json"
2025-05-06T23:13:45.914512Z INFO scan_sources: tailwindcss_oxide::scanner: exit
2025-05-06T23:13:45.914515Z INFO extract_candidates: tailwindcss_oxide::scanner: enter
2025-05-06T23:13:45.914518Z INFO extract_candidates:read_all_files: tailwindcss_oxide::scanner: enter
2025-05-06T23:13:45.914524Z INFO extract_candidates:read_all_files: tailwindcss_oxide::scanner: Reading 58 file(s)
2025-05-06T23:13:45.914808Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/tailwindcss-61347.log"
2025-05-06T23:13:45.914990Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/routes/_index.tsx"
2025-05-06T23:13:45.915138Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/.github/workflows/ci.yml"
2025-05-06T23:13:45.915140Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/statistics.test.ts"
2025-05-06T23:13:45.915145Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/tokenizer.ts"
2025-05-06T23:13:45.915163Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/tsconfig.json"
2025-05-06T23:13:45.915153Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/engineering.ts"
2025-05-06T23:13:45.915226Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/utils/matrix.ts"
2025-05-06T23:13:45.915229Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/walk-ast.ts"
2025-05-06T23:13:45.915372Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/test/utils.ts"
2025-05-06T23:13:45.915578Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/ast.ts"
2025-05-06T23:13:45.915599Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/validate.ts"
2025-05-06T23:13:45.915637Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/version-control.ts"
2025-05-06T23:13:45.915647Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/parser.ts"
2025-05-06T23:13:45.915657Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/vite.config.ts"
2025-05-06T23:13:45.915680Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/types.test.ts"
2025-05-06T23:13:45.915681Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/utils/flatten.ts"
2025-05-06T23:13:45.915701Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/parser.test.ts"
2025-05-06T23:13:45.915706Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/README.md"
2025-05-06T23:13:45.915691Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/utils/default-map.ts"
2025-05-06T23:13:45.915701Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/spreadsheet.test.ts"
2025-05-06T23:13:45.915730Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/package.json"
2025-05-06T23:13:45.915753Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/tokenizer.ts"
2025-05-06T23:13:45.915731Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/biome.json"
2025-05-06T23:13:45.915787Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/validate.test.ts"
2025-05-06T23:13:45.915822Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/signature/tokenizer.test.ts"
2025-05-06T23:13:45.915826Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/text.ts"
2025-05-06T23:13:45.915885Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/root.tsx"
2025-05-06T23:13:45.915885Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/types.ts"
2025-05-06T23:13:45.915886Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/type-checker.ts"
2025-05-06T23:13:45.915990Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/type-checker.test.ts"
2025-05-06T23:13:45.915995Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/text.test.ts"
2025-05-06T23:13:45.915998Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/logic.ts"
2025-05-06T23:13:45.916000Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/intrinsics.ts"
2025-05-06T23:13:45.916024Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/sequence.ts"
2025-05-06T23:13:45.916056Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/function-utils.ts"
2025-05-06T23:13:45.916058Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/scripts/generate-documentation.ts"
2025-05-06T23:13:45.916070Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/expression.ts"
2025-05-06T23:13:45.916075Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/logic.test.ts"
2025-05-06T23:13:45.916063Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/date.test.ts"
2025-05-06T23:13:45.916119Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/evaluation-result.ts"
2025-05-06T23:13:45.916120Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/index.ts"
2025-05-06T23:13:45.916148Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/expression.test.ts"
2025-05-06T23:13:45.916152Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/engineering.test.ts"
2025-05-06T23:13:45.916193Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/lookup.ts"
2025-05-06T23:13:45.916219Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/tmp.test.ts"
2025-05-06T23:13:45.916228Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/tokenizer.test.ts"
2025-05-06T23:13:45.916245Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/evaluation.test.ts"
2025-05-06T23:13:45.916256Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/spreadsheet.ts"
2025-05-06T23:13:45.916253Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/lookup.test.ts"
2025-05-06T23:13:45.916267Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/sequence.test.ts"
2025-05-06T23:13:45.916287Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/math.test.ts"
2025-05-06T23:13:45.916286Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/version-control.test.ts"
2025-05-06T23:13:45.916317Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/intrinsics.test.ts"
2025-05-06T23:13:45.916323Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/evaluation.ts"
2025-05-06T23:13:45.916354Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/statistics.ts"
2025-05-06T23:13:45.916562Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/date.ts"
2025-05-06T23:13:45.916609Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/domain/functions/math.ts"
2025-05-06T23:13:45.916676Z INFO extract_candidates:read_all_files: tailwindcss_oxide::scanner: exit
2025-05-06T23:13:45.916682Z INFO extract_candidates:parse_all_blobs: tailwindcss_oxide::scanner: enter
2025-05-06T23:13:45.916688Z INFO extract_candidates:parse_all_blobs:extract: tailwindcss_oxide::scanner: enter
2025-05-06T23:13:45.918271Z INFO extract_candidates:parse_all_blobs:extract: tailwindcss_oxide::scanner: exit
2025-05-06T23:13:45.918282Z INFO extract_candidates:parse_all_blobs: tailwindcss_oxide::scanner: exit
2025-05-06T23:13:45.918286Z INFO extract_candidates:read_all_files: tailwindcss_oxide::scanner: enter
2025-05-06T23:13:45.918288Z INFO extract_candidates:read_all_files: tailwindcss_oxide::scanner: Reading 2 file(s)
2025-05-06T23:13:45.918315Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/app/tailwind.css"
2025-05-06T23:13:45.918504Z INFO tailwindcss_oxide::scanner: Reading "/Users/robin/github.com/RobinMalfait/spreadsheet/out.css"
2025-05-06T23:13:45.918512Z INFO extract_candidates:read_all_files: tailwindcss_oxide::scanner: exit
2025-05-06T23:13:45.918519Z INFO extract_candidates:extract_css_variables: tailwindcss_oxide::scanner: enter
2025-05-06T23:13:45.918522Z INFO extract_candidates:extract_css_variables:extract: tailwindcss_oxide::scanner: enter
2025-05-06T23:13:45.918635Z INFO extract_candidates:extract_css_variables:extract: tailwindcss_oxide::scanner: exit
2025-05-06T23:13:45.918640Z INFO extract_candidates:extract_css_variables: tailwindcss_oxide::scanner: exit
2025-05-06T23:13:45.919059Z INFO extract_candidates: tailwindcss_oxide::scanner: exit
```
</details>
We also output where we are writing the file to. This looks like this
when using the CLI:
<img width="1462" alt="image"
src="https://github.com/user-attachments/assets/79c2cc95-adea-4bbd-a4f1-101de45726f5"
/>
Last but not least, this also ignores `.log` files by default
## Test plan
Ran the CLI (but you can use any tool real, since this is implemented in
Oxide) with the `DEBUG=*` flag.
The file generated, looks like the example I shared above.
This PR fixes a Windows CI issue due to the recently merged #17846
There might be better ways to solve this, but essentially we want to
make sure we are always dealing with `\n` and not `\r\n`. This PR fixes
that by replacing all `\r\n` with `\n` in the tests.
This PR fixes and improves the HAML extractor by ensuring that whenever
we detect lines or blocks of Ruby code (`-`, `=` and `~`) characters at
the beginning of the line, we treat them as Ruby code.
Fixes: #17813
## Test Plan
1. Existing tests pass
2. Changed 1 existing test which embedded Ruby syntax
3. Added a dedicated test to ensure the HAML file in the linked issue is
parsed correctly
Running this in the internal extractor tool you can see that the
`w-[12px]`, `w-[16px]`, `h-[12px]`, and `h-[16px]` are properly
extracted.
Note: the `mr-12px` is also extracted, but not highlighted because this
is not a valid Tailwind CSS class.
<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/fc5929ca-bc71-47d2-b21b-7abeec86f54d"
/>
This PR ignores `.db` files by default. We already ignored `.sqlite` and
`.sqlite3` files but we didn't ignore `.db` files which is a common
extension for database files as well.
Due to the binary nature of `.db` files, scanning these could result in
hard to debug errors such as:

Resolves https://github.com/tailwindlabs/tailwindcss/issues/17699
GeoJSON files are giant JSON files for geographic data structures and
will never contain Tailwind classes, but because they are often huge
they will slow down the build a lot if scanned.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
We generate a glob to ignore binary extensions that looks something like
this:
```
*.{mp4,pages,exe,…}
```
However, if you have a folder that happens to end in `.pages` for
example, then this folder will be ignored in its entirety.
To solve this, we added a new flag to the `Gitignore` struct so we can
register a bunch of ignore rules that _only_ apply to paths and not
folders.
Fixes: #17569
## Test plan
- Added a unit test
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This PR fixes an issue where the Ruby `%w[…]` syntax causes utilities to
not be properly extracted.
This PR will now ensure that it does get extracted correctly.
Given this input:
```slim
div[
class=%w[bg-blue-500 w-10 h-10]
]
div[
class=%w[w-10 bg-green-500 h-10]
]
```
Before this PR, we extracted everything but the `bg-blue-500`. The
`w-10` was extracted but not because of the second div, but because of
the first one.
Fixes: #17542
## Test plan
1. Added a test to ensure it's working correctly.
Looking at the extractor tool, you can see that it now gets extracted
correctly. Top is before, bottom is with this change.
<img width="1199" alt="image"
src="https://github.com/user-attachments/assets/028d9abd-8917-438c-a423-88ba887b7f97"
/>
This PR is a follow-up PR for:
https://github.com/tailwindlabs/tailwindcss/pull/17433
In the other PR we allow scanning CSS files for extracting usages of CSS
variables. This is important for `.module.css` files that reference
these variables but aren't in the same big AST of the main CSS file.
This PR also makes sure to watch for changes in those registered CSS
files and re-extract the variables when they change.
This PR took a bit longer than expected because I was trying to make
sure that writing to `./dist/out.css` works without infinite-looping
(e.g.: we had issues with this in Tailwind CSS v3 with webpack).
But I couldn't reproduce the issue at all. I did had some code that
tried to detect if the CSS file contained license headers and skip in
(because then it's very likely an output CSS file) but even without it
the tests were fine.
I setup integration tests with `@tailwindcss/cli` itself, and with tools
that use webpack. Added a test for Next.js, and a dedicated webpack test
as well.
Even without tests, locally, I couldn't reproduce an infinite loop due
to changes in an output CSS file...
Eventually dropped the code that tries to detect output CSS files.
One thing to keep in mind is that if you change any of your "main" CSS
files, then we will trigger a full rebuild anyway, so this change is
only required for unrelated CSS files (like CSS module files) that use
CSS variables.
## Test plan
1. Added integration tests for the CLI and Next.js
2. Added new dedicated test for webpack
This PR fixes an issue where CSS variables could be used in CSS modules,
but where never emitted in your final CSS.
Some backstory, when Tailwind CSS v4 came out, we _always_ emitted all
CSS variables whether they were used or not.
Later, we added an optimization where we only emit the CSS variables
that were actually used. The definition of "used" in this case is:
1. Used in your CSS file(s) — (we check the final CSS AST for this)
2. Used _somewhere_ in any of your source files (e.g.: a JavaScript file
accessing a variable)
The issue this PR tries to solve is with the very first point. If you
are using CSS modules, then every CSS file is processed separately. This
is not a choice Tailwind CSS made, but how other build tooling works
(like Vite for example).
To prevent emitting all of Tailwind's Preflight reset and all utilities
per CSS file, you can use the `@reference` directive instead of
repeating `@import "tailwindcss";`. This is explained here:
https://tailwindcss.com/docs/compatibility#explicit-context-sharing
But now we are just _referencing_ them, not emitting them. And since the
CSS module is not connected in any way to the main `index.css` file that
contains the `@import "tailwindcss";` directive, we don't even see the
CSS variables while processing the `index.css` file. (or wherever your
main CSS file is)
This is where point 2 from above comes in. This is a situation where we
rely on the extractor to find the used CSS variables so we can
internally mark them as used.
To finally get to the point of this PR, the extractor only scans
`.html`, `.js`, ... files but not `.css` files. So all the CSS variables
used inside of CSS modules will not be generated.
This PR changes that behavior to also scan `.css` files. But _only_ for
CSS variables (not any other type of class candidate). This is
important, otherwise all your custom `@utility foo {}` definitions would
always mark `foo` as a used class and include it in the CSS which is not
always the case.
On top extracting CSS variables, we will also make sure that the CSS
variables we find are in usage positions (e.g.: `var(--color-red-500)`)
and not in definition positions (e.g.: `--color-red-500: #ff0000;`).
This is important because we only want to emit the variables that are
actually used in the final CSS output.
One future improvement not implemented here, is that technically we will
also extract CSS variables that might not be used if defined in a
`@utility`.
```css
@utility never-used {
color: var(--color-red-500); /* --color-red-500 will be emitted, even if it might not be used */
}
```
Fixes: #16904Fixes: #17429
# Test plan
1. Added a test where CSS variables are defined in `.css` files (and
ignored)
2. Added a test where CSS variables are used in `.css` files (and
included)
Testing on the reproduction defined in #16904, the `.module.css` file
contains a reference to `var(--color-hot-pink)`, but generating a build
shows that the variable definition is not available:
<img width="1630" alt="image"
src="https://github.com/user-attachments/assets/a0d5c37e-6813-4cd5-a677-6c356b5a73d4"
/>
When you run the build again with the changes from this PR, then we _do_
see the definition of the `--color-hot-pink` in the root CSS file:
<img width="2876" alt="image"
src="https://github.com/user-attachments/assets/beab7c11-a31b-4ea4-8235-4849a8e92859"
/>
This PR fixes an extraction issue in Razor files where `@@md:bg-red-500`
can't always be extracted properly. We already convert `@@md:bg-red-500`
to ` @md:bg-red-500` but in certain situations Razor will emit the
double `@@` to the DOM.
A workaround in Razor land would be to write `@("@")md:bg-red-500`
instead. See: https://github.com/dotnet/aspnetcore/issues/38595 But then
we don't extract the `@md:bg-red-500` properly anymore.
This is where this PR comes in, essentially we will pre process the
Razor contents and apply the following replacement internally:
```diff
- @("@")md:bg-red-500
+ @md:bg-red-500
```
Notice that the `)` looks like it's replaced with `@`. This will have a
small side effect later when we get to the testing part.
But this way we properly see the `@md:bg-red-500` class during class
extraction.
> [!WARNING]
> There is technically a bug here because of the replacement with `@`,
because if you now run the `npx @tailwindcss/upgrade@latest` tool, then
we would replace `)md:bg-red-500` if changes are required, not the
`@("@")md:bg-red-500` part. We can try to fix that in this PR but it
seems unlikely that we will actually run into this issue realistically
speaking. I think fixing the actual extraction here is much more
important than the upgrade tooling that could fail _if_ we ever have to
migrate `@md:…` to something else.
Fixes: #17424
## Test plan
1. Added a test to verify the fix
2. Existing tests pass
3. Verified this in the extractor tool, but it looks a tiny bit funky.
Typically we remove characters by replacing it with a space ` `. But
this time, we replace it with some spaces _and_ an `@` character that
didn't exist at that specific position. If you look at the diff above,
you will notice that `)` was replaced with `@`.
That's why in the extractor tool it is highlighted that it could extract
it, but it's just funny looking because it highlights `)md:bg-red-500`
<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/57e6a3ac-bfd5-4cad-a1ce-0039b4d7d9b5"
/>
Was working on another issue and noticed that I wanted these but they
aren't related to any of the issues. So opening a separate PR to do some
internal cleanup.
This PR fixes some issues related to symlinks when using them in the
`@source` directive.
Fixes: #16765Fixes: #16038
## Test plan
1. Added tests to prove this works
- Added a recursive symlink test as well to make sure we don't hang
2. Existing tests still pass
[ci-all]
Fixes#17379
The preprocessor we added to detect embedded languages uses a back
reference and given a long enough file with certain byte / character
patterns it'll cause what appears to be an indefinite hang (might just
be catastrophically exponential backtracking but not sure)
This replaces the one regex w/ back references with two, anchored,
multi-line regexes
Now we search for all the starting & ending delimiters in the file. We
then loop over all the starting delimiters, find the paired ending one,
and preprocess the content inside
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This PR adds a new source detection feature: `@source not "…"`. It can
be used to exclude files specifically from your source configuration
without having to think about creating a rule that matches all but the
requested file:
```css
@import "tailwindcss";
@source not "../src/my-tailwind-js-plugin.js";
```
While working on this feature, we noticed that there are multiple places
with different heuristics we used to scan the file system. These are:
- Auto source detection (so the default configuration or an `@source
"./my-dir"`)
- Custom sources ( e.g. `@source "./**/*.bin"` — these contain file
extensions)
- The code to detect updates on the file system
Because of the different heuristics, we were able to construct failing
cases (e.g. when you create a new file into `my-dir` that would be
thrown out by auto-source detection, it'd would actually be scanned). We
were also leaving a lot of performance on the table as the file system
is traversed multiple times for certain problems.
To resolve these issues, we're now unifying all of these systems into
one `ignore` crate walker setup. We also implemented features like
auto-source-detection and the `not` flag as additional _gitignore_ rules
only, avoid the need for a lot of custom code needed to make decisions.
High level, this is what happens after the now:
- We collect all non-negative `@source` rules into a list of _roots_
(that is the source directory for this rule) and optional _globs_ (that
is the actual rules for files in this file). For custom sources (i.e
with a custom `glob`), we add an allowlist rule to the gitignore setup,
so that we can be sure these files are always included.
- For every negative `@source` rule, we create respective ignore rules.
- Furthermore we have a custom filter that ensures files are only read
if they have been changed since the last time they were read.
So, consider the following setup:
```css
/* packages/web/src/index.css */
@import "tailwindcss";
@source "../../lib/ui/**/*.bin";
@source not "../../lib/ui/expensive.bin";
```
This creates a git ignore file that (simplified) looks like this:
```gitignore
# Auto-source rules
*.{exe,node,bin,…}
*.{css,scss,sass,…}
{node_modules,git}/
# Custom sources can overwrite auto-source rules
!lib/ui/**/*.bin
# Negative rules
lib/ui/expensive.bin
```
We then use this information _on top of your existing `.gitignore`
setup_ to resolve files (i.e so if your `.gitignore` contains rules e.g.
`dist/` this line is going to be added _before_ any of the rules lined
out in the example above. This allows negative rules to allow-list your
`.gitignore` rules.
To implement this, we're rely on the `ignore` crate but we had to make
various changes, very specific, to it so we decided to fork the crate.
All changes are prefixed with a `// CHANGED:` block but here are the
most-important ones:
- We added a way to add custom ignore rules that _extend_ (rather than
overwrite) your existing `.gitignore` rules
- We updated the order in which files are resolved and made it so that
more-specific files can allow-list more generic ignore rules.
- We resolved various issues related to adding more than one base path
to the traversal and ensured it works consistent for Linux, macOS, and
Windows.
## Behavioral changes
1. Any custom glob defined via `@source` now wins over your `.gitignore`
file and the auto-content rules.
- Resolves#16920
3. The `node_modules` and `.git` folders as well as the `.gitignore`
file are now ignored by default (but can be overridden by an explicit
`@source` rule).
- Resolves#17318
- Resolves#15882
4. Source paths into ignored-by-default folders (like `node_modules`)
now also win over your `.gitignore` configuration and auto-content
rules.
- Resolves#16669
5. Introduced `@source not "…"` to negate any previous rules.
- Resolves#17058
6. Negative `content` rules in your legacy JavaScript configuration
(e.g. `content: ['!./src']`) now work with v4.
- Resolves#15943
7. The order of `@source` definitions matter now, because you can
technically include or negate previous rules. This is similar to your
`.gitingore` file.
9. Rebuilds in watch mode now take the `@source` configuration into
account
- Resolves#15684
## Combining with other features
Note that the `not` flag is also already compatible with [`@source
inline(…)`](https://github.com/tailwindlabs/tailwindcss/pull/17147)
added in an earlier commit:
```css
@import "tailwindcss";
@source not inline("container");
```
## Test plan
- We added a bunch of oxide unit tests to ensure that the right files
are scanned
- We updated the existing integration tests with new `@source not "…"`
specific examples and updated the existing tests to match the subtle
behavior changes
- We also added a new special tag `[ci-all]` that, when added to the
description of a PR, causes the PR to run unit and integration tests on
all operating systems.
[ci-all]
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This PR fixes an issue where embedded Slim templates inside of Ruby
files are not pre processed because we pre process based on a file
extension.
This PR also handles embedded SLIM templates using the following syntax:
```rb
slim_template <<~SLIM
.flex
.flex-1
h1 Hello World
.flex-1
p This is a test
SLIM
```
~~As far as I can tell, this is only a Slim template thing and not a
Haml template thing but I could be wrong here. See:
https://viewcomponent.org/guide/templates.html#interpolations~~
The ViewComponent package handles anything that looks like
`{lang}_template`, so the lang here will be used as the pre processing
language for now.
Fixes: #17334
# Test plan
1. Added test for this
2. Existing tests pass
3. Made sure that the snippet from the issue works as expected:
Added an example where we have a `slim_template` and a `svelte_template`
to prove that it embeds based on the language. I also added a
`html_template` with Svelte syntax to really make sure that that
_doesn't_ work.
<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/35564a32-9c46-4b51-bb1f-e02f4ffe8b01"
/>
This PR fixes an issue where a class shorthand in Pug followed by a `(`
is not properly extracted.
```html
<template lang="pug">
.text-sky-600.bg-neutral-900(title="A tooltip") This div has an HTML attribute.
</template>
```
The `text-sky-600` is extracted, but the `bg-neutral-900` is not.
Fixes: #17313
# Test plan
1. Added test to cover this case
2. Existing tests pass (after a few small adjustments due to _more_
extracted candidates, but definitely not _less_)
3. Verified against the original issue (top is before, bottom is this
PR)
<img width="1307" alt="image"
src="https://github.com/user-attachments/assets/68a0529f-63ad-477d-a342-e3f91c5a1690"
/>
We had this exact same bug in Slim
(https://github.com/tailwindlabs/tailwindcss/pull/17278). Since Pug,
Slim and Haml are the only pre processors we have right now with this
dot-separated class notation I also double checked the Haml
pre-processor if this is an issue or not (and it's already covered
there).
<img width="1263" alt="image"
src="https://github.com/user-attachments/assets/c658168b-d124-46c9-9ec0-9697151a57bf"
/>
This PR fixes an issue where using the class shorthand in Slim
templates, followed by an `(` results in the last class being ignored.
E.g.:
```slim
body.border-t-4.p-8(class="#{body_classes}" data-hotwire-native="#{hotwire_native_app?}" data-controller="update-time-zone")
```
This is because we will eventually extract `p-8` but it's followed by an
invalid boundary character `(`.
To solve this, we make sure to replace the `(` with a space. We already
do a similar thing when the classes are followed by an `[`.
One caveat, we _can_ have `(` in our classes, like `bg-(--my-color)`.
But in my testing this is not something that can be used in the
shorthand version.
E.g.:
```slim
div.bg-(--my-color)
```
Compiles to:
```html
<div --my-color="" class="bg-"></div>
```
So I didn't add any special handling for this. Even when trying to
escape the `(`, `-` and `)` characters, it still doesn't work. E.g.:
```slim
div.bg-\(--my-color\)
```
Compiles to:
```html
<div class="bg-">\(--my-color\)</div>
```
# Test plan
1. Added test for the issue
2. Existing tests pass
3. Verified via the extractor tool:
| Before | After |
| --- | --- |
| <img width="958" alt="image"
src="https://github.com/user-attachments/assets/f72c420e-5429-424f-a01d-12f724062bf2"
/> | <img width="958" alt="image"
src="https://github.com/user-attachments/assets/b0cc8f2f-97a8-4fca-8813-3bb1da8d99a8"
/> |
---
Fixes: #17277
This PR fixes an issue where `<template lang="…">…</template>` in Vue
files should be handled as-if it's the language specified in the `lang`
attribute.
To do this, we added a new Vue pre processor and run the content through
the same pre processor logic as we do for other languages.
Fixes: #17211
# Test plan
1. Added a test to verify this works
2. Existing tests still work
Visually verified against the reproduction in the issue:
| Before | After |
| --- | --- |
| <img width="1273" alt="image"
src="https://github.com/user-attachments/assets/d1accdeb-97cf-48ef-83fb-978832b3e599"
/> | <img width="1273" alt="image"
src="https://github.com/user-attachments/assets/ab7ec19c-b6c4-43be-8845-096ff4e58808"
/> |
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR fixes an issue where if you use a number with a decimal in a
variant then it wasn't picked up correctly.
E.g.:
```
<div class="2xl:flex 1.5xl:flex"></div>
^^^^^^^^ Picked up
^^^^^^^^^^ Not picket up
```
This PR fixes that behavior by applying the same rules for utilities
where a `.` is valid if it is surrounded by numbers.
# Test plan
1. Added test to ensure this is picked up
2. Existing tests pass
3. Ran the extractor on a real example with the following results:
| Before | After |
| --- | --- |
| <img width="821" alt="image"
src="https://github.com/user-attachments/assets/a77ed5e4-6848-4fe3-8cbf-cf61ff8db41d"
/> | <img width="821" alt="image"
src="https://github.com/user-attachments/assets/61aca66a-e38d-4b61-bf86-e6286a89a3d9"
/> |
They are crossed out just because it's not a default value we know in
the system, but you can see that the `1.` part is also extracted now.
Fixes: #17148
This PR fixes an issue where often people run into issues where they try
to use string interpolation and it doesn't work. Even worse, it could
result in crashes because we will actually generate CSS. This fix only
filters out candidates with a pattern like `${`. If this occurs in a
string position it is fine.
Another solution would be to add a pre processor for JS/TS (and all
thousand file extension combinations) but the problem is that you can
also write JS in HTML files so we would have to pre process HTML as well
which would not be ideal.
# Test plan
1. Added tests to prove this works in arbitrary values, arbitrary
variables in both utilities and variants.
2. Existing tests pass.
3. Some screenshots with before / after situations:
Given this input:
```ts
let color = '#0088cc';
let opacity = 0.8;
let name = 'variable-name';
let classes = [
// Arbitrary Properties
`[color:${color}]`
`[${property}:value]`,
`[--img:url('https://example.com?q=${name}')]`, // WONT WORK BUT VALID CSS
// Arbitrary Values
`bg-[${color}]`,
// Arbitrary Variables
`bg-(--my-${color})`,
`bg-(--my-color,${color})`,
// Arbitrary Modifier
`bg-red-500/[${opacity}]`,
`bg-red-500/(--my-${name})`,
`bg-red-500/(--my-opacity,${opacity})`,
// Arbitrary Variant
`data-[state=${name}]:flex`,
`supports-(--my-${name}):flex`,
`[@media(width>=${value})]:flex`,
];
```
This is the result:
| Before | After |
| --- | --- |
| <img width="908" alt="image"
src="https://github.com/user-attachments/assets/c64d1b16-d39d-48a6-a098-bc4477cb4b0a"
/> | <img width="908" alt="image"
src="https://github.com/user-attachments/assets/d71aaf62-5e13-4174-82bb-690eb81aaeaf"
/> |
Fixes: #17054Fixes: #15853
This PR fixes an issue where some classes weren't properly extracted due
to some incorrect assumptions in the pre processors.
Templating languages such as Haml, Slim and Pug allow you to write
classes in a shorter way that are not properly contained inside of
strings. E.g.:
```slim
p.flex.px-2
```
These candidates are not properly extracted because there are no
bounding characters like quotes. To solve this, we pre-process it and
replace `.` with ` ` characters. This results in something like:
```
p flex px-2
```
However, this has some challenges on its own. Candidates like `px-2.5`
cannot be written in this shorthand form, instead they need to be in
strings. Now we _cannot_ replace the `.` because otherwise we would
change `px-2.5` to `px-2 5` which is wrong.
The next problem is that we need to know when they are in a "string".
This has another set of problems because these templating languages
allow you to write normal text that will eventually be the contents of
the HTML tags.
```haml
.text-red-500.text-3xl
| This text can't should be red
^ Wait, is this the start of a string now???
```
In this example, if we consider the `'` the start of a string, when it's
clearly not, how would we know it's for _sure_ not a string?
This ended up as a bit of a rabbit hole, but we came up with another
approach entirely if we think about the original problem we want to
solve which is when do we change `.` to ` ` characters.
One of the rules in our current extractor is that a `.` has to be
between 2 numbers. Which works great in a scenario like: `px-2.5`.
However, if you look at Haml or Slim syntax, this is also allowed:
```slim
p.bg-red-500.2xl:flex
^^^ Uh oh...
```
In this scenario, a `.` is surrounded by numbers so we shouldn't replace
it with a space. But as you can see, we clearly do... so we need another
heuristic in this case.
Luckily, one of the rules in Tailwind CSS is that a utility cannot start
with a number, but a variant _can_. This means that if we see a scenario
like `<digit>.<digit>` then we can just check if the value after the `.`
is a valid variant or not.
In this case it is a valid variant so we _do_ want to replace the `.`
with a ` ` even though we do have the `<digit>.<digit>` format.
🥴
# Test plan
1. Added additional tests.
2. Existing tests still pass
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>