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>
Closes#17128
This PR prevents extraction of links inside square brackets as valid
candidate:
```
[https://example/]
```
We do this by throwing out arbitrary properties when the value starts
with a slash (`/`).
## Test plan
- Added unit test
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR adds a small JSON pre processor to improve parsing JSON files.
Due to the sheer amount of potential `[` and `]` brackets, it could be
that parsing JSON files are way slower than they need to be.
We saw this while debugging this issue:
https://github.com/tailwindlabs/tailwindcss/issues/17092
# Test plan
1. Added test to verify the pre processing works
2. Existing tests still pass
Closes#17092
After a lot of spelunking we found one specific reason for the very slow
builds in the repro from #17092: Turns our we are needlessly scanning
the binary `.node` extension for class names 😬. This PR adds `.wasm` and
`.node` to the list of known binary extensions.
## Test plan
- Check out the repro from `#17092`
- Delete the `.gitignore` file
- Observe that builds are very slow (`527.79s`)
- Add a _pnpm override_ to load local versions of Oxide
- `pnpm build` now completes in ~50s
This PR implements the state machines using the type state pattern at
compile time (via generic types) instead of a runtime state variable.
There is no runtime check to see what state we are in, instead we
transition to the new state when it's necessary.
This has some nice performance improvements for some of the state
machines, e.g.:
```diff
- ArbitraryVariableMachine: Throughput: 744.92 MB/s
+ ArbitraryVariableMachine: Throughput: 1.21 GB/s
```
We also don't have to store the current state because each machine runs
to completion. It's during execution that we can move to a new state if
necessary.
Unfortunately the diff is a tiny bit annoying to read, but essentially
this is what happened:
### The `enum` is split up in it's individual states as structs:
```rs
enum State {
A,
B,
C,
}
```
Becomes:
```rs
struct A;
struct B;
struct C;
```
### Generics
The current machine will receive a generic `State` that we can default
to the `IdleState`. Then we use `PhantomData` to "use" the type because
the generic type is otherwise not used as a concrete value, it's just a
marker.
```rs
struct MyMachine {}
```
Becomes:
```rs
struct MyMachine<State = Idle> {
_state: std::marker::PhantomData<State>
}
```
### Split
Next, the `next` function used to match on the current state, but now
each match arm is moved to a dedicated implementation instead:
```rs
impl Machine for MyMachine {
fn next(&mut self) -> MachineState {
match self.state {
State::A => { /* … */ },
State::B => { /* … */ },
State::C => { /* … */ },
}
}
}
```
Becomes:
```rs
impl Machine for MyMachine<A> {
fn next(&mut self) -> MachineState {
/* … */
}
}
impl Machine for MyMachine<B> {
fn next(&mut self) -> MachineState {
/* … */
}
}
impl Machine for MyMachine<C> {
fn next(&mut self) -> MachineState {
/* … */
}
}
```
It's a bit more verbose, but now each state is implemented in its own
block. This also removes 2 levels of nesting which is a nice benefit.
This PR fixes an issue in Slim templates where a single quote `'` at the
start of the line (excluding white space) is considered a line indicator
for verbatim text. It is not considered a string in this scenario.
So something like this:
```slim
div
'Foo'
```
Will compile to:
```html
<div>Foo'</div>
```
Fixes: #17081
This PR fixes an issue where candidates inside `>…<` were not always
correctly extracted. This happens in XML-like languages where the
classes are inside of these boundaries.
E.g.:
```html
<!-- Fluid template language -->
<f:variable name="bgStyle">
<f:switch expression="{data.layout}">
<f:case value="0">from-blue-900 to-cyan-200</f:case>
<!-- ^^^^^^^^^^^^^^^^^^^^^^^^^ -->
<f:case value="1">from-cyan-600 to-teal-200</f:case>
<f:defaultCase>from-blue-300 to-cyan-100</f:defaultCase>
</f:switch>
</f:variable>
```
Fixes: https://github.com/tailwindlabs/tailwindcss/issues/17088
# Test plan
1. Added a new test
2. Existing tests pass
This PR fixes an issue where candidates are not properly extractor when
they end in `\`. This can happen if you embed a programming language
like JS inside another language like PHP where you need to escape some
strings.
Here is an example of Livewire flux:
```blade
@php
if ($sidebarIsStashable) {
$attributes = $attributes->merge([
'x-init' => '$el.classList.add(\'-translate-x-full\'); $el.classList.add(\'transition-transform\')',
// ^ ^
]);
}
@endphp
<div x-data {{ $attributes->class('border-r w-64 p-4 min-h-dvh max-h-dvh top-0 fixed left-0') }}>
{{ $slot }}
</div>
```
Where the `\'` is causing some issues.
Another solution might be to add a custom pre processor for blade files
where we drop the escaped characters, but that felt overkill for now
because some escapes are still valid.
Fixes: #17023
# Test plan
1. Added a test to cover this case.
2. Existing tests still pass
This PR fixes an issue in Razor template files where `@sm:flex` doesn't
work and `@@sm:flex` is required.
In Tailwind CSS v3, some people used a custom transform to replace `@@`
with just `@`. But in Tailwind CSS v4 we don't have this.
However, we can add a pre processor for `.cshtml` and `.razor` files.
Fixes: #17022
This PR cleans up the boundary character checking by using similar
classification techniques as we used for other classification problems.
For starters, this moves the boundary related items to its own file,
next we setup the classification enum.
Last but not least, we removed `}` as an _after_ boundary character, and
instead handle that situation in the Ruby pre processor where we need
it. This means the `%w{flex}` will still work in Ruby files.
---
This PR is a followup for
https://github.com/tailwindlabs/tailwindcss/pull/17001, the main goal is
to clean up some of the boundary character checking code. The other big
improvement is performance. Changing the boundary character checking to
use a classification instead results in:
Took the best score of 10 runs each:
```diff
- CandidateMachine: Throughput: 311.96 MB/s
+ CandidateMachine: Throughput: 333.52 MB/s
```
So a ~20MB/s improvement.
# Test plan
1. Existing tests should pass. Due to the removal of `}` as an after
boundary character, some tests are updated.
2. Added new tests to ensure the Ruby pre processor still works as
expected.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This PR fixes an issue where strings in the Pug and Slim pre-processor
were handled using the `string_machine`. However, the `string_machine`
is not for strings inside of Tailwind CSS classes which means that
whitespace is invalid.
This means that parts of the code that _are_ inside strings will not be
inside strings and parts of the code that are not inside strings will be
part of a potential string. This is a bit confusing to wrap your head
around, but here is a visual representation of the problem:
```
.join(' ')
^ 3. start of new string, which means that the `)` _could_ be part of a string if a new `'` occurs later.
^ 2. whitespace is not allowed, stop string
^ 1. start of string
```
Fixes: #16998
# Test plan
1. Added new test
2. Existing tests still pass
3. Added a simple test helper to make sure that we can extract the
correct candidates _after_ pre-processing
This PR fixes an issue in Slim templates where the start of attributes
causes some candidates to be missing.
```slim
.text-xl.text-red-600[
data-foo="bar"
]
| This line should be red
```
Because of the `[` attached to the `text-red-600`, the `text-red-600`
was not extracted because `[` is not a valid boundary character.
To solve this, we copied the Pug pre processor and created a dedicated
Slim pre processor. Next, we ensure that we replace `[` with ` ` in this
scenario (by also making sure that we don't replace `[` where it's
important).
Additionally, we noticed that `.` was also replaced inside of arbitrary
values such as URLs. This has been fixed for both Pug and Slim.
Fixes: #16975
# Test plan
1. Added failing tests
2. Existing tests still pass
Closes#16982
Handle the case of variants looking like this: `@[32rem]:flex`.
## Test plan
Added regression tests
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Fixes#16978, and also added support for dash.
Classes like `text-theme1-primary` or `text-theme1_primary` should be
treated as valid.
If this approach is not aligned with the project’s direction or there
are any concerns, please feel free to close or edit this PR 😃
<br/>
### As is
Classes conatining number followed by dash or underscore (e.g.
`bg-theme1-primary`, `text-title1_strong`) are ignored, and utility
classes are not generated.
### To be
Classes conatining number followed by dash or underscore (e.g.
`bg-theme1-primary`, `text-title1_strong`) are treated as valid
tailwindcss classes
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This PR fixes an issue where named utilities that contain double dashes
`--` are not extracted correctly.
Some people use `--` in the middle of the utility to create some form of
namespaced utility.
Given this input:
```js
let x = 'foo--bar'
```
The extracted candidates before this change:
```js
[ "let", "x", "--bar" ]
```
The extracted candidates after this change:
```js
[ "let", "x", "foo--bar", "--bar" ]
```
The reason `--bar` is still extracted in both cases is because of the
CSS variable machine. We could improve its extraction by checking its
boundary characters but that's a different issue.
For now, the important thing is that `foo--bar` was extracted.
# Test plan
1. Added new test
2. Existing tests pass
This PR improves the internal DX when working with `u8` classification
into a smaller enum. This is done by implementing a `ClassifyBytes` proc
derive macro. The benefit of this is that the DX is much better and
everything you will see here is done at compile time.
Before:
```rs
#[derive(Debug, Clone, Copy, PartialEq)]
enum Class {
ValidStart,
ValidInside,
OpenBracket,
OpenParen,
Slash,
Other,
}
const CLASS_TABLE: [Class; 256] = {
let mut table = [Class::Other; 256];
macro_rules! set {
($class:expr, $($byte:expr),+ $(,)?) => {
$(table[$byte as usize] = $class;)+
};
}
macro_rules! set_range {
($class:expr, $start:literal ..= $end:literal) => {
let mut i = $start;
while i <= $end {
table[i as usize] = $class;
i += 1;
}
};
}
set_range!(Class::ValidStart, b'a'..=b'z');
set_range!(Class::ValidStart, b'A'..=b'Z');
set_range!(Class::ValidStart, b'0'..=b'9');
set!(Class::OpenBracket, b'[');
set!(Class::OpenParen, b'(');
set!(Class::Slash, b'/');
set!(Class::ValidInside, b'-', b'_', b'.');
table
};
```
After:
```rs
#[derive(Debug, Clone, Copy, PartialEq, ClassifyBytes)]
enum Class {
#[bytes_range(b'a'..=b'z', b'A'..=b'Z', b'0'..=b'9')]
ValidStart,
#[bytes(b'-', b'_', b'.')]
ValidInside,
#[bytes(b'[')]
OpenBracket,
#[bytes(b'(')]
OpenParen,
#[bytes(b'/')]
Slash,
#[fallback]
Other,
}
```
Before we were generating a `CLASS_TABLE` that we could access directly,
but now it will be part of the `Class`. This means that the usage has to
change:
```diff
- CLASS_TABLE[cursor.curr as usize]
+ Class::TABLE[cursor.curr as usize]
```
This is slightly worse UX, and this is where another change comes in. We
implemented the `From<u8> for #enum_name` trait inside of the
`ClassifyBytes` derive macro. This allows us to use `.into()` on any
`u8` as long as we are comparing it to a `Class` instance. In our
scenario:
```diff
- Class::TABLE[cursor.curr as usize]
+ cursor.curr.into()
```
Usage wise, this looks something like this:
```diff
while cursor.pos < len {
- match Class::TABLE[cursor.curr as usize] {
+ match cursor.curr.into() {
- Class::Escape => match Class::Table[cursor.next as usize] {
+ Class::Escape => match cursor.next.into() {
// An escaped whitespace character is not allowed
Class::Whitespace => return MachineState::Idle,
// An escaped character, skip ahead to the next character
_ => cursor.advance(),
},
// End of the string
Class::Quote if cursor.curr == end_char => return self.done(start_pos, cursor),
// Any kind of whitespace is not allowed
Class::Whitespace => return MachineState::Idle,
// Everything else is valid
_ => {}
};
cursor.advance()
}
MachineState::Idle
}
}
```
If you manually look at the `Class::TABLE` in your editor for example,
you can see that it is properly generated at compile time.
Given this input:
```rs
#[derive(Clone, Copy, ClassifyBytes)]
enum Class {
#[bytes_range(b'a'..=b'z')]
AlphaLower,
#[bytes_range(b'A'..=b'Z')]
AlphaUpper,
#[bytes(b'@')]
At,
#[bytes(b':')]
Colon,
#[bytes(b'-')]
Dash,
#[bytes(b'.')]
Dot,
#[bytes(b'\0')]
End,
#[bytes(b'!')]
Exclamation,
#[bytes_range(b'0'..=b'9')]
Number,
#[bytes(b'[')]
OpenBracket,
#[bytes(b']')]
CloseBracket,
#[bytes(b'(')]
OpenParen,
#[bytes(b'%')]
Percent,
#[bytes(b'"', b'\'', b'`')]
Quote,
#[bytes(b'/')]
Slash,
#[bytes(b'_')]
Underscore,
#[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')]
Whitespace,
#[fallback]
Other,
}
```
This is the result:
<img width="1244" alt="image"
src="https://github.com/user-attachments/assets/6ffd6ad3-0b2f-4381-a24c-593e4c72080e"
/>
This PR adds a new candidate[^candidate] extractor with 2 major goals in
mind:
1. It must be way easier to reason about and maintain.
2. It must have on-par performance or better than the current candidate
extractor.
### Problem
Candidate extraction is a bit of a wild west in Tailwind CSS and it's a
very critical step to make sure that all your classes are picked up
correctly to ensure that your website/app looks good.
One issue we run into is that Tailwind CSS is used in many different
"host" languages and frameworks with their own syntax. It's not only
used in HTML but also in JSX/TSX, Vue, Svelte, Angular, Pug, Rust, PHP,
Rails, Clojure, .NET, … the list goes on and all of these have different
syntaxes. Introducing dedicated parsers for each of these languages
would be a huge maintenance burden because there will be new languages
and frameworks coming up all the time. The best thing we can do is make
assumptions and so far we've done a pretty good job at that.
The only certainty we have is that there is at least _some_ structure to
the possible Tailwind classes used in a file. E.g.: `abc#def` is
definitely not a valid class, `hover:flex` definitely is. In a perfect
world we limit the characters that can be used and defined a formal
grammar that each candidate must follow, but that's not really an option
right now (maybe this is something we can implement in future major
versions).
The current candidate extractor we have has grown organically over time
and required patching things here and there to make it work in various
scenarios (and edge cases due to the different languages Tailwind is
used in).
While there is definitely some structure, we essentially work in 2
phases:
1. Try to extract `0..n` candidates. (This is the hard part)
2. Validate each candidate to make sure they are valid looking classes
(by validating against the few rules we have)
Another reason the current extractor is hard to reason about is that we
need it to be fast and that comes with some trade-offs to readability
and maintainability.
Unfortunately there will always be a lot of false positives, but if we
extract more classes than necessary then that's fine. It's only when we
pass the candidates to the core engine that we will know for sure if
they are valid or not. (we have some ideas to limit the amount of false
positives but that's for another time)
### Solution
Since the introduction of Tailwind CSS v4, we re-worked the internals
quite a bit and we have a dedicated internal AST structure for
candidates. For example, if you take a look at this:
```html
<div class="[@media(pointer:fine)]:data-[state=pending]:hover:text-red-500/(--my-opacity)"></div>
```
<details>
<summary>This will be parsed into the following AST:</summary>
```json
[
{
"kind": "functional",
"root": "text",
"value": {
"kind": "named",
"value": "red-500",
"fraction": null
},
"modifier": {
"kind": "arbitrary",
"value": "var(--my-opacity)"
},
"variants": [
{
"kind": "static",
"root": "hover"
},
{
"kind": "functional",
"root": "data",
"value": {
"kind": "arbitrary",
"value": "state=pending"
},
"modifier": null
},
{
"kind": "arbitrary",
"selector": "@media(pointer:fine)",
"relative": false
}
],
"important": false,
"raw": "[@media(pointer:fine)]:data-[state=pending]:hover:text-red-500/(--my-opacity)"
}
]
```
</details>
We have a lot of information here and we gave these patterns a name
internally. You'll see names like `functional`, `static`, `arbitrary`,
`modifier`, `variant`, `compound`, ...
Some of these patterns will be important for the new candidate extractor
as well:
| Name | Example | Description |
| -------------------------- | ----------------- |
---------------------------------------------------------------------------------------------------
|
| Static utility (named) | `flex` | A simple utility with no inputs
whatsoever |
| Functional utility (named) | `bg-red-500` | A utility `bg` with an
input that is named `red-500` |
| Arbitrary value | `bg-[#0088cc]` | A utility `bg` with an input that
is arbitrary, denoted by `[…]` |
| Arbitrary variable | `bg-(--my-color)` | A utility `bg` with an input
that is arbitrary and has a CSS variable shorthand, denoted by `(--…)` |
| Arbitrary property | `[color:red]` | A utility that sets a property to
a value on the fly |
A similar structure exist for modifiers, where each modifier must start
with `/`:
| Name | Example | Description |
| ------------------ | --------------------------- |
---------------------------------------- |
| Named modifier | bg-red-500`/20` | A named modifier |
| Arbitrary value | bg-red-500`/[20%]` | An arbitrary value, denoted by
`/[…]` |
| Arbitrary variable | bg-red-500`/(--my-opacity)` | An arbitrary
variable, denoted by `/(…)` |
Last but not least, we have variants. They have a very similar pattern
but they _must_ end in a `:`.
| Name | Example | Description |
| ------------------ | --------------------------- |
------------------------------------------------------------------------
|
| Named variant | `hover:` | A named variant |
| Arbitrary value | `data-[state=pending]:` | An arbitrary value,
denoted by `[…]` |
| Arbitrary variable | `supports-(--my-variable):` | An arbitrary
variable, denoted by `(…)` |
| Arbitrary variant | `[@media(pointer:fine)]:` | Similar to arbitrary
properties, this will generate a variant on the fly |
The goal with the new extractor is to encode these separate patterns in
dedicated pieces of code (we called them "machines" because they are
mostly state machine based and because I've been watching Person of
Interest but I digress).
This will allow us to focus on each pattern separately, so if there is a
bug or some new syntax we want to support we can add it to those
machines.
One nice benefit of this is that we can encode the rules and handle
validation as we go. The moment we know that some pattern is invalid, we
can bail out early.
At the time of writing this, there are a bunch of machines:
<details>
<summary>Overview of the machines</summary>
- `ArbitraryPropertyMachine`
Extracts candidates such as `[color:red]`. Some of the rules are:
1. There must be a property name
2. There must be a `:`
3. There must ba a value
There cannot be any spaces, the brackets are included, if the property
is a CSS variable, it must be a valid CSS variable (uses the
`CssVariableMachine`).
```
[color:red]
^^^^^^^^^^^
[--my-color:red]
^^^^^^^^^^^^^^^^
```
Depends on the `StringMachine` and `CssVariableMachine`.
- `ArbitraryValueMachine`
Extracts arbitrary values for utilities and modifiers including the
brackets:
```
bg-[#0088cc]
^^^^^^^^^
bg-red-500/[20%]
^^^^^
```
Depends on the `StringMachine`.
- `ArbitraryVariableMachine`
Extracts arbitrary variables including the parentheses. The first
argument must be a valid CSS variable, the other arguments are optional
fallback arguments.
```
(--my-value)
^^^^^^^^^^^^
bg-red-500/(--my-opacity)
^^^^^^^^^^^^^^
```
Depends on the `StringMachine` and `CssVariableMachine`.
- `CandidateMachine`
Uses the variant machine and utility machine. It will make sure that 0
or more variants are directly touching and followed by a utility.
```
hover:focus:flex
^^^^^^^^^^^^^^^^
aria-invalid:bg-red-500/(--my-opacity)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```
Depends on the `VariantMachine` and `UtilityMachine`.
- `CssVariableMachine`
Extracts CSS variables, they must start with `--` and must contain at
least one alphanumeric character or, `-`, `_` and can contain any
escaped character (except for whitespace).
```
bg-(--my-color)
^^^^^^^^^^
bg-red-500/(--my-opacity)
^^^^^^^^^^^^
bg-(--my-color)/(--my-opacity)
^^^^^^^^^^ ^^^^^^^^^^^^
```
- `ModifierMachine`
Extracts modifiers including the `/`
- `/[` will delegate to the `ArbitraryValueMachine`
- `/(` will delegate to the `ArbitraryVariableMachine`
```
bg-red-500/20
^^^
bg-red-500/[20%]
^^^^^^
bg-red-500/(--my-opacity)
^^^^^^^^^^^^^^^
```
Depends on the `ArbitraryValueMachine` and `ArbitraryVariableMachine`.
- `NamedUtilityMachine`
Extracts named utilities regardless of whether they are functional or
static.
```
flex
^^^^
px-2.5
^^^^^^
```
This includes rules like: A `.` must be surrounded by digits.
Depends on the `ArbitraryValueMachine` and `ArbitraryVariableMachine`.
- `NamedVariantMachine`
Extracts named variants regardless of whether they are functional or
static. This is very similar to the `NamedUtilityMachine` but with
different rules. We could combine them, but splitting things up makes it
easier to reason about.
Another rule is that the `:` must be included.
```
hover:flex
^^^^^^
data-[state=pending]:flex
^^^^^^^^^^^^^^^^^^^^^
supports-(--my-variable):flex
^^^^^^^^^^^^^^^^^^^^^^^^^
```
Depends on the `ArbitraryVariableMachine`, `ArbitraryValueMachine`, and
`ModifierMachine`.
- `StringMachine`
This is a low-level machine that is used by various other machines. The
only job this has is to extract strings that start with double quotes,
single quotes or backticks.
We have this because once you are in a string, we don't have to make
sure that brackets, parens and curlies are properly balanced. We have to
make sure that balancing brackets are properly handled in other
machines.
```
content-["Hello_World!"]
^^^^^^^^^^^^^^
bg-[url("https://example.com")]
^^^^^^^^^^^^^^^^^^^^^
```
- `UtilityMachine`
Extracts utilities, it will use the lower level `NamedUtilityMachine`,
`ArbitraryPropertyMachine` and `ModifierMachine` to extract the utility.
It will also handle important markers (including the legacy important
marker).
```
flex
^^^^
bg-red-500/20
^^^^^^^^^^^^^
!bg-red-500/20 Legacy important marker
^^^^^^^^^^^^^^
bg-red-500/20! New important marker
^^^^^^^^^^^^^^
!bg-red-500/20! Both, but this is considered invalid
^^^^^^^^^^^^^^^
```
Depends on the `ArbitraryPropertyMachine`, `NamedUtilityMachine`, and
`ModifierMachine`.
- `VariantMachine`
Extracts variants, it will use the lower level `NamedVariantMachine` and
`ArbitraryValueMachine` to extract the variant.
```
hover:focus:flex
^^^^^^
^^^^^^
```
Depends on the `NamedVariantMachine` and `ArbitraryValueMachine`.
</details>
One important thing to know here is that each machine runs to
completion. They all implement a `Machine` trait that has a
`next(cursor)` method and returns a `MachineState`.
The `MachineState` looks like this:
```rs
enum MachineState {
Idle,
Done(Span)
}
```
Where a `Span` is just the location in the input where the candidate was
found.
```rs
struct Span {
pub start: usize,
pub end: usize,
}
```
#### Complexities
**Boundary characters:**
When running these machines to completion, they don't typically check
for boundary characters, the wrapping `CandidateMachine` will check for
boundary characters.
A boundary character is where we know that even though the character is
touching the candidate it will not be part of the candidate.
```html
<div class="flex"></div>
<!-- ^ ^ -->
```
The quotes are touching the candidate `flex`, but they will not be part
of the candidate itself, so this is considered a valid candidate.
**What to pick?**
Let's imagine you are parsing this input:
```html
<div class="hover:flex"></div>
```
The `UtilityMachine` will find `hover` and `flex`. The `VariantMachine`
will find `hover:`. This means that at a certain point in the
`CandidateMachine` you will see something like this:
```rs
let variant_machine_state = variant_machine.next(cursor);
// MachineState::Done(Span { start: 12, end: 17 }) // `hover:`
let utility_machine_state = utility_machine.next(cursor);
// MachineState::Done(Span { start: 12, end: 16 }) // `hover`
```
They are both done, but which one do we pick? In this scenario we will
always pick the variant because its range will always be 1 character
longer than the utility.
Of course there is an exception to this rule and it has to do with the
fact that Tailwind CSS can be used in different languages and
frameworks. A lot of people use `clsx` for dynamically applying classes
to their React components. E.g.:
```tsx
<div
class={clsx({
underline: someCondition(),
})}
></div>
```
In this scenario, we will see `underline:` as a variant, and `underline`
as a utility. We will pick the utility in this scenario because the next
character is whitespace so this will never be a valid candidate
otherwise (variants and utilities must be touching). Another reason this
is valid, is because there wasn't a variant present prior to this
candidate.
E.g.:
```tsx
<div
class={clsx({
hover:underline: someCondition(),
})}
></div>
```
This will be considered invalid, if you do want this, you should use
quotes.
E.g.:
```tsx
<div
class={clsx({
'hover:underline': someCondition(),
})}
></div>
```
**Overlapping/covered spans:**
Another complexity is that the extracted spans for candidates can and
will overlap. Let's take a look at this C# example:
```csharp
public enum StackSpacing
{
[CssClass("gap-y-4")]
Small,
[CssClass("gap-y-6")]
Medium,
[CssClass("gap-y-8")]
Large
}
```
In this scenario, `[CssClass("gap-y-4")]` starts with a `[` so we have a
few options here:
1. It is an arbitrary property, e.g.: `[color:red]`
2. It is an arbitrary variant, e.g.: `[@media(pointer:fine)]:`
When running the parsers, both the `VariantMachine` and the
`UtilityMachine` will run to completion but end up in a
`MachineState::Idle` state.
- This is because it is not a valid variant because it didn't end with a
`:`.
- It's also not a valid arbitrary property, because it didn't include a
`:` to separate the property from the value.
Looking at the code as a human it's very clear what this is supposed to
be, but not from the individual machines perspective.
Obviously we want to extract the `gap-y-*` classes here.
To solve this problem, we will run over an additional slice of the
input, starting at the position before the machines started parsing
until the position where the machines stopped parsing.
That slice will be this one: `[CssClass("gap-y-6")]` (we already skipped
over the whitespace). Now, for every `[` character we see, will start a
new `CandidateMachine` right after the `[`'s position and run the
machines over that slice. This will now eventually extract the `gap-y-6`
class.
The next question is, what if there was a `:` (e.g.:
`[CssClass("gap-y-6")]:`), then the `VariantMachine` would complete, but
the `UtilityMachine` will not because not exists after it. We will apply
the same idea in this case.
Another issue is if we _do_ have actual overlapping ranges. E.g.: `let
classes = ['[color:red]'];`. This will extract both the `[color:red]`
and `color:red` classes. You have to use your imagination, but the last
one has the exact same structure as `hover:flex` (variant + utility).
In this case we will make sure to drop spans that are covered by other
spans.
The extracted `Span`s will be valid candidates therefore if the outer
most candidate is valid, we can throw away the inner candidate.
```
Position: 11112222222
67890123456
↓↓↓↓↓↓↓↓↓↓↓
Span { start: 17, end: 25 } // color:red
Span { start: 16, end: 26 } // [color:red]
```
#### Exceptions
**JavaScript keys as candidates:**
We already talked about the `clsx` scenario, but there are a few more
exceptions and that has to do with different syntaxes.
**CSS class shorthand in certain templating languages:**
In Pug and Slim, you can have a syntax like this:
```pug
.flex.underline
div Hello World
```
<details>
<summary>Generated HTML</summary>
```html
<div class="flex underline">
<div>Hello World</div>
</div>
```
</details>
We have to make sure that in these scenarios the `.` is a valid boundary
character. For this, we introduce a pre-processing step to massage the
input a little bit to improve the extraction of the data. We have to
make sure we don't make the input smaller or longer otherwise the
positions might be off.
In this scenario, we could simply replace the `.` with a space. But of
course, there are scenarios in these languages where it's not safe to do
that.
If you want to use `px-2.5` with this syntax, then you'd write:
```pug
.flex.px-2.5
div Hello World
```
But that's invalid because that technically means `flex`, `px-2`, and
`5` as classes.
You can use this syntax to get around that:
```pug
div(class="px-2.5")
div Hello World
```
<details>
<summary>Generated HTML</summary>
```html
<div class="px-2.5">
<div>Hello World</div>
</div>
```
</details>
Which means that we can't simply replace `.` with a space, but have to
parse the input. Luckily we only care about strings (and we have a
`StringMachine` for that) and ignore replacing `.` inside of strings.
**Ruby's weird string syntax:**
```ruby
%w[flex underline]
```
This is valid syntax and is shorthand for:
```ruby
["flex", "underline"]
```
Luckily this problem is solved by the running the sub-machines after
each `[` character.
### Performance
**Testing:**
Each machine has a `test_…_performance` test (that is ignored by
default) that allows you to test the throughput of that machine. If you
want to run them, you can use the following command:
```sh
cargo test test_variant_machine_performance --release -- --ignored
```
This will run the test in release mode and allows you to run the ignored
test.
> [!CAUTION]
> This test **_will_** fail, but it will print some output. E.g.:
```
tailwindcss_oxide::extractor::variant_machine::VariantMachine: Throughput: 737.75 MB/s over 0.02s
tailwindcss_oxide::extractor::variant_machine::VariantMachine: Duration: 500ns
```
**Readability:**
One thing to note when looking at the code is that it's not always
written in the cleanest way but we had to make some sacrifices for
performance reasons.
The `input` is of type `&[u8]`, so we are already dealing with bytes.
Luckily, Rust has some nice ergonomics to easily write `b'['` instead of
`0x5b`.
A concrete example where we had to sacrifice readability is the state
machines where we check the `previous`, `current` and `next` character
to make decisions. For a named utility one of the rules is that a `.`
must be preceded by and followed by a digit. This can be written as:
```rs
match (cursor.prev, cursor.curr, cursor.next) {
(b'0'..=b'9', b'.', b'0'..=b'9') => { /* … */ }
_ => { /* … */ }
}
```
But this is not very fast because Rust can't optimize the match
statement very well, especially because we are dealing with tuples
containing 3 values and each value is a `u8`.
To solve this we use some nesting, once we reach `b'.'` only then will
we check for the previous and next characters. We will also early return
in most places. If the previous character is not a digit, there is no
need to check the next character.
**Classification and jump tables:**
Another optimization we did is to classify the characters into a much
smaller `enum` such that Rust _can_ optimize all `match` arms and create
some jump tables behind the scenes.
E.g.:
```rs
#[derive(Debug, Clone, Copy, PartialEq)]
enum Class {
/// ', ", or `
Quote,
/// \
Escape,
/// Whitespace characters
Whitespace,
Other,
}
const CLASS_TABLE: [Class; 256] = {
let mut table = [Class::Other; 256];
macro_rules! set {
($class:expr, $($byte:expr),+ $(,)?) => {
$(table[$byte as usize] = $class;)+
};
}
set!(Class::Quote, b'"', b'\'', b'`');
set!(Class::Escape, b'\\');
set!(Class::Whitespace, b' ', b'\t', b'\n', b'\r', b'\x0C');
table
};
```
There are only 4 values in this enum, so Rust can optimize this very
well. The `CLASS_TABLE` is generated at compile time and must be exactly
256 elements long to fit all `u8` values.
**Inlining**:
Last but not least, sometimes we use functions to abstract some logic.
Luckily Rust will optimize and inline most of the functions
automatically. In some scenarios, explicitly adding a
`#[inline(always)]` improves performance, sometimes it doesn't improve
it at all.
You might notice that in some functions the annotation is added and in
some it's not. Every state machine was tested on its own and whenever
the performance was better with the annotation, it was added.
### Test Plan
1. Each machine has a dedicated set of tests to try and extract the
relevant part for that machine. Most machines don't even check boundary
characters or try to extract nested candidates. So keep that in mind
when adding new tests. Extracting inside of nested `[…]` is only handled
by the outer most `extractor/mod.rs`.
2. The main `extractor/mod.rs` has dedicated tests for recent bug
reports related to missing candidates.
3. You can test each machine's performance if you want to.
There is a chance that this new parser is missing candidates even though
a lot of tests are added and existing tests have been ported.
To double check, we ran the new extractor on our own projects to make
sure we didn't miss anything obvious.
#### Tailwind UI
On Tailwind UI the diff looks like this:
<details>
<summary>diff</summary>
```diff
diff --git a/./main.css b/./pr.css
index d83b0a506..b3dd94a1d 100644
--- a/./main.css
+++ b/./pr.css
@@ -5576,9 +5576,6 @@ @layer utilities {
--tw-saturate: saturate(0%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
- .\!filter {
- filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,) !important;
- }
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
```
</details>
The reason `!filter` is gone, is because it was used like this:
```js
getProducts.js
23: if (!filter) return true
```
And right now `(` and `)` are not considered valid boundary characters
for a candidate.
#### Catalyst
On Catalyst, the diff looks like this:
<details>
<summary>diff</summary>
```diff
diff --git a/./main.css b/./pr.css
index 9f8ed129..4aec992e 100644
--- a/./main.css
+++ b/./pr.css
@@ -2105,9 +2105,6 @@
.outline-transparent {
outline-color: transparent;
}
- .filter {
- filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
- }
.backdrop-blur-\[6px\] {
--tw-backdrop-blur: blur(6px);
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
@@ -7141,46 +7138,6 @@
inherits: false;
initial-value: solid;
}
-@property --tw-blur {
- syntax: "*";
- inherits: false;
-}
-@property --tw-brightness {
- syntax: "*";
- inherits: false;
-}
-@property --tw-contrast {
- syntax: "*";
- inherits: false;
-}
-@property --tw-grayscale {
- syntax: "*";
- inherits: false;
-}
-@property --tw-hue-rotate {
- syntax: "*";
- inherits: false;
-}
-@property --tw-invert {
- syntax: "*";
- inherits: false;
-}
-@property --tw-opacity {
- syntax: "*";
- inherits: false;
-}
-@property --tw-saturate {
- syntax: "*";
- inherits: false;
-}
-@property --tw-sepia {
- syntax: "*";
- inherits: false;
-}
-@property --tw-drop-shadow {
- syntax: "*";
- inherits: false;
-}
@property --tw-backdrop-blur {
syntax: "*";
inherits: false;
```
</details>
The reason for this is that `filter` was only used as a function call:
```tsx
src/app/docs/Code.tsx
31: .filter((x) => x !== null)
```
This was tested on all templates and they all remove a very small amount
of classes that aren't used.
The script to test this looks like this:
```sh
bun --bun ~/github.com/tailwindlabs/tailwindcss/packages/@tailwindcss-cli/src/index.t -- -i ./src/styles/tailwind.css -o pr.css
bun --bun ~/github.com/tailwindlabs/tailwindcss--main/packages/@tailwindcss-cli/src/index.t -- -i ./src/styles/tailwind.css -o main.css
git diff --no-index --patch ./{main,pr}.css
```
This is using git worktrees, so the `pr` branch lives in a `tailwindcss`
folder, and the `main` branch lives in a `tailwindcss--main` folder.
---
### Fixes:
- Fixes: https://github.com/tailwindlabs/tailwindcss/issues/15616
- Fixes: https://github.com/tailwindlabs/tailwindcss/issues/16750
- Fixes: https://github.com/tailwindlabs/tailwindcss/issues/16790
- Fixes: https://github.com/tailwindlabs/tailwindcss/issues/16801
- Fixes: https://github.com/tailwindlabs/tailwindcss/issues/16880 (due
to validating the arbitrary property)
---
### Ideas for in the future
1. Right now each machine takes in a `Cursor` object. One potential
improvement we can make is to rely on the `input` on its own instead of
going via the wrapping `Cursor` object.
2. If you take a look at the AST, you'll notice that utilities and
variants have a "root", these are basically prefixes of each available
utility and/or variant. We can use this information to filter out
candidates and bail out early if we know that a certain candidate will
never produce a valid class.
3. Passthrough the `prefix` information. Everything that doesn't start
with `tw:` can be skipped.
### Design decisions that didn't make it
Once you reach this part, you can stop reading if you want to, but this
is more like a brain dump of the things we tried and didn't work out.
Wanted to include them as a reference in case we want to look back at
this issue and know _why_ certain things are implemented the way they
are.
#### One character at a time
In an earlier implementation, the state machines were pure state
machines where the `next()` function was called on every single
character of the input. This had a lot of overhead because for every
character we had to:
1. Ask the `CandidateMachine` which state it was in.
2. Check the `cursor.curr` (and potentially the `cursor.prev` and
`cursor.next`) character.
3. If we were in a state where a nested state machine was running, we
had to check its current state as well and so on.
4. Once we did all of that we could go to the next character.
In this approach, the `MachineState` looked like this instead:
```rs
enum MachineState {
Idle,
Parsing,
Done(Span)
}
```
This had its own set of problems because now it's very hard to know
whether we are done or not.
```html
<div class="hover:flex"></div>
<!-- ^ -->
```
Let's look at the current position in the example above. At this point,
it's both a valid variant and valid utility, so there was a lot of
additional state we had to track to know whether we were done or not.
#### `Span` stitching
Another approach we tried was to just collect all valid variants and
utilities and throw them in a big `Vec<Span>`. This reduced the amount
of additional state to track and we could track a span the moment we saw
a `MachineState::Done(span)`.
The next thing we had to do was to make sure that:
1. Covered spans were removed. We still do this part in the current
implementation.
2. Combine all touching variant spans (where `span_a.end + 1 ==
span_b.start`).
3. For every combined variant span, find a corresponding utility span.
- If there is no utility span, the candidate is invalid.
- If there are multiple candidate spans (this is in theory not possible
because we dropped covered spans)
- If there is a candidate _but_ it is attached to another set of spans,
then the candidate is invalid. E.g.: `flex!block`
4. All left-over utility spans are candidates without variants.
This approach was slow, and still a bit hard to reason about.
#### Matching on tuples
While matching against the `prev`, `curr` and `next` characters was very
readable and easy to reason about. It was not very fast. Unfortunately
had to abandon this approach in favor of a more optimized approach.
In a perfect world, we would still write it this way, but have some
compile time macro that would optimize this for us.
#### Matching against `b'…'` instead of classification and jump tables
Similar to the previous point, while this is better for readability,
it's not fast enough. The jump tables are much faster.
Luckily for us, each machine has it's own set of rules and context, so
it's much easier to reason about a single problem and optimize a single
machine.
[^candidate]: A candidate is what a potential Tailwind CSS class _could_
be. It's a candidate because at this stage we don't know if it will
actually produce something but it looks like it could be a valid class.
E.g.: `hover:bg-red-500` is a candidate, but it will only produce
something if `--color-red-500` is defined in your theme.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
Fixes a crash found in Dockerfiles when a build takes place at the root
directory. It's not a good practice to keep your application logic in
`/`, but it probably shouldn't cause a crash either.
I found this particularly difficult to write tests for because it would
involve either running a glob on my real filesystem starting from `/` or
mocking the calls to `fs` which as far as I can tell isn't supported in
the codebase and would be out of scope to try to do here.
Fixes#15987
Fixes#15632Fixes#15740
This PR fixes a number of Oxide scanner bugs reported over various
channels, specifically:
- When using the Svelte class shorthand split over various lines, we
weren't extracting class names properly:
```svelte
<div
class:underline={isUnderline}>
</div>
```
- We now extract classes when using the class shortcut in Angular:
```html
<div [class.underline]=\"bool\"></div>
```
- We now validate parentheses within arbitrary candidates so that we
don't consume invalid arbitrary candidates anymore which allows us to
parse the following case properly:
```js
const classes = [wrapper("bg-red-500")]
```
## Test plan
Added unit tests
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Right now, when Oxide is scanning for files, it considers ignore files
in the "root" directory it is scanning as well as all parent
directories.
We honor .gitignore files even when not in a git repo as an optimization
in case a project has been created, contains a .gitignore, but no repo
has actually been initialized. However, this has an unintended side
effect of including ignore files _ouside of a repo_ when there is one.
This means that if you have a .gitignore file in your home folder it'll
get applied even when you're inside a git repo which is not what you'd
expect.
This PR addresses this by checking to see the folder being scanned is
inside a repo and turns on a flag that ensures .gitignore files from the
repo are the only ones used (global ignore files configured in git still
work tho).
This still needs lots of tests to make sure things work as expected.
Fixes#15876
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>