diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 42645aa492d5..63f79fa100e6 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -50,7 +50,7 @@ If you open a pull request for a new feature, we're likely to close it not becau
## Coding standards
-Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindcss/tailwindcss/blob/main/package.json). You can check your code against these standards by running:
+Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindlabs/tailwindcss/blob/main/package.json). You can check your code against these standards by running:
```sh
pnpm run lint
@@ -76,7 +76,7 @@ To run the integration tests, use:
pnpm build && pnpm test:integrations
```
-Additionally, some features require testing in browsers (i.e to ensure CSS variable resolution works as expected). These can be run via:
+Additionally, some features require testing in browsers (i.e. to ensure CSS variable resolution works as expected). These can be run via:
```sh
pnpm build && pnpm test:ui
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index ee9d3f9abcc0..aa0793aa39f8 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -8,7 +8,7 @@ It's never a fun experience to have your pull request declined after investing a
For more info, check out the contributing guide:
-https://github.com/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md
+https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md
-->
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e369c64fcd69..fca477c08bec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,12 +8,18 @@ on:
permissions:
contents: read
+env:
+ NODE_VERSION: 24
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
tests:
strategy:
fail-fast: false
matrix:
- node-version: [20]
runner:
- name: Windows
os: windows-latest
@@ -41,18 +47,18 @@ jobs:
name: ${{ matrix.runner.name }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
+ - name: Use Node.js ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@v6
with:
- node-version: ${{ matrix.node-version }}
+ node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
@@ -64,7 +70,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
./crates/node/*.node
@@ -104,9 +110,13 @@ jobs:
- name: Run Playwright tests
run: npm run test:ui
+ notify:
+ if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }}
+ needs: tests
+ runs-on: ubuntu-latest
+ steps:
- name: Notify Discord
- if: failure() && github.ref == 'refs/heads/main'
uses: discord-actions/message@v2
with:
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
- message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
+ message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 475570615bf8..3b2cf8f1b80a 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -8,13 +8,18 @@ on:
permissions:
contents: read
+env:
+ NODE_VERSION: 24
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
tests:
strategy:
fail-fast: false
matrix:
- node-version: [20]
-
runner:
- name: Windows
os: windows-latest
@@ -50,22 +55,22 @@ jobs:
name: ${{ matrix.runner.name }} / ${{ matrix.integration }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
+ - name: Use Node.js ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@v6
with:
- node-version: ${{ matrix.node-version }}
+ node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
@@ -77,7 +82,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
./crates/node/*.node
@@ -108,9 +113,13 @@ jobs:
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
+ notify:
+ if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }}
+ needs: tests
+ runs-on: ubuntu-latest
+ steps:
- name: Notify Discord
- if: failure() && github.ref == 'refs/heads/main'
uses: discord-actions/message@v2
with:
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
- message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
+ message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index be1050b03b9c..d5ad798be455 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -8,12 +8,16 @@ on:
env:
APP_NAME: tailwindcss-oxide
- NODE_VERSION: 20
+ NODE_VERSION: 24
OXIDE_LOCATION: ./crates/node
permissions:
contents: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
strategy:
@@ -73,11 +77,11 @@ jobs:
container: ${{ matrix.container }}
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
@@ -90,7 +94,7 @@ jobs:
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
@@ -102,7 +106,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
./crates/node/*.node
@@ -117,7 +121,7 @@ jobs:
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
- name: Install Node.JS
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
@@ -143,7 +147,7 @@ jobs:
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: bindings-${{ matrix.target }}
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -153,7 +157,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Build FreeBSD
uses: cross-platform-actions/action@v0.25.0
env:
@@ -187,7 +191,7 @@ jobs:
strip -x ${{ env.OXIDE_LOCATION }}/*.node
ls -la ${{ env.OXIDE_LOCATION }}
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: bindings-x86_64-unknown-freebsd
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -207,7 +211,7 @@ jobs:
- build-freebsd
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
fetch-depth: 20
@@ -221,7 +225,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
@@ -229,7 +233,7 @@ jobs:
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
@@ -241,7 +245,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
./crates/node/*.node
@@ -262,7 +266,7 @@ jobs:
run: pnpm --filter=!./playgrounds/* install
- name: Download artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v7
with:
path: ${{ env.OXIDE_LOCATION }}
@@ -301,13 +305,13 @@ jobs:
echo "EOF" >> $GITHUB_ENV
- name: Upload standalone artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: tailwindcss-standalone
path: packages/@tailwindcss-standalone/dist/
- name: Upload npm package tarballs
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: npm-package-tarballs
path: dist/*.tgz
diff --git a/.github/workflows/release-insiders.yml b/.github/workflows/release-insiders.yml
index 53478a227ee4..4852cce7b7a9 100644
--- a/.github/workflows/release-insiders.yml
+++ b/.github/workflows/release-insiders.yml
@@ -9,10 +9,14 @@ permissions:
env:
APP_NAME: tailwindcss-oxide
- NODE_VERSION: 20
+ NODE_VERSION: 24
OXIDE_LOCATION: ./crates/node
RELEASE_CHANNEL: insiders
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
strategy:
@@ -72,11 +76,11 @@ jobs:
container: ${{ matrix.container }}
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
@@ -89,7 +93,7 @@ jobs:
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
@@ -101,7 +105,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
./crates/node/*.node
@@ -116,7 +120,7 @@ jobs:
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
- name: Install Node.JS
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
@@ -142,7 +146,7 @@ jobs:
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: bindings-${{ matrix.target }}
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -152,7 +156,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Build FreeBSD
uses: cross-platform-actions/action@v0.25.0
env:
@@ -186,7 +190,7 @@ jobs:
strip -x ${{ env.OXIDE_LOCATION }}/*.node
ls -la ${{ env.OXIDE_LOCATION }}
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: bindings-x86_64-unknown-freebsd
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -206,7 +210,7 @@ jobs:
- build-freebsd
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
fetch-depth: 20
@@ -218,7 +222,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
@@ -226,7 +230,7 @@ jobs:
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
@@ -238,7 +242,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
./crates/node/*.node
@@ -259,7 +263,7 @@ jobs:
run: pnpm --filter=!./playgrounds/* install
- name: Download artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v7
with:
path: ${{ env.OXIDE_LOCATION }}
@@ -292,7 +296,7 @@ jobs:
run: node ./scripts/lock-pre-release-versions.mjs
- name: Upload npm package tarballs
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: npm-package-tarballs
path: dist/*.tgz
@@ -306,7 +310,7 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Trigger Tailwind Play update
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
script: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8b27773b3dd5..a872900ff9e2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,7 +10,7 @@ permissions:
env:
APP_NAME: tailwindcss-oxide
- NODE_VERSION: 20
+ NODE_VERSION: 24
OXIDE_LOCATION: ./crates/node
jobs:
@@ -72,11 +72,11 @@ jobs:
container: ${{ matrix.container }}
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
@@ -89,7 +89,7 @@ jobs:
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
@@ -101,7 +101,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
./crates/node/*.node
@@ -116,7 +116,7 @@ jobs:
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
- name: Install Node.JS
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
@@ -142,7 +142,7 @@ jobs:
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: bindings-${{ matrix.target }}
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -152,7 +152,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Build FreeBSD
uses: cross-platform-actions/action@v0.25.0
env:
@@ -186,7 +186,7 @@ jobs:
strip -x ${{ env.OXIDE_LOCATION }}/*.node
ls -la ${{ env.OXIDE_LOCATION }}
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: bindings-x86_64-unknown-freebsd
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -206,14 +206,14 @@ jobs:
- build-freebsd
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
fetch-depth: 20
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
@@ -221,7 +221,7 @@ jobs:
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
@@ -233,7 +233,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
./crates/node/*.node
@@ -254,7 +254,7 @@ jobs:
run: pnpm --filter=!./playgrounds/* install
- name: Download artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v7
with:
path: ${{ env.OXIDE_LOCATION }}
@@ -300,7 +300,7 @@ jobs:
- name: Trigger Tailwind Play update
if: env.RELEASE_CHANNEL == 'latest'
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
script: |
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index ded82e2f63f3..000000000000
--- a/.npmrc
+++ /dev/null
@@ -1 +0,0 @@
-auto-install-peers = true
diff --git a/.prettierignore b/.prettierignore
index 3de4530ddcf2..4f50b932fbca 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,5 +4,6 @@ pnpm-lock.yaml
target/
crates/node/index.d.ts
crates/node/index.js
+crates/ignore/
.next
.fingerprint
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2dc2c8629be8..dd549036ed0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
+
+### Fixed
+
+- Allow trailing dash in functional utility names for backwards compatibility ([#19696](https://github.com/tailwindlabs/tailwindcss/pull/19696))
+- Fix missing extracted classes in mdx files containing `.` ([#19711](https://github.com/tailwindlabs/tailwindcss/pull/19711))
+
+## [4.2.0] - 2026-02-18
+
+### Added
+
+- Add mauve, olive, mist, and taupe color palettes to the default theme ([#19627](https://github.com/tailwindlabs/tailwindcss/pull/19627))
+- Add `@tailwindcss/webpack` package to run Tailwind CSS as a webpack plugin ([#19610](https://github.com/tailwindlabs/tailwindcss/pull/19610))
+- Add `pbs-*` and `pbe-*` utilities for `padding-block-start` and `padding-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `mbs-*` and `mbe-*` utilities for `margin-block-start` and `margin-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `scroll-pbs-*` and `scroll-pbe-*` utilities for `scroll-padding-block-start` and `scroll-padding-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `scroll-mbs-*` and `scroll-mbe-*` utilities for `scroll-margin-block-start` and `scroll-margin-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `border-bs-*` and `border-be-*` utilities for `border-block-start` and `border-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `inline-*`, `min-inline-*`, `max-inline-*` utilities for `inline-size`, `min-inline-size`, and `max-inline-size` ([#19612](https://github.com/tailwindlabs/tailwindcss/pull/19612))
+- Add `block-*`, `min-block-*`, `max-block-*` utilities for `block-size`, `min-block-size`, and `max-block-size` ([#19612](https://github.com/tailwindlabs/tailwindcss/pull/19612))
+- Add `inset-s-*`, `inset-e-*`, `inset-bs-*`, `inset-be-*` utilities for `inset-inline-start`, `inset-inline-end`, `inset-block-start`, and `inset-block-end` ([#19613](https://github.com/tailwindlabs/tailwindcss/pull/19613))
+- Add `font-features-*` utility for `font-feature-settings` ([#19623](https://github.com/tailwindlabs/tailwindcss/pull/19615))
+
+### Fixed
+
+- Prevent double `@supports` wrapper for `color-mix` values ([#19450](https://github.com/tailwindlabs/tailwindcss/pull/19450))
+- Allow whitespace around `@source inline()` argument ([#19461](https://github.com/tailwindlabs/tailwindcss/pull/19461))
+- Emit comment when source maps are saved to files when using `@tailwindcss/cli` ([#19447](https://github.com/tailwindlabs/tailwindcss/pull/19447))
+- Detect utilities containing capital letters followed by numbers ([#19465](https://github.com/tailwindlabs/tailwindcss/pull/19465))
+- Fix class extraction for Rails' strict locals ([#19525](https://github.com/tailwindlabs/tailwindcss/pull/19525))
+- Align `@utility` name validation with Oxide scanner rules ([#19524](https://github.com/tailwindlabs/tailwindcss/pull/19524))
+- Fix infinite loop when using `@variant` inside `@custom-variant` ([#19633](https://github.com/tailwindlabs/tailwindcss/pull/19633))
+- Allow multiples of `.25` in `aspect-*` fractions (e.g. `aspect-8.5/11`) ([#19688](https://github.com/tailwindlabs/tailwindcss/pull/19688))
+- Ensure changes to external files listed via `@source` trigger a full page reload when using `@tailwindcss/vite` ([#19670](https://github.com/tailwindlabs/tailwindcss/pull/19670))
+- Improve performance of Oxide scanner in bigger projects by reducing file system walks ([#19632](https://github.com/tailwindlabs/tailwindcss/pull/19632))
+- Ensure import aliases in Astro v5 work without crashing when using `@tailwindcss/vite` ([#19677](https://github.com/tailwindlabs/tailwindcss/issues/19677))
+- Allow escape characters in `@utility` names to improve support with formatters such as Biome ([#19626](https://github.com/tailwindlabs/tailwindcss/pull/19626))
+- Fix incorrect canonicalization results when canonicalizing multiple times ([#19675](https://github.com/tailwindlabs/tailwindcss/pull/19675))
+- Add `.jj` to default ignored content directories ([#19687](https://github.com/tailwindlabs/tailwindcss/pull/19687))
+
+### Deprecated
+
+- Deprecate `start-*` and `end-*` utilities in favor of `inset-s-*` and `inset-e-*` utilities ([#19613](https://github.com/tailwindlabs/tailwindcss/pull/19613))
+
+## [4.1.18] - 2025-12-11
+
### Fixed
- Ensure validation of `source(…)` happens relative to the file it is in ([#19274](https://github.com/tailwindlabs/tailwindcss/pull/19274))
@@ -26,9 +74,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Don’t emit color-mix fallback rules inside `@keyframes` ([#19419](https://github.com/tailwindlabs/tailwindcss/pull/19419))
- CLI: Don't hang when output is `/dev/stdout` ([#19421](https://github.com/tailwindlabs/tailwindcss/pull/19421))
-### Added
+## [3.4.19] - 2025-12-10
-- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
+### Fixed
+
+- Don’t break `sibling-*()` functions when used inside `calc(…)` ([#19335](https://github.com/tailwindlabs/tailwindcss/pull/19335))
## [4.1.17] - 2025-11-06
@@ -86,6 +136,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Upgrade: Ensure first class inside `className` is migrated ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031))
- Upgrade: Migrate classes inside `*ClassName` and `*Class` attributes ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031))
+## [3.4.18] - 2025-10-01
+
+### Fixed
+
+- Improve support for raw `supports-[…]` queries in arbitrary values ([#13605](https://github.com/tailwindlabs/tailwindcss/pull/13605))
+- Fix `require.cache` error when loaded through a TypeScript file in Node 22.18+ ([#18665](https://github.com/tailwindlabs/tailwindcss/pull/18665))
+- Support `import.meta.resolve(…)` in configs for new enough Node.js versions ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
+- Allow using newer versions of `postcss-load-config` for better ESM and TypeScript PostCSS config support with the CLI ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
+- Remove irrelevant utility rules when matching important classes ([#19030](https://github.com/tailwindlabs/tailwindcss/pull/19030))
+
## [4.1.13] - 2025-09-03
### Changed
@@ -1418,16 +1478,6 @@ For a deep-dive into everything that's new, [check out the announcement post](ht
- First 4.0.0-alpha.1 release
-## [3.4.18] - 2024-10-01
-
-### Fixed
-
-- Improve support for raw `supports-[…]` queries in arbitrary values ([#13605](https://github.com/tailwindlabs/tailwindcss/pull/13605))
-- Fix `require.cache` error when loaded through a TypeScript file in Node 22.18+ ([#18665](https://github.com/tailwindlabs/tailwindcss/pull/18665))
-- Support `import.meta.resolve(…)` in configs for new enough Node.js versions ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
-- Allow using newer versions of `postcss-load-config` for better ESM and TypeScript PostCSS config support with the CLI ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
-- Remove irrelevant utility rules when matching important classes ([#19030](https://github.com/tailwindlabs/tailwindcss/pull/19030))
-
## [3.4.17] - 2024-12-17
### Fixed
@@ -3896,11 +3946,15 @@ No release notes
- Everything!
-[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.17...HEAD
+[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.0...HEAD
+[4.2.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.18...v4.2.0
+[4.1.18]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.17...v4.1.18
+[3.4.19]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.18...v3.4.19
[4.1.17]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.16...v4.1.17
[4.1.16]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.15...v4.1.16
[4.1.15]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.14...v4.1.15
[4.1.14]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.13...v4.1.14
+[3.4.18]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v3.4.18
[4.1.13]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.12...v4.1.13
[4.1.12]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.11...v4.1.12
[4.1.11]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.10...v4.1.11
@@ -3958,7 +4012,6 @@ No release notes
[4.0.0-alpha.24]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.0-alpha.23...v4.0.0-alpha.24
[4.0.0-alpha.23]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.0-alpha.22...v4.0.0-alpha.23
[4.0.0-alpha.22]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v4.0.0-alpha.22
-[3.4.18]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v3.4.18
[3.4.17]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.16...v3.4.17
[3.4.16]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.15...v3.4.16
[3.4.15]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.14...v3.4.15
diff --git a/README.md b/README.md
index 7d21bd88385a..5f532607d00a 100644
--- a/README.md
+++ b/README.md
@@ -13,10 +13,10 @@
-
+
-
-
+
+
---
@@ -29,8 +29,8 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com).
For help, discussion about best practices, or feature ideas:
-[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
+[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions)
## Contributing
-If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**.
+If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**.
diff --git a/crates/node/npm/android-arm-eabi/package.json b/crates/node/npm/android-arm-eabi/package.json
index f1adb084c4ab..cba6fedce0b2 100644
--- a/crates/node/npm/android-arm-eabi/package.json
+++ b/crates/node/npm/android-arm-eabi/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-android-arm-eabi",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,6 +22,6 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
}
}
diff --git a/crates/node/npm/android-arm64/package.json b/crates/node/npm/android-arm64/package.json
index 53de99c88d67..0950b7a03e95 100644
--- a/crates/node/npm/android-arm64/package.json
+++ b/crates/node/npm/android-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-android-arm64",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,6 +22,6 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
}
}
diff --git a/crates/node/npm/darwin-arm64/package.json b/crates/node/npm/darwin-arm64/package.json
index 51a49b2052bd..423333d61dcf 100644
--- a/crates/node/npm/darwin-arm64/package.json
+++ b/crates/node/npm/darwin-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-darwin-arm64",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,6 +22,6 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
}
}
diff --git a/crates/node/npm/darwin-x64/package.json b/crates/node/npm/darwin-x64/package.json
index 46ef28397dc3..81007674ee5c 100644
--- a/crates/node/npm/darwin-x64/package.json
+++ b/crates/node/npm/darwin-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-darwin-x64",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,6 +22,6 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
}
}
diff --git a/crates/node/npm/freebsd-x64/package.json b/crates/node/npm/freebsd-x64/package.json
index b52abc69fc53..8c25927edc39 100644
--- a/crates/node/npm/freebsd-x64/package.json
+++ b/crates/node/npm/freebsd-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-freebsd-x64",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,6 +22,6 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
}
}
diff --git a/crates/node/npm/linux-arm-gnueabihf/package.json b/crates/node/npm/linux-arm-gnueabihf/package.json
index b64401cd44ca..b2a4c0281127 100644
--- a/crates/node/npm/linux-arm-gnueabihf/package.json
+++ b/crates/node/npm/linux-arm-gnueabihf/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,6 +22,6 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
}
}
diff --git a/crates/node/npm/linux-arm64-gnu/package.json b/crates/node/npm/linux-arm64-gnu/package.json
index b5b464bb0080..e1855f2894c3 100644
--- a/crates/node/npm/linux-arm64-gnu/package.json
+++ b/crates/node/npm/linux-arm64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm64-gnu",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,7 +22,7 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
},
"libc": [
"glibc"
diff --git a/crates/node/npm/linux-arm64-musl/package.json b/crates/node/npm/linux-arm64-musl/package.json
index e59e369523cd..c6b96ab8ab14 100644
--- a/crates/node/npm/linux-arm64-musl/package.json
+++ b/crates/node/npm/linux-arm64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm64-musl",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,7 +22,7 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
},
"libc": [
"musl"
diff --git a/crates/node/npm/linux-x64-gnu/package.json b/crates/node/npm/linux-x64-gnu/package.json
index bd2e7638d95b..2d4fa6fe3c84 100644
--- a/crates/node/npm/linux-x64-gnu/package.json
+++ b/crates/node/npm/linux-x64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-x64-gnu",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,7 +22,7 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
},
"libc": [
"glibc"
diff --git a/crates/node/npm/linux-x64-musl/package.json b/crates/node/npm/linux-x64-musl/package.json
index fc69e5035542..5855b5d6a331 100644
--- a/crates/node/npm/linux-x64-musl/package.json
+++ b/crates/node/npm/linux-x64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-x64-musl",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,7 +22,7 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
},
"libc": [
"musl"
diff --git a/crates/node/npm/wasm32-wasi/package.json b/crates/node/npm/wasm32-wasi/package.json
index e7c1184f348b..0c1defca3a43 100644
--- a/crates/node/npm/wasm32-wasi/package.json
+++ b/crates/node/npm/wasm32-wasi/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-wasm32-wasi",
- "version": "4.1.17",
+ "version": "4.2.0",
"cpu": [
"wasm32"
],
@@ -27,12 +27,12 @@
},
"browser": "tailwindcss-oxide.wasi-browser.js",
"dependencies": {
- "@napi-rs/wasm-runtime": "^1.0.7",
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1",
+ "@napi-rs/wasm-runtime": "^1.1.1",
+ "@emnapi/core": "^1.8.1",
+ "@emnapi/runtime": "^1.8.1",
"@tybys/wasm-util": "^0.10.1",
"@emnapi/wasi-threads": "^1.1.0",
- "tslib": "^2.4.0"
+ "tslib": "^2.8.1"
},
"bundledDependencies": [
"@napi-rs/wasm-runtime",
diff --git a/crates/node/npm/win32-arm64-msvc/package.json b/crates/node/npm/win32-arm64-msvc/package.json
index 13853d380cfc..633a4d12eb9c 100644
--- a/crates/node/npm/win32-arm64-msvc/package.json
+++ b/crates/node/npm/win32-arm64-msvc/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-win32-arm64-msvc",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,6 +22,6 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
}
}
diff --git a/crates/node/npm/win32-x64-msvc/package.json b/crates/node/npm/win32-x64-msvc/package.json
index b6f9f768b0b4..99dca35d18c4 100644
--- a/crates/node/npm/win32-x64-msvc/package.json
+++ b/crates/node/npm/win32-x64-msvc/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-win32-x64-msvc",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -22,6 +22,6 @@
},
"license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
}
}
diff --git a/crates/node/package.json b/crates/node/package.json
index 037d0c354dd0..6da669ced184 100644
--- a/crates/node/package.json
+++ b/crates/node/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide",
- "version": "4.1.17",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -33,12 +33,12 @@
},
"license": "MIT",
"devDependencies": {
- "@napi-rs/cli": "^3.4.1",
- "@napi-rs/wasm-runtime": "^1.0.7",
- "emnapi": "1.7.1"
+ "@napi-rs/cli": "3.4.1",
+ "@napi-rs/wasm-runtime": "^1.1.1",
+ "emnapi": "1.8.1"
},
"engines": {
- "node": ">= 10"
+ "node": ">= 20"
},
"files": [
"index.js",
diff --git a/crates/oxide/src/cursor.rs b/crates/oxide/src/cursor.rs
index 818a0ef1b450..71cdccdf6aeb 100644
--- a/crates/oxide/src/cursor.rs
+++ b/crates/oxide/src/cursor.rs
@@ -1,44 +1,49 @@
use std::{ascii::escape_default, fmt::Display};
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Copy)]
pub struct Cursor<'a> {
// The input we're scanning
pub input: &'a [u8],
// The location of the cursor in the input
pub pos: usize,
-
- /// Is the cursor at the start of the input
- pub at_start: bool,
-
- /// Is the cursor at the end of the input
- pub at_end: bool,
-
- /// The previously consumed character
- /// If `at_start` is true, this will be NUL
- pub prev: u8,
-
- /// The current character
- pub curr: u8,
-
- /// The upcoming character (if any)
- /// If `at_end` is true, this will be NUL
- pub next: u8,
}
impl<'a> Cursor<'a> {
+ #[inline(always)]
pub fn new(input: &'a [u8]) -> Self {
- let mut cursor = Self {
- input,
- pos: 0,
- at_start: true,
- at_end: false,
- prev: 0x00,
- curr: 0x00,
- next: 0x00,
- };
- cursor.move_to(0);
- cursor
+ Self { input, pos: 0 }
+ }
+
+ /// The current byte at `pos`, or 0x00 if past the end.
+ #[inline(always)]
+ pub fn curr(&self) -> u8 {
+ if self.pos < self.input.len() {
+ unsafe { *self.input.get_unchecked(self.pos) }
+ } else {
+ 0x00
+ }
+ }
+
+ /// The next byte at `pos + 1`, or 0x00 if past the end.
+ #[inline(always)]
+ pub fn next(&self) -> u8 {
+ let next_pos = self.pos + 1;
+ if next_pos < self.input.len() {
+ unsafe { *self.input.get_unchecked(next_pos) }
+ } else {
+ 0x00
+ }
+ }
+
+ /// The previous byte at `pos - 1`, or 0x00 if at the start.
+ #[inline(always)]
+ pub fn prev(&self) -> u8 {
+ if self.pos > 0 {
+ unsafe { *self.input.get_unchecked(self.pos - 1) }
+ } else {
+ 0x00
+ }
}
pub fn advance_by(&mut self, amount: usize) {
@@ -48,38 +53,15 @@ impl<'a> Cursor<'a> {
#[inline(always)]
pub fn advance(&mut self) {
self.pos += 1;
-
- self.prev = self.curr;
- self.curr = self.next;
- self.next = *self
- .input
- .get(self.pos.saturating_add(1))
- .unwrap_or(&0x00u8);
}
#[inline(always)]
pub fn advance_twice(&mut self) {
self.pos += 2;
-
- self.prev = self.next;
- self.curr = *self.input.get(self.pos).unwrap_or(&0x00u8);
- self.next = *self
- .input
- .get(self.pos.saturating_add(1))
- .unwrap_or(&0x00u8);
}
pub fn move_to(&mut self, pos: usize) {
- let len = self.input.len();
- let pos = pos.clamp(0, len);
-
- self.pos = pos;
- self.at_start = pos == 0;
- self.at_end = pos + 1 >= len;
-
- self.prev = *self.input.get(pos.wrapping_sub(1)).unwrap_or(&0x00u8);
- self.curr = *self.input.get(pos).unwrap_or(&0x00u8);
- self.next = *self.input.get(pos.saturating_add(1)).unwrap_or(&0x00u8);
+ self.pos = pos.min(self.input.len());
}
}
@@ -90,9 +72,9 @@ impl Display for Cursor<'_> {
let pos = format!("{: >len_count$}", self.pos, len_count = len.len());
write!(f, "{}/{} ", pos, len)?;
- if self.at_start {
+ if self.pos == 0 {
write!(f, "S ")?;
- } else if self.at_end {
+ } else if self.pos + 1 >= self.input.len() {
write!(f, "E ")?;
} else {
write!(f, "M ")?;
@@ -109,9 +91,9 @@ impl Display for Cursor<'_> {
write!(
f,
"[{} {} {}]",
- to_str(self.prev),
- to_str(self.curr),
- to_str(self.next)
+ to_str(self.prev()),
+ to_str(self.curr()),
+ to_str(self.next())
)
}
}
@@ -125,36 +107,28 @@ mod test {
fn test_cursor() {
let mut cursor = Cursor::new(b"hello world");
assert_eq!(cursor.pos, 0);
- assert!(cursor.at_start);
- assert!(!cursor.at_end);
- assert_eq!(cursor.prev, 0x00);
- assert_eq!(cursor.curr, b'h');
- assert_eq!(cursor.next, b'e');
+ assert_eq!(cursor.prev(), 0x00);
+ assert_eq!(cursor.curr(), b'h');
+ assert_eq!(cursor.next(), b'e');
cursor.advance_by(1);
assert_eq!(cursor.pos, 1);
- assert!(!cursor.at_start);
- assert!(!cursor.at_end);
- assert_eq!(cursor.prev, b'h');
- assert_eq!(cursor.curr, b'e');
- assert_eq!(cursor.next, b'l');
+ assert_eq!(cursor.prev(), b'h');
+ assert_eq!(cursor.curr(), b'e');
+ assert_eq!(cursor.next(), b'l');
// Advancing too far should stop at the end
cursor.advance_by(10);
assert_eq!(cursor.pos, 11);
- assert!(!cursor.at_start);
- assert!(cursor.at_end);
- assert_eq!(cursor.prev, b'd');
- assert_eq!(cursor.curr, 0x00);
- assert_eq!(cursor.next, 0x00);
+ assert_eq!(cursor.prev(), b'd');
+ assert_eq!(cursor.curr(), 0x00);
+ assert_eq!(cursor.next(), 0x00);
// Can't advance past the end
cursor.advance_by(1);
assert_eq!(cursor.pos, 11);
- assert!(!cursor.at_start);
- assert!(cursor.at_end);
- assert_eq!(cursor.prev, b'd');
- assert_eq!(cursor.curr, 0x00);
- assert_eq!(cursor.next, 0x00);
+ assert_eq!(cursor.prev(), b'd');
+ assert_eq!(cursor.curr(), 0x00);
+ assert_eq!(cursor.next(), 0x00);
}
}
diff --git a/crates/oxide/src/extractor/arbitrary_property_machine.rs b/crates/oxide/src/extractor/arbitrary_property_machine.rs
index 8fd13e39405f..a4325333925c 100644
--- a/crates/oxide/src/extractor/arbitrary_property_machine.rs
+++ b/crates/oxide/src/extractor/arbitrary_property_machine.rs
@@ -74,7 +74,7 @@ impl Machine for ArbitraryPropertyMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Start of an arbitrary property
Class::OpenBracket => {
self.start_pos = cursor.pos;
@@ -97,8 +97,8 @@ impl Machine for ArbitraryPropertyMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Dash => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Dash => match cursor.next().into() {
// Start of a CSS variable
//
// E.g.: `[--my-color:red]`
@@ -137,7 +137,7 @@ impl ArbitraryPropertyMachine {
fn parse_property_variable(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.css_variable_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// End of the CSS variable, must be followed by a `:`
//
// E.g.: `[--my-color:red]`
@@ -165,8 +165,8 @@ impl Machine for ArbitraryPropertyMachine {
let len = cursor.input.len();
let start_of_value_pos = cursor.pos;
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Escape => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
//
// E.g.: `[color:var(--my-\ color)]`
@@ -181,7 +181,7 @@ impl Machine for ArbitraryPropertyMachine {
},
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
- if !self.bracket_stack.push(cursor.curr) {
+ if !self.bracket_stack.push(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -190,7 +190,7 @@ impl Machine for ArbitraryPropertyMachine {
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
if !self.bracket_stack.is_empty() =>
{
- if !self.bracket_stack.pop(cursor.curr) {
+ if !self.bracket_stack.pop(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -227,7 +227,7 @@ impl Machine for ArbitraryPropertyMachine {
Class::Slash if start_of_value_pos == cursor.pos => return self.restart(),
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
- Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
+ Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
return self.restart()
}
diff --git a/crates/oxide/src/extractor/arbitrary_value_machine.rs b/crates/oxide/src/extractor/arbitrary_value_machine.rs
index f5a612df13f2..cbef62c42681 100644
--- a/crates/oxide/src/extractor/arbitrary_value_machine.rs
+++ b/crates/oxide/src/extractor/arbitrary_value_machine.rs
@@ -32,7 +32,7 @@ impl Machine for ArbitraryValueMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
// An arbitrary value must start with an open bracket
- if Class::OpenBracket != cursor.curr.into() {
+ if Class::OpenBracket != cursor.curr().into() {
return MachineState::Idle;
}
@@ -42,8 +42,8 @@ impl Machine for ArbitraryValueMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Escape => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
//
// E.g.: `[color:var(--my-\ color)]`
@@ -61,7 +61,7 @@ impl Machine for ArbitraryValueMachine {
},
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
- if !self.bracket_stack.push(cursor.curr) {
+ if !self.bracket_stack.push(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -70,7 +70,7 @@ impl Machine for ArbitraryValueMachine {
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
if !self.bracket_stack.is_empty() =>
{
- if !self.bracket_stack.pop(cursor.curr) {
+ if !self.bracket_stack.pop(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -96,7 +96,7 @@ impl Machine for ArbitraryValueMachine {
Class::Whitespace => return self.restart(),
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
- Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
+ Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
return self.restart()
}
diff --git a/crates/oxide/src/extractor/arbitrary_variable_machine.rs b/crates/oxide/src/extractor/arbitrary_variable_machine.rs
index 6990e110b087..e8d4755571c7 100644
--- a/crates/oxide/src/extractor/arbitrary_variable_machine.rs
+++ b/crates/oxide/src/extractor/arbitrary_variable_machine.rs
@@ -80,13 +80,13 @@ impl Machine for ArbitraryVariableMachine {
#[inline(always)]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Arbitrary variables start with `(` followed by a CSS variable
//
// E.g.: `(--my-variable)`
// ^^
//
- Class::OpenParen => match cursor.next.into() {
+ Class::OpenParen => match cursor.next().into() {
Class::Dash => {
self.start_pos = cursor.pos;
cursor.advance();
@@ -117,7 +117,7 @@ impl Machine for ArbitraryVariableMachine {
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.css_variable_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// A CSS variable followed by a `,` means that there is a fallback
//
// E.g.: `(--my-color,red)`
@@ -134,7 +134,7 @@ impl Machine for ArbitraryVariableMachine {
_ => {
cursor.advance();
- match cursor.curr.into() {
+ match cursor.curr().into() {
// End of an arbitrary variable, must be followed by `)`
Class::CloseParen => self.done(self.start_pos, cursor),
@@ -156,7 +156,7 @@ impl Machine for ArbitraryVariableMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Valid data type characters
//
// E.g.: `(length:--my-length)`
@@ -169,7 +169,7 @@ impl Machine for ArbitraryVariableMachine {
//
// E.g.: `(length:--my-length)`
// ^
- Class::Colon => match cursor.next.into() {
+ Class::Colon => match cursor.next().into() {
Class::Dash => {
cursor.advance();
return self.transition::().next(cursor);
@@ -196,8 +196,8 @@ impl Machine for ArbitraryVariableMachine {
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Escape => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
//
// E.g.: `(--my-\ color)`
@@ -212,7 +212,7 @@ impl Machine for ArbitraryVariableMachine {
},
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
- if !self.bracket_stack.push(cursor.curr) {
+ if !self.bracket_stack.push(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -221,7 +221,7 @@ impl Machine for ArbitraryVariableMachine {
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
if !self.bracket_stack.is_empty() =>
{
- if !self.bracket_stack.pop(cursor.curr) {
+ if !self.bracket_stack.pop(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -253,7 +253,7 @@ impl Machine for ArbitraryVariableMachine {
Class::Whitespace => return self.restart(),
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
- Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
+ Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
return self.restart()
}
diff --git a/crates/oxide/src/extractor/candidate_machine.rs b/crates/oxide/src/extractor/candidate_machine.rs
index c82cc3707993..a755cf02a2b5 100644
--- a/crates/oxide/src/extractor/candidate_machine.rs
+++ b/crates/oxide/src/extractor/candidate_machine.rs
@@ -32,14 +32,14 @@ impl Machine for CandidateMachine {
while cursor.pos < len {
// Skip ahead for known characters that will never be part of a candidate. No need to
// run any sub-machines.
- if cursor.curr.is_ascii_whitespace() {
+ if cursor.curr().is_ascii_whitespace() {
self.reset();
cursor.advance();
continue;
}
// Candidates don't start with these characters, so we can skip ahead.
- if matches!(cursor.curr, b':' | b'"' | b'\'' | b'`') {
+ if matches!(cursor.curr(), b':' | b'"' | b'\'' | b'`') {
self.reset();
cursor.advance();
continue;
@@ -56,7 +56,7 @@ impl Machine for CandidateMachine {
// E.g.: `Some Class`
// ^ ^ Invalid, we can jump ahead to the next boundary
//
- if matches!(cursor.curr, b'<' | b'A'..=b'Z') {
+ if matches!(cursor.curr(), b'<' | b'A'..=b'Z') {
if let Some(offset) = cursor.input[cursor.pos..]
.iter()
.position(|&c| is_valid_before_boundary(&c))
diff --git a/crates/oxide/src/extractor/css_variable_machine.rs b/crates/oxide/src/extractor/css_variable_machine.rs
index 4e19c7111f5d..7beb827ee84b 100644
--- a/crates/oxide/src/extractor/css_variable_machine.rs
+++ b/crates/oxide/src/extractor/css_variable_machine.rs
@@ -20,7 +20,7 @@ impl Machine for CssVariableMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
// CSS Variables must start with `--`
- if Class::Dash != cursor.curr.into() || Class::Dash != cursor.next.into() {
+ if Class::Dash != cursor.curr().into() || Class::Dash != cursor.next().into() {
return MachineState::Idle;
}
@@ -30,11 +30,11 @@ impl Machine for CssVariableMachine {
cursor.advance_twice();
while cursor.pos < len {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
//
Class::AllowedCharacter | Class::Dash => {
- match cursor.next.into() {
+ match cursor.next().into() {
// Valid character followed by a valid character or an escape character
//
// E.g.: `--my-variable`
@@ -58,7 +58,7 @@ impl Machine for CssVariableMachine {
}
}
- Class::Escape => match cursor.next.into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
//
// In CSS it is allowed, but in the context of a class it's not because then we
diff --git a/crates/oxide/src/extractor/mod.rs b/crates/oxide/src/extractor/mod.rs
index add0de4acaca..a37d6a0a49b3 100644
--- a/crates/oxide/src/extractor/mod.rs
+++ b/crates/oxide/src/extractor/mod.rs
@@ -86,7 +86,7 @@ impl<'a> Extractor<'a> {
{
let cursor = &mut self.cursor.clone();
while cursor.pos < len {
- if cursor.curr.is_ascii_whitespace() {
+ if cursor.curr().is_ascii_whitespace() {
cursor.advance();
continue;
}
@@ -104,7 +104,7 @@ impl<'a> Extractor<'a> {
let cursor = &mut self.cursor.clone();
while cursor.pos < len {
- if cursor.curr.is_ascii_whitespace() {
+ if cursor.curr().is_ascii_whitespace() {
cursor.advance();
continue;
}
@@ -147,7 +147,7 @@ impl<'a> Extractor<'a> {
let cursor = &mut self.cursor.clone();
while cursor.pos < len {
- if cursor.curr.is_ascii_whitespace() {
+ if cursor.curr().is_ascii_whitespace() {
cursor.advance();
continue;
}
@@ -238,7 +238,7 @@ mod tests {
use std::hint::black_box;
fn pre_process_input(input: &str, extension: &str) -> String {
- let input = crate::scanner::pre_process_input(input.as_bytes(), extension);
+ let input = crate::scanner::pre_process_input(input.as_bytes().to_vec(), extension);
String::from_utf8(input).unwrap()
}
diff --git a/crates/oxide/src/extractor/modifier_machine.rs b/crates/oxide/src/extractor/modifier_machine.rs
index 22668a828749..811bc9ded9e0 100644
--- a/crates/oxide/src/extractor/modifier_machine.rs
+++ b/crates/oxide/src/extractor/modifier_machine.rs
@@ -31,14 +31,14 @@ impl Machine for ModifierMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
// A modifier must start with a `/`, everything else is not a valid start of a modifier
- if Class::Slash != cursor.curr.into() {
+ if Class::Slash != cursor.curr().into() {
return MachineState::Idle;
}
let start_pos = cursor.pos;
cursor.advance();
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Start of an arbitrary value:
//
// ```
@@ -70,9 +70,9 @@ impl Machine for ModifierMachine {
Class::ValidStart => {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
+ match cursor.curr().into() {
Class::ValidStart | Class::ValidInside => {
- match cursor.next.into() {
+ match cursor.next().into() {
// Only valid characters are allowed, if followed by another valid character
Class::ValidStart | Class::ValidInside => cursor.advance(),
diff --git a/crates/oxide/src/extractor/named_utility_machine.rs b/crates/oxide/src/extractor/named_utility_machine.rs
index 7c847a2e2915..24dc027b2d69 100644
--- a/crates/oxide/src/extractor/named_utility_machine.rs
+++ b/crates/oxide/src/extractor/named_utility_machine.rs
@@ -52,8 +52,8 @@ impl Machine for NamedUtilityMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
- Class::AlphaLower => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::AlphaLower => match cursor.next().into() {
// Valid single character utility in between quotes
//
// E.g.: ``
@@ -90,7 +90,7 @@ impl Machine for NamedUtilityMachine {
//
// E.g.: `-mx-2.5`
// ^^
- Class::Dash => match cursor.next.into() {
+ Class::Dash => match cursor.next().into() {
Class::AlphaLower => {
self.start_pos = cursor.pos;
cursor.advance();
@@ -118,7 +118,7 @@ impl Machine for NamedUtilityMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Followed by a boundary character, we are at the end of the utility.
//
// E.g.: `'flex'`
@@ -132,14 +132,14 @@ impl Machine for NamedUtilityMachine {
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
// ^
Class::AlphaLower | Class::AlphaUpper => {
- if is_valid_after_boundary(&cursor.next) || {
+ if is_valid_after_boundary(&cursor.next()) || {
// Or any of these characters
//
// - `:`, because of JS object keys
// - `/`, because of modifiers
// - `!`, because of important
matches!(
- cursor.next.into(),
+ cursor.next().into(),
Class::Colon | Class::Slash | Class::Exclamation
)
} {
@@ -150,7 +150,7 @@ impl Machine for NamedUtilityMachine {
cursor.advance()
}
- Class::Dash => match cursor.next.into() {
+ Class::Dash => match cursor.next().into() {
// Start of an arbitrary value
//
// E.g.: `bg-[#0088cc]`
@@ -196,7 +196,7 @@ impl Machine for NamedUtilityMachine {
_ => return self.restart(),
},
- Class::Underscore => match cursor.next.into() {
+ Class::Underscore => match cursor.next().into() {
// Valid characters _if_ followed by another valid character. These characters are
// only valid inside of the utility but not at the end of the utility.
//
@@ -225,14 +225,14 @@ impl Machine for NamedUtilityMachine {
// ^
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
// ^
- _ if is_valid_after_boundary(&cursor.next) || {
+ _ if is_valid_after_boundary(&cursor.next()) || {
// Or any of these characters
//
// - `:`, because of JS object keys
// - `/`, because of modifiers
// - `!`, because of important
matches!(
- cursor.next.into(),
+ cursor.next().into(),
Class::Colon | Class::Slash | Class::Exclamation
)
} =>
@@ -249,11 +249,11 @@ impl Machine for NamedUtilityMachine {
// E.g.: `px-2.5`
// ^^^
Class::Dot => {
- if !matches!(cursor.prev.into(), Class::Number) {
+ if !matches!(cursor.prev().into(), Class::Number) {
return self.restart();
}
- if !matches!(cursor.next.into(), Class::Number) {
+ if !matches!(cursor.next().into(), Class::Number) {
return self.restart();
}
@@ -278,18 +278,19 @@ impl Machine for NamedUtilityMachine {
//
Class::Number => {
if !matches!(
- cursor.prev.into(),
+ cursor.prev().into(),
Class::Dash
| Class::Underscore
| Class::Dot
| Class::Number
| Class::AlphaLower
+ | Class::AlphaUpper
) {
return self.restart();
}
if !matches!(
- cursor.next.into(),
+ cursor.next().into(),
Class::Dot
| Class::Number
| Class::AlphaLower
@@ -313,7 +314,7 @@ impl Machine for NamedUtilityMachine {
// ^^
// ```
Class::Percent => {
- if !matches!(cursor.prev.into(), Class::Number) {
+ if !matches!(cursor.prev().into(), Class::Number) {
return self.restart();
}
@@ -425,6 +426,8 @@ mod tests {
// With number followed by dash or underscore
("text-title1-strong", vec!["text-title1-strong"]),
("text-title1_strong", vec!["text-title1_strong"]),
+ // With capital letter followed by number
+ ("text-titleV1-strong", vec!["text-titleV1-strong"]),
// With trailing % sign
("from-15%", vec!["from-15%"]),
// Arbitrary value with bracket notation
diff --git a/crates/oxide/src/extractor/named_variant_machine.rs b/crates/oxide/src/extractor/named_variant_machine.rs
index b6de8220b4cb..285fe6ab51a6 100644
--- a/crates/oxide/src/extractor/named_variant_machine.rs
+++ b/crates/oxide/src/extractor/named_variant_machine.rs
@@ -81,8 +81,8 @@ impl Machine for NamedVariantMachine {
#[inline(always)]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
- Class::AlphaLower | Class::Star => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::AlphaLower | Class::Star => match cursor.next().into() {
// Valid single character variant, must be followed by a `:`
//
// E.g.: ``
@@ -134,8 +134,8 @@ impl Machine for NamedVariantMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Dash => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Dash => match cursor.next().into() {
// Start of an arbitrary value
//
// E.g.: `data-[state=pending]:`.
@@ -192,7 +192,7 @@ impl Machine for NamedVariantMachine {
};
}
- Class::Underscore => match cursor.next.into() {
+ Class::Underscore => match cursor.next().into() {
// Valid characters _if_ followed by another valid character. These characters are
// only valid inside of the variant but not at the end of the variant.
//
@@ -240,11 +240,11 @@ impl Machine for NamedVariantMachine {
// E.g.: `2.5xl:flex`
// ^^^
Class::Dot => {
- if !matches!(cursor.prev.into(), Class::Number) {
+ if !matches!(cursor.prev().into(), Class::Number) {
return self.restart();
}
- if !matches!(cursor.next.into(), Class::Number) {
+ if !matches!(cursor.next().into(), Class::Number) {
return self.restart();
}
@@ -263,7 +263,7 @@ impl Machine for NamedVariantMachine {
impl NamedVariantMachine {
#[inline(always)]
fn parse_arbitrary_end(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.next.into() {
+ match cursor.next().into() {
Class::Slash => {
cursor.advance();
self.transition::().next(cursor)
@@ -285,7 +285,7 @@ impl Machine for NamedVariantMachine {
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.modifier_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// Modifier must be followed by a `:`
//
// E.g.: `group-hover/name:`
@@ -308,7 +308,7 @@ impl Machine for NamedVariantMachine {
#[inline(always)]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// The end of a variant must be the `:`
//
// E.g.: `hover:`
diff --git a/crates/oxide/src/extractor/pre_processors/clojure.rs b/crates/oxide/src/extractor/pre_processors/clojure.rs
index ecf02b4ff634..3fb4ce601cd6 100644
--- a/crates/oxide/src/extractor/pre_processors/clojure.rs
+++ b/crates/oxide/src/extractor/pre_processors/clojure.rs
@@ -32,14 +32,14 @@ impl PreProcessor for Clojure {
let mut cursor = cursor::Cursor::new(&content);
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Consume strings as-is
b'"' => {
result[cursor.pos] = b' ';
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -57,8 +57,8 @@ impl PreProcessor for Clojure {
// Discard line comments until the end of the line.
// Comments start with `;;`
- b';' if matches!(cursor.next, b';') => {
- while cursor.pos < len && cursor.curr != b'\n' {
+ b';' if matches!(cursor.next(), b';') => {
+ while cursor.pos < len && cursor.curr() != b'\n' {
result[cursor.pos] = b' ';
cursor.advance();
}
@@ -70,7 +70,7 @@ impl PreProcessor for Clojure {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// A `.` surrounded by digits is a decimal number, so we don't want to replace it.
//
// E.g.:
@@ -78,8 +78,8 @@ impl PreProcessor for Clojure {
// gap-1.5
// ^
// ```
- b'.' if cursor.prev.is_ascii_digit()
- && cursor.next.is_ascii_digit() =>
+ b'.' if cursor.prev().is_ascii_digit()
+ && cursor.next().is_ascii_digit() =>
{
// Keep the `.` as-is
}
@@ -95,7 +95,7 @@ impl PreProcessor for Clojure {
result[cursor.pos] = b' ';
}
// End of keyword.
- _ if !is_keyword_character(cursor.curr) => {
+ _ if !is_keyword_character(cursor.curr()) => {
result[cursor.pos] = b' ';
break;
}
@@ -110,11 +110,11 @@ impl PreProcessor for Clojure {
// Handle quote with a list, e.g.: `'(…)`
// and with a vector, e.g.: `'[…]`
- b'\'' if matches!(cursor.next, b'[' | b'(') => {
+ b'\'' if matches!(cursor.next(), b'[' | b'(') => {
result[cursor.pos] = b' ';
cursor.advance();
result[cursor.pos] = b' ';
- let end = match cursor.curr {
+ let end = match cursor.curr() {
b'[' => b']',
b'(' => b')',
_ => unreachable!(),
@@ -122,7 +122,7 @@ impl PreProcessor for Clojure {
// Consume until the closing `]`
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
x if x == end => {
result[cursor.pos] = b' ';
break;
@@ -134,7 +134,7 @@ impl PreProcessor for Clojure {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -157,14 +157,14 @@ impl PreProcessor for Clojure {
}
// Handle quote with a keyword, e.g.: `'bg-white`
- b'\'' if !cursor.next.is_ascii_whitespace() => {
+ b'\'' if !cursor.next().is_ascii_whitespace() => {
result[cursor.pos] = b' ';
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// End of keyword.
- _ if !is_keyword_character(cursor.curr) => {
+ _ if !is_keyword_character(cursor.curr()) => {
result[cursor.pos] = b' ';
break;
}
diff --git a/crates/oxide/src/extractor/pre_processors/elixir.rs b/crates/oxide/src/extractor/pre_processors/elixir.rs
index 87b89a2a9dda..7737a6b2d1ae 100644
--- a/crates/oxide/src/extractor/pre_processors/elixir.rs
+++ b/crates/oxide/src/extractor/pre_processors/elixir.rs
@@ -13,13 +13,13 @@ impl PreProcessor for Elixir {
while cursor.pos < content.len() {
// Look for a sigil marker
- if cursor.curr != b'~' {
+ if cursor.curr() != b'~' {
cursor.advance();
continue;
}
// Scan charlists, strings, and wordlists
- if !matches!(cursor.next, b'c' | b'C' | b's' | b'S' | b'w' | b'W') {
+ if !matches!(cursor.next(), b'c' | b'C' | b's' | b'S' | b'w' | b'W') {
cursor.advance();
continue;
}
@@ -27,7 +27,7 @@ impl PreProcessor for Elixir {
cursor.advance_twice();
// Match the opening for a sigil
- if !matches!(cursor.curr, b'(' | b'[' | b'{') {
+ if !matches!(cursor.curr(), b'(' | b'[' | b'{') {
continue;
}
@@ -35,19 +35,19 @@ impl PreProcessor for Elixir {
result[cursor.pos] = b' ';
// Scan until we find a balanced closing one and replace it too
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
while cursor.pos < content.len() {
cursor.advance();
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
b'(' | b'[' | b'{' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
if bracket_stack.is_empty() {
// Replace the closing bracket with a space
diff --git a/crates/oxide/src/extractor/pre_processors/haml.rs b/crates/oxide/src/extractor/pre_processors/haml.rs
index a131618abe66..99b7bd052302 100644
--- a/crates/oxide/src/extractor/pre_processors/haml.rs
+++ b/crates/oxide/src/extractor/pre_processors/haml.rs
@@ -101,7 +101,7 @@ impl PreProcessor for Haml {
let mut last_newline_position = 0;
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escape the next character
b'\\' => {
cursor.advance_twice();
@@ -119,7 +119,7 @@ impl PreProcessor for Haml {
b'-' if cursor.input[last_newline_position..cursor.pos]
.iter()
.all(u8::is_ascii_whitespace)
- && matches!(cursor.next, b'#') =>
+ && matches!(cursor.next(), b'#') =>
{
// Just consume the comment
let updated_last_newline_position =
@@ -159,7 +159,7 @@ impl PreProcessor for Haml {
// Override the last known newline position
last_newline_position = end;
- let replaced = pre_process_input(ruby_code, "rb");
+ let replaced = pre_process_input(ruby_code.to_vec(), "rb");
result.replace_range(start..end, replaced);
}
@@ -190,7 +190,7 @@ impl PreProcessor for Haml {
// digit.
// E.g.: `bg-red-500.2xl:flex`
// ^^^
- if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
+ if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
let mut next_cursor = cursor.clone();
next_cursor.advance();
@@ -213,11 +213,11 @@ impl PreProcessor for Haml {
if bracket_stack.is_empty() {
result[cursor.pos] = b' ';
}
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
// Replace closing bracket with a space
if bracket_stack.is_empty() {
@@ -257,7 +257,7 @@ impl Haml {
// :url => { :action => "add", :id => product.id },
// :update => { :success => "cart", :failure => "error" }
// ```
- let evaluation_type = cursor.curr;
+ let evaluation_type = cursor.curr();
let block_indentation_level = cursor
.pos
@@ -267,17 +267,17 @@ impl Haml {
let mut last_newline_position = last_known_newline_position;
// Consume until the end of the line first
- while cursor.pos < len && cursor.curr != b'\n' {
+ while cursor.pos < len && cursor.curr() != b'\n' {
cursor.advance();
}
// Block is already done, aka just a line
- if evaluation_type == b'=' && cursor.prev != b',' {
+ if evaluation_type == b'=' && cursor.prev() != b',' {
return cursor.pos;
}
'outer: while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escape the next character
b'\\' => {
cursor.advance_twice();
@@ -289,7 +289,7 @@ impl Haml {
last_newline_position = cursor.pos;
// We are done with this block
- if evaluation_type == b'=' && cursor.prev != b',' {
+ if evaluation_type == b'=' && cursor.prev() != b',' {
break;
}
@@ -300,11 +300,11 @@ impl Haml {
// Skip whitespace and compute the indentation level
x if x.is_ascii_whitespace() => {
// Find first non-whitespace character
- while cursor.pos < len && cursor.curr.is_ascii_whitespace() {
- if cursor.curr == b'\n' {
+ while cursor.pos < len && cursor.curr().is_ascii_whitespace() {
+ if cursor.curr() == b'\n' {
last_newline_position = cursor.pos;
- if evaluation_type == b'=' && cursor.prev != b',' {
+ if evaluation_type == b'=' && cursor.prev() != b',' {
// We are done with this block
break 'outer;
}
diff --git a/crates/oxide/src/extractor/pre_processors/json.rs b/crates/oxide/src/extractor/pre_processors/json.rs
index 809eb501e679..cd457050c5fc 100644
--- a/crates/oxide/src/extractor/pre_processors/json.rs
+++ b/crates/oxide/src/extractor/pre_processors/json.rs
@@ -11,13 +11,13 @@ impl PreProcessor for Json {
let mut cursor = cursor::Cursor::new(content);
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Consume strings as-is
b'"' => {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
diff --git a/crates/oxide/src/extractor/pre_processors/markdown.rs b/crates/oxide/src/extractor/pre_processors/markdown.rs
index 63dc37d1eb4b..488614ee57bd 100644
--- a/crates/oxide/src/extractor/pre_processors/markdown.rs
+++ b/crates/oxide/src/extractor/pre_processors/markdown.rs
@@ -9,20 +9,27 @@ impl PreProcessor for Markdown {
let len = content.len();
let mut result = content.to_vec();
let mut cursor = cursor::Cursor::new(content);
+ let mut bracket_stack = vec![];
let mut in_directive = false;
while cursor.pos < len {
- match (in_directive, cursor.curr) {
+ match (in_directive, cursor.curr()) {
(false, b'{') => {
result[cursor.pos] = b' ';
in_directive = true;
}
+ (true, b'(' | b'[' | b'{' | b'<') => {
+ bracket_stack.push(cursor.curr());
+ }
+ (true, b')' | b']' | b'}' | b'>') if !bracket_stack.is_empty() => {
+ bracket_stack.pop();
+ }
(true, b'}') => {
result[cursor.pos] = b' ';
in_directive = false;
}
- (true, b'.') => {
+ (true, b'.') if bracket_stack.is_empty() => {
result[cursor.pos] = b' ';
}
_ => {}
@@ -60,4 +67,17 @@ mod tests {
Markdown::test(input, expected);
}
}
+
+ #[test]
+ fn test_nested_classes_keep_the_dots() {
+ for (input, expected) in [
+ (
+ r#"{}"#,
+ r#" "#,
+ ),
+ (r#"{content-['example.js']}"#, r#" content-['example.js'] "#),
+ ] {
+ Markdown::test(input, expected);
+ }
+ }
}
diff --git a/crates/oxide/src/extractor/pre_processors/pre_processor.rs b/crates/oxide/src/extractor/pre_processors/pre_processor.rs
index 8d42e744a73a..8e02b8f695fe 100644
--- a/crates/oxide/src/extractor/pre_processors/pre_processor.rs
+++ b/crates/oxide/src/extractor/pre_processors/pre_processor.rs
@@ -25,7 +25,33 @@ pub trait PreProcessor: Sized + Default {
}
#[cfg(test)]
- fn test_extract_contains(input: &str, items: Vec<&str>) {
+ fn test_extract_exact(input: &str, expected: Vec<&str>) {
+ use crate::extractor::{Extracted, Extractor};
+
+ let input = input.as_bytes();
+
+ let processor = Self::default();
+ let transformed = processor.process(input);
+
+ let extracted = Extractor::new(&transformed).extract();
+
+ // Extract all candidates and css variables.
+ let candidates = extracted
+ .iter()
+ .filter_map(|x| match x {
+ Extracted::Candidate(bytes) => std::str::from_utf8(bytes).ok(),
+ Extracted::CssVariable(bytes) => std::str::from_utf8(bytes).ok(),
+ })
+ .collect::<_>>();
+
+ if candidates != expected {
+ dbg!(&candidates, &expected);
+ panic!("Extracted candidates do not match expected candidates");
+ }
+ }
+
+ #[cfg(test)]
+ fn test_extract_contains(input: &str, expected: Vec<&str>) {
use crate::extractor::{Extracted, Extractor};
let input = input.as_bytes();
@@ -46,7 +72,7 @@ pub trait PreProcessor: Sized + Default {
// Ensure all items are present in the candidates.
let mut missing = vec![];
- for item in &items {
+ for item in &expected {
if !candidates.contains(item) {
missing.push(item);
}
diff --git a/crates/oxide/src/extractor/pre_processors/pug.rs b/crates/oxide/src/extractor/pre_processors/pug.rs
index 7b44fcf5be04..70dfe5683197 100644
--- a/crates/oxide/src/extractor/pre_processors/pug.rs
+++ b/crates/oxide/src/extractor/pre_processors/pug.rs
@@ -15,7 +15,7 @@ impl PreProcessor for Pug {
let mut bracket_stack = BracketStack::default();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
//
// ```diff
@@ -43,7 +43,7 @@ impl PreProcessor for Pug {
// digit.
// E.g.: `bg-red-500.2xl:flex`
// ^^^
- if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
+ if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
let mut next_cursor = cursor.clone();
next_cursor.advance();
@@ -68,17 +68,17 @@ impl PreProcessor for Pug {
//
// However, we also need to make sure that we keep the parens that are part of the
// utility class. E.g.: `bg-(--my-color)`.
- b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => {
+ b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => {
result[cursor.pos] = b' ';
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b'(' | b'[' | b'{' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
}
// Consume everything else
diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs
index c94bd945f56e..1f2221414ff4 100644
--- a/crates/oxide/src/extractor/pre_processors/ruby.rs
+++ b/crates/oxide/src/extractor/pre_processors/ruby.rs
@@ -68,7 +68,8 @@ impl PreProcessor for Ruby {
}
let body = &content_as_str[body_start..body_end];
- let replaced = pre_process_input(body.as_bytes(), &lang.to_ascii_lowercase());
+ let replaced =
+ pre_process_input(body.as_bytes().to_vec(), &lang.to_ascii_lowercase());
result.replace_range(body_start..body_end, replaced);
break;
@@ -77,12 +78,12 @@ impl PreProcessor for Ruby {
// Ruby extraction
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
b'"' => {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -102,7 +103,7 @@ impl PreProcessor for Ruby {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -119,12 +120,16 @@ impl PreProcessor for Ruby {
}
// Replace comments in Ruby files
- b'#' => {
+ //
+ // Except for strict locals, these are defined in a `<%# locals: … %>`. Checking if
+ // the comment is preceded by a `%` should be enough without having to perform more
+ // parsing logic. Worst case we _do_ scan a few comments.
+ b'#' if !matches!(cursor.prev(), b'%') => {
result[cursor.pos] = b' ';
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// End of the comment
b'\n' => break,
@@ -144,7 +149,7 @@ impl PreProcessor for Ruby {
}
// Looking for `%w`, `%W`, or `%p`
- if cursor.curr != b'%' || !matches!(cursor.next, b'w' | b'W' | b'p') {
+ if cursor.curr() != b'%' || !matches!(cursor.next(), b'w' | b'W' | b'p') {
cursor.advance();
continue;
}
@@ -152,7 +157,7 @@ impl PreProcessor for Ruby {
cursor.advance_twice();
// Boundary character
- let boundary = match cursor.curr {
+ let boundary = match cursor.curr() {
b'[' => b']',
b'(' => b')',
b'{' => b'}',
@@ -173,11 +178,11 @@ impl PreProcessor for Ruby {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Skip escaped characters
b'\\' => {
// Use backslash to embed spaces in the strings.
- if cursor.next == b' ' {
+ if cursor.next() == b' ' {
result[cursor.pos] = b' ';
}
@@ -186,19 +191,19 @@ impl PreProcessor for Ruby {
// Start of a nested bracket
b'[' | b'(' | b'{' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
// End of a nested bracket
b']' | b')' | b'}' if !bracket_stack.is_empty() => {
- if !bracket_stack.pop(cursor.curr) {
+ if !bracket_stack.pop(cursor.curr()) {
// Unbalanced
cursor.advance();
}
}
// End of the pattern, replace the boundary character with a space
- _ if cursor.curr == boundary => {
+ _ if cursor.curr() == boundary => {
if boundary != b'\n' {
result[cursor.pos] = b' ';
}
@@ -382,4 +387,45 @@ mod tests {
"#;
Ruby::test_extract_contains(input, vec!["z-1", "z-2", "z-3"]);
}
+
+ // https://github.com/tailwindlabs/tailwindcss/issues/19239
+ #[test]
+ fn test_skip_comments() {
+ let input = r#"
+ # From activerecord-8.1.1/lib/active_record/errors.rb:147
+ # Rails uses RDoc cross-reference syntax in inline documentation:
+ # {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]
+ "#;
+
+ // Nothing should be extracted from comments, so expect an empty array.
+ Ruby::test_extract_exact(input, vec![]);
+ }
+
+ // https://github.com/tailwindlabs/tailwindcss/issues/19481
+ #[test]
+ fn test_strict_locals() {
+ // Strict locals are defined in a `<%# locals: … %>`, but the `#` looks like a comment
+ // which we should not ignore in this case.
+ let input = r#"
+ <%# locals: (css: "text-amber-600") %>
+ <% more_css = "text-sky-500" %>
+
+
+ In a partial
+
+
+
+ In a partial using explicit local variables
+
+
+
+ In a partial using explicit local variables
+
+ "#;
+
+ Ruby::test_extract_contains(
+ input,
+ vec!["text-amber-600", "text-sky-500", "text-green-500"],
+ );
+ }
}
diff --git a/crates/oxide/src/extractor/pre_processors/rust.rs b/crates/oxide/src/extractor/pre_processors/rust.rs
index 6404fffb5e29..34c9e0c916b7 100644
--- a/crates/oxide/src/extractor/pre_processors/rust.rs
+++ b/crates/oxide/src/extractor/pre_processors/rust.rs
@@ -33,7 +33,7 @@ impl Rust {
let mut bracket_stack = bracket_stack::BracketStack::default();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => {
cursor.advance_twice();
@@ -46,7 +46,7 @@ impl Rust {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -89,7 +89,7 @@ impl Rust {
// digit.
// E.g.: `bg-red-500.2xl:flex`
// ^^^
- if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
+ if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
let mut next_cursor = cursor.clone();
next_cursor.advance();
@@ -103,11 +103,11 @@ impl Rust {
}
b'[' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b']' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
}
// Consume everything else
diff --git a/crates/oxide/src/extractor/pre_processors/slim.rs b/crates/oxide/src/extractor/pre_processors/slim.rs
index 6d41e5a09b72..eacfb55eedfd 100644
--- a/crates/oxide/src/extractor/pre_processors/slim.rs
+++ b/crates/oxide/src/extractor/pre_processors/slim.rs
@@ -15,7 +15,7 @@ impl PreProcessor for Slim {
let mut bracket_stack = BracketStack::default();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
//
// ```diff
@@ -43,7 +43,7 @@ impl PreProcessor for Slim {
// digit.
// E.g.: `bg-red-500.2xl:flex`
// ^^^
- if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
+ if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
let mut next_cursor = cursor.clone();
next_cursor.advance();
@@ -65,7 +65,7 @@ impl PreProcessor for Slim {
// class=%w[bg-blue-500 w-10 h-10]
// ]
// ```
- b'%' if matches!(cursor.next, b'w' | b'W')
+ b'%' if matches!(cursor.next(), b'w' | b'W')
&& matches!(cursor.input.get(cursor.pos + 2), Some(b'[' | b'(' | b'{')) =>
{
result[cursor.pos] = b' '; // Replace `%`
@@ -73,7 +73,7 @@ impl PreProcessor for Slim {
result[cursor.pos] = b' '; // Replace `w`
cursor.advance();
result[cursor.pos] = b' '; // Replace `[` or `(` or `{`
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
cursor.advance(); // Move past the bracket
continue;
}
@@ -96,10 +96,10 @@ impl PreProcessor for Slim {
// Instead of listing all boundary characters, let's list the characters we know
// will be invalid instead.
b'[' if bracket_stack.is_empty()
- && matches!(cursor.prev, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
+ && matches!(cursor.prev(), b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
{
result[cursor.pos] = b' ';
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
// In Slim the class name shorthand can be followed by a parenthesis. E.g.:
@@ -114,17 +114,17 @@ impl PreProcessor for Slim {
//
// However, we also need to make sure that we keep the parens that are part of the
// utility class. E.g.: `bg-(--my-color)`.
- b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => {
+ b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => {
result[cursor.pos] = b' ';
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b'(' | b'[' | b'{' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
}
// Consume everything else
diff --git a/crates/oxide/src/extractor/pre_processors/vue.rs b/crates/oxide/src/extractor/pre_processors/vue.rs
index 119e2a3d2079..1b9bf1668cb4 100644
--- a/crates/oxide/src/extractor/pre_processors/vue.rs
+++ b/crates/oxide/src/extractor/pre_processors/vue.rs
@@ -14,13 +14,12 @@ pub struct Vue;
impl PreProcessor for Vue {
fn process(&self, content: &[u8]) -> Vec {
let mut result = content.to_vec();
-
let content_as_str = std::str::from_utf8(content).unwrap();
for (_, [lang, body]) in TEMPLATE_REGEX
.captures_iter(content_as_str)
.map(|c| c.extract())
{
- let replaced = pre_process_input(body.as_bytes(), lang);
+ let replaced = pre_process_input(body.as_bytes().to_vec(), lang);
result = result.replace(body, replaced);
}
diff --git a/crates/oxide/src/extractor/string_machine.rs b/crates/oxide/src/extractor/string_machine.rs
index 9148a1fbf619..4fad49faae19 100644
--- a/crates/oxide/src/extractor/string_machine.rs
+++ b/crates/oxide/src/extractor/string_machine.rs
@@ -30,20 +30,20 @@ impl Machine for StringMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- if Class::Quote != cursor.curr.into() {
+ if Class::Quote != cursor.curr().into() {
return MachineState::Idle;
}
// Start of a string
let len = cursor.input.len();
let start_pos = cursor.pos;
- let end_char = cursor.curr;
+ let end_char = cursor.curr();
cursor.advance();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Escape => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
Class::Whitespace => return MachineState::Idle,
@@ -52,7 +52,7 @@ impl Machine for StringMachine {
},
// End of the string
- Class::Quote if cursor.curr == end_char => return self.done(start_pos, cursor),
+ 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,
diff --git a/crates/oxide/src/extractor/utility_machine.rs b/crates/oxide/src/extractor/utility_machine.rs
index b9b1038cdb3a..c116ec4e4226 100644
--- a/crates/oxide/src/extractor/utility_machine.rs
+++ b/crates/oxide/src/extractor/utility_machine.rs
@@ -27,12 +27,12 @@ impl Machine for UtilityMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// LEGACY: Important marker
Class::Exclamation => {
self.legacy_important = true;
- match cursor.next.into() {
+ match cursor.next().into() {
// Start of an arbitrary property
//
// E.g.: `![color:red]`
@@ -78,7 +78,7 @@ impl UtilityMachine {
fn parse_arbitrary_property(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.arbitrary_property_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// End of arbitrary property, but there is a potential modifier.
//
// E.g.: `[color:#0088cc]/`
@@ -109,7 +109,7 @@ impl UtilityMachine {
fn parse_named_utility(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.named_utility_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// End of a named utility, but there is a potential modifier.
//
// E.g.: `bg-red-500/`
@@ -140,7 +140,7 @@ impl UtilityMachine {
fn parse_modifier(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.modifier_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// A modifier followed by a modifier is invalid
Class::Slash => self.restart(),
diff --git a/crates/oxide/src/extractor/variant_machine.rs b/crates/oxide/src/extractor/variant_machine.rs
index 23aa61798643..9ec2adc0922a 100644
--- a/crates/oxide/src/extractor/variant_machine.rs
+++ b/crates/oxide/src/extractor/variant_machine.rs
@@ -16,7 +16,7 @@ impl Machine for VariantMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Start of an arbitrary variant
//
// E.g.: `[&:hover]:`
@@ -48,7 +48,7 @@ impl VariantMachine {
start_pos: usize,
cursor: &mut cursor::Cursor<'_>,
) -> MachineState {
- match cursor.next.into() {
+ match cursor.next().into() {
// End of an arbitrary value, must be followed by a `:`
//
// E.g.: `[&:hover]:`
diff --git a/crates/oxide/src/fast_skip.rs b/crates/oxide/src/fast_skip.rs
index 488e61bd68da..54930548c620 100644
--- a/crates/oxide/src/fast_skip.rs
+++ b/crates/oxide/src/fast_skip.rs
@@ -10,7 +10,7 @@ pub fn fast_skip(cursor: &Cursor) -> Option {
return None;
}
- if !cursor.curr.is_ascii_whitespace() {
+ if !cursor.curr().is_ascii_whitespace() {
return None;
}
diff --git a/crates/oxide/src/scanner/detect_sources.rs b/crates/oxide/src/scanner/detect_sources.rs
index 9dc340fbaf47..c519cd00d788 100644
--- a/crates/oxide/src/scanner/detect_sources.rs
+++ b/crates/oxide/src/scanner/detect_sources.rs
@@ -30,11 +30,9 @@ fn sort_by_dir_and_name(a: &DirEntry, z: &DirEntry) -> Ordering {
pub fn resolve_globs(
base: PathBuf,
- dirs: &[PathBuf],
+ dirs: &FxHashSet,
extensions: &FxHashSet,
) -> Vec {
- let allowed_paths: FxHashSet = FxHashSet::from_iter(dirs.iter().cloned());
-
// A list of known extensions + a list of extensions we found in the project.
let mut found_extensions: FxHashSet =
FxHashSet::from_iter(KNOWN_EXTENSIONS.iter().map(|x| x.to_string()));
@@ -72,7 +70,7 @@ pub fn resolve_globs(
continue;
}
- if !allowed_paths.contains(path) {
+ if !dirs.contains(path) {
let mut path = path;
while let Some(parent) = path.parent() {
if parent == base {
@@ -113,7 +111,7 @@ pub fn resolve_globs(
continue;
}
- if !allowed_paths.contains(path) {
+ if !dirs.contains(path) {
continue;
}
diff --git a/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt b/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt
index 0921d2ff8c8e..79f01ecc117a 100644
--- a/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt
+++ b/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt
@@ -1,14 +1,15 @@
.git
.hg
-.svn
-node_modules
-.yarn
-.venv
-venv
+.jj
.next
-.turbo
.parcel-cache
-__pycache__
-.svelte-kit
.pnpm-store
+.svelte-kit
+.svn
+.turbo
+.venv
.vercel
+.yarn
+__pycache__
+node_modules
+venv
diff --git a/crates/oxide/src/scanner/init_tracing.rs b/crates/oxide/src/scanner/init_tracing.rs
new file mode 100644
index 000000000000..2a570ff20188
--- /dev/null
+++ b/crates/oxide/src/scanner/init_tracing.rs
@@ -0,0 +1,70 @@
+use std::fs::OpenOptions;
+use std::io::{self, Write};
+use std::path::Path;
+use std::sync::{self, Arc, Mutex};
+use tracing_subscriber::fmt::writer::BoxMakeWriter;
+
+pub static SHOULD_TRACE: sync::LazyLock = sync::LazyLock::new(
+ || matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
+);
+
+fn dim(input: &str) -> String {
+ format!("\u{001b}[2m{input}\u{001b}[22m")
+}
+
+fn blue(input: &str) -> String {
+ format!("\u{001b}[34m{input}\u{001b}[39m")
+}
+
+fn highlight(input: &str) -> String {
+ format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`")))
+}
+
+struct MutexWriter(Arc>);
+
+impl Write for MutexWriter {
+ fn write(&mut self, buf: &[u8]) -> io::Result {
+ self.0.lock().unwrap().write(buf)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.0.lock().unwrap().flush()
+ }
+}
+
+pub fn init_tracing() {
+ if !*SHOULD_TRACE {
+ return;
+ }
+
+ let file_path = format!("tailwindcss-{}.log", std::process::id());
+ let file = OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(&file_path)
+ .unwrap_or_else(|_| panic!("Failed to open {file_path}"));
+
+ let file_path = Path::new(&file_path);
+ let absolute_file_path = dunce::canonicalize(file_path)
+ .unwrap_or_else(|_| panic!("Failed to canonicalize {file_path:?}"));
+ eprintln!(
+ "{} Writing debug info to: {}\n",
+ dim("[DEBUG]"),
+ highlight(absolute_file_path.as_path().to_str().unwrap())
+ );
+
+ let file = Arc::new(Mutex::new(file));
+
+ let writer: BoxMakeWriter = BoxMakeWriter::new({
+ let file = file.clone();
+ move || Box::new(MutexWriter(file.clone())) as Box
+ });
+
+ _ = tracing_subscriber::fmt()
+ .with_max_level(tracing::Level::INFO)
+ .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
+ .with_writer(writer)
+ .with_ansi(false)
+ .compact()
+ .try_init();
+}
diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs
index ec6aea642481..6d6cabc2aa80 100644
--- a/crates/oxide/src/scanner/mod.rs
+++ b/crates/oxide/src/scanner/mod.rs
@@ -1,5 +1,6 @@
pub mod auto_source_detection;
pub mod detect_sources;
+pub mod init_tracing;
pub mod sources;
use crate::extractor::{Extracted, Extractor};
@@ -14,15 +15,13 @@ use bstr::ByteSlice;
use fast_glob::glob_match;
use fxhash::{FxHashMap, FxHashSet};
use ignore::{gitignore::GitignoreBuilder, WalkBuilder};
+use init_tracing::{init_tracing, SHOULD_TRACE};
use rayon::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
-use std::fs::OpenOptions;
-use std::io::{self, Write};
use std::path::{Path, PathBuf};
-use std::sync::{self, Arc, Mutex};
+use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use tracing::event;
-use tracing_subscriber::fmt::writer::BoxMakeWriter;
// @source "some/folder"; // This is auto source detection
// @source "some/folder/**/*"; // This is auto source detection
@@ -34,70 +33,6 @@ use tracing_subscriber::fmt::writer::BoxMakeWriter;
//
// @source "do-include-me.bin"; // `.bin` is typically ignored, but now it's explicit so should be included
// @source "git-ignored.html"; // A git ignored file that is listed explicitly, should be scanned
-static SHOULD_TRACE: sync::LazyLock = sync::LazyLock::new(
- || matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
-);
-
-fn dim(input: &str) -> String {
- format!("\u{001b}[2m{input}\u{001b}[22m")
-}
-
-fn blue(input: &str) -> String {
- format!("\u{001b}[34m{input}\u{001b}[39m")
-}
-
-fn highlight(input: &str) -> String {
- format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`")))
-}
-
-fn init_tracing() {
- if !*SHOULD_TRACE {
- return;
- }
-
- let file_path = format!("tailwindcss-{}.log", std::process::id());
- let file = OpenOptions::new()
- .create(true)
- .append(true)
- .open(&file_path)
- .unwrap_or_else(|_| panic!("Failed to open {file_path}"));
-
- let file_path = Path::new(&file_path);
- let absolute_file_path = dunce::canonicalize(file_path)
- .unwrap_or_else(|_| panic!("Failed to canonicalize {file_path:?}"));
- eprintln!(
- "{} Writing debug info to: {}\n",
- dim("[DEBUG]"),
- highlight(absolute_file_path.as_path().to_str().unwrap())
- );
-
- let file = Arc::new(Mutex::new(file));
-
- let writer: BoxMakeWriter = BoxMakeWriter::new({
- let file = file.clone();
- move || Box::new(MutexWriter(file.clone())) as Box
- });
-
- _ = tracing_subscriber::fmt()
- .with_max_level(tracing::Level::INFO)
- .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
- .with_writer(writer)
- .with_ansi(false)
- .compact()
- .try_init();
-}
-
-struct MutexWriter(Arc>);
-
-impl Write for MutexWriter {
- fn write(&mut self, buf: &[u8]) -> io::Result {
- self.0.lock().unwrap().write(buf)
- }
-
- fn flush(&mut self) -> io::Result<()> {
- self.0.lock().unwrap().flush()
- }
-}
#[derive(Debug, Clone)]
pub enum ChangedContent {
@@ -129,26 +64,32 @@ pub struct Scanner {
/// The walker to detect all files that we have to scan
walker: Option,
- /// All changed content that we have to parse
- changed_content: Vec,
-
/// All found extensions
extensions: FxHashSet,
- /// All CSS files we want to scan for CSS variable usage
- css_files: Vec,
-
/// All files that we have to scan
- files: Vec,
+ files: FxHashSet,
/// All directories, sub-directories, etc… we saw during source detection
- dirs: Vec,
+ dirs: FxHashSet,
/// All generated globs, used for setting up watchers
globs: Option>,
/// Track unique set of candidates
candidates: FxHashSet,
+
+ /// Track mtimes for files so re-scans can skip unchanged files.
+ /// Only populated after the first scan completes (to avoid unnecessary
+ /// metadata calls on initial build).
+ mtimes: FxHashMap,
+
+ /// Whether we've completed at least one full scan. When false, we skip
+ /// mtime tracking entirely so the initial build stays fast.
+ has_scanned_once: bool,
+
+ /// Whether sources have been scanned since the last `scan()` call
+ sources_scanned: bool,
}
impl Scanner {
@@ -163,7 +104,6 @@ impl Scanner {
}
let sources = Sources::new(public_source_entries_to_private_source_entries(sources));
-
if *SHOULD_TRACE {
event!(tracing::Level::INFO, "Optimized sources:");
for source in sources.iter() {
@@ -171,29 +111,26 @@ impl Scanner {
}
}
+ let walker = create_walker(&sources);
+
Self {
- sources: sources.clone(),
- walker: create_walker(sources),
+ sources,
+ walker,
..Default::default()
}
}
pub fn scan(&mut self) -> Vec {
- self.scan_sources();
+ self.sources_scanned = false;
- // TODO: performance improvement, bail early if we don't have any changed content
- // if self.changed_content.is_empty() {
- // return vec![];
- // }
+ let (scanned_blobs, css_files) = self.discover_sources();
- let _new_candidates = self.extract_candidates();
+ self.extract_candidates(scanned_blobs, css_files);
- // Make sure we have a sorted list of candidates
- let mut candidates = self.candidates.iter().cloned().collect::<_>>();
- candidates.par_sort_unstable();
-
- // Return all candidates instead of only the new ones
- candidates
+ // Return all candidates sorted
+ let mut result = self.candidates.iter().cloned().collect::<_>>();
+ result.par_sort_unstable();
+ result
}
#[tracing::instrument(skip_all)]
@@ -208,7 +145,7 @@ impl Scanner {
// Raw content can be parsed directly, no need to verify if the file exists and is allowed
// to be scanned.
- self.changed_content.extend(changed_contents);
+ let mut content_to_scan: Vec = changed_contents;
// Fully resolve all files
let changed_files = changed_files
@@ -232,7 +169,7 @@ impl Scanner {
});
// All known files are allowed to be scanned
- self.changed_content.extend(known_files);
+ content_to_scan.extend(known_files);
// Figure out if the new unknown files are allowed to be scanned
if !new_unknown_files.is_empty() {
@@ -252,8 +189,8 @@ impl Scanner {
// When the file is found on disk it means that all the rules pass. We can
// extract the current file and remove it from the list of passed in files.
if file == path {
- self.files.push(path.to_path_buf()); // Track for future use
- self.changed_content.push(changed_file.clone()); // Track for parsing
+ self.files.insert(path.to_path_buf()); // Track for future use
+ content_to_scan.push(changed_file.clone()); // Track for parsing
drop_file_indexes.push(idx);
}
}
@@ -274,18 +211,17 @@ impl Scanner {
}
}
- self.extract_candidates()
+ // Read all content into blobs for extraction
+ let blobs = read_all_files(content_to_scan);
+ self.extract_candidates(blobs, vec![])
}
#[tracing::instrument(skip_all)]
- fn extract_candidates(&mut self) -> Vec {
- let changed_content = self.changed_content.drain(..).collect::<_>>();
-
- // Extract all candidates from the changed content
- let mut new_candidates = parse_all_blobs(read_all_files(changed_content));
+ fn extract_candidates(&mut self, blobs: Vec>, css_files: Vec) -> Vec {
+ // Extract all candidates from the pre-read blobs
+ let mut new_candidates = parse_all_blobs(blobs);
// Extract all CSS variables from the CSS files
- let css_files = self.css_files.drain(..).collect::<_>>();
if !css_files.is_empty() {
let css_variables = extract_css_variables(read_all_files(
css_files
@@ -297,63 +233,23 @@ impl Scanner {
new_candidates.extend(css_variables);
}
- // Only compute the new candidates and ignore the ones we already have. This is for
- // subsequent calls to prevent serializing the entire set of candidates every time.
- let mut new_candidates = new_candidates
- .into_par_iter()
- .filter(|candidate| !self.candidates.contains(candidate))
- .collect::<_>>();
-
- new_candidates.par_sort_unstable();
+ // Only keep candidates we haven't seen before
+ for existing in self.candidates.iter() {
+ new_candidates.remove(existing);
+ }
// Track new candidates for subsequent calls
- self.candidates.par_extend(new_candidates.clone());
-
- new_candidates
- }
-
- #[tracing::instrument(skip_all)]
- fn scan_sources(&mut self) {
- let Some(walker) = &mut self.walker else {
- return;
- };
+ self.candidates.extend(new_candidates.iter().cloned());
- for entry in walker.build().filter_map(Result::ok) {
- let path = entry.into_path();
- let Ok(metadata) = path.metadata() else {
- continue;
- };
- if metadata.is_dir() {
- self.dirs.push(path);
- } else if metadata.is_file() {
- let extension = path
- .extension()
- .and_then(|x| x.to_str())
- .unwrap_or_default(); // In case the file has no extension
+ let mut result: Vec = new_candidates.into_iter().collect();
+ result.par_sort_unstable();
- match extension {
- // Special handing for CSS files, we don't want to extract candidates from
- // these files, but we do want to extract used CSS variables.
- "css" => {
- self.css_files.push(path.clone());
- }
- _ => {
- self.changed_content.push(ChangedContent::File(
- path.to_path_buf(),
- extension.to_owned(),
- ));
- }
- }
-
- self.extensions.insert(extension.to_owned());
- self.files.push(path);
- }
- }
+ result
}
#[tracing::instrument(skip_all)]
pub fn get_files(&mut self) -> Vec {
- self.scan_sources();
+ let _ = self.discover_sources();
self.files
.par_iter()
@@ -367,7 +263,7 @@ impl Scanner {
return globs.clone();
}
- self.scan_sources();
+ let _ = self.discover_sources();
let mut globs = vec![];
for source in self.sources.iter() {
@@ -456,6 +352,99 @@ impl Scanner {
})
.collect()
}
+
+ #[tracing::instrument(skip_all)]
+ fn discover_sources(&mut self) -> (Vec>, Vec) {
+ if self.sources_scanned {
+ return (vec![], vec![]);
+ }
+ self.sources_scanned = true;
+
+ let Some(walker) = &mut self.walker else {
+ return (vec![], vec![]);
+ };
+
+ // Use synchronous walk for the initial build (lower overhead) and parallel
+ // walk for subsequent calls (watch mode) where the overhead is amortised.
+ let all_entries = if self.has_scanned_once {
+ walk_parallel(walker)
+ } else {
+ walk_synchronous(walker)
+ };
+
+ let mut css_files: Vec = vec![];
+ let mut content_paths: Vec<(PathBuf, String)> = Vec::new();
+ let mut seen_files: FxHashSet = FxHashSet::default();
+
+ for (path, is_dir, extension) in all_entries {
+ if is_dir {
+ self.dirs.insert(path);
+ } else {
+ // Deduplicate: parallel walk can visit the same file from multiple threads
+ if !seen_files.insert(path.clone()) {
+ continue;
+ }
+
+ // On re-scans, check mtime to skip unchanged files.
+ // On the first scan we skip this entirely to avoid extra
+ // metadata syscalls.
+ let changed = if self.has_scanned_once {
+ let current_mtime = std::fs::metadata(&path)
+ .ok()
+ .and_then(|m| m.modified().ok());
+
+ match current_mtime {
+ Some(mtime) => {
+ let prev = self.mtimes.insert(path.clone(), mtime);
+ prev.is_none_or(|prev| prev != mtime)
+ }
+ None => true,
+ }
+ } else {
+ true
+ };
+
+ match extension.as_str() {
+ // Special handing for CSS files, we don't want to extract candidates from
+ // these files, but we do want to extract used CSS variables.
+ "css" => {
+ if changed {
+ css_files.push(path.clone());
+ }
+ }
+ _ => {
+ if changed {
+ content_paths.push((path.clone(), extension.clone()));
+ }
+ }
+ }
+
+ self.extensions.insert(extension);
+ self.files.insert(path);
+ }
+ }
+
+ // Read + preprocess all discovered files in parallel
+ let scanned_blobs: Vec> = content_paths
+ .into_par_iter()
+ .filter_map(|(path, ext)| {
+ let content = std::fs::read(&path).ok()?;
+ event!(tracing::Level::INFO, "Reading {:?}", path);
+ let processed = pre_process_input(content, &ext);
+ if processed.is_empty() {
+ None
+ } else {
+ Some(processed)
+ }
+ })
+ .collect();
+
+ if !self.has_scanned_once {
+ self.has_scanned_once = true;
+ }
+
+ (scanned_blobs, css_files)
+ }
}
fn read_changed_content(c: ChangedContent) -> Option> {
@@ -474,26 +463,26 @@ fn read_changed_content(c: ChangedContent) -> Option> {
ChangedContent::Content(contents, extension) => (contents.into_bytes(), extension),
};
- Some(pre_process_input(&content, &extension))
+ Some(pre_process_input(content, &extension))
}
-pub fn pre_process_input(content: &[u8], extension: &str) -> Vec {
+pub fn pre_process_input(content: Vec, extension: &str) -> Vec {
use crate::extractor::pre_processors::*;
match extension {
- "clj" | "cljs" | "cljc" => Clojure.process(content),
- "heex" | "eex" | "ex" | "exs" => Elixir.process(content),
- "cshtml" | "razor" => Razor.process(content),
- "haml" => Haml.process(content),
- "json" => Json.process(content),
- "md" | "mdx" => Markdown.process(content),
- "pug" => Pug.process(content),
- "rb" | "erb" => Ruby.process(content),
- "slim" | "slang" => Slim.process(content),
- "svelte" => Svelte.process(content),
- "rs" => Rust.process(content),
- "vue" => Vue.process(content),
- _ => content.to_vec(),
+ "clj" | "cljs" | "cljc" => Clojure.process(&content),
+ "heex" | "eex" | "ex" | "exs" => Elixir.process(&content),
+ "cshtml" | "razor" => Razor.process(&content),
+ "haml" => Haml.process(&content),
+ "json" => Json.process(&content),
+ "md" | "mdx" => Markdown.process(&content),
+ "pug" => Pug.process(&content),
+ "rb" | "erb" => Ruby.process(&content),
+ "slim" | "slang" => Slim.process(&content),
+ "svelte" => Svelte.process(&content),
+ "rs" => Rust.process(&content),
+ "vue" => Vue.process(&content),
+ _ => content,
}
}
@@ -512,23 +501,23 @@ fn read_all_files(changed_content: Vec) -> Vec> {
}
#[tracing::instrument(skip_all)]
-fn extract_css_variables(blobs: Vec>) -> Vec {
+fn extract_css_variables(blobs: Vec>) -> FxHashSet {
extract(blobs, |mut extractor| {
extractor.extract_variables_from_css()
})
}
#[tracing::instrument(skip_all)]
-fn parse_all_blobs(blobs: Vec>) -> Vec {
+fn parse_all_blobs(blobs: Vec>) -> FxHashSet {
extract(blobs, |mut extractor| extractor.extract())
}
#[tracing::instrument(skip_all)]
-fn extract(blobs: Vec>, handle: H) -> Vec
+fn extract(blobs: Vec>, handle: H) -> FxHashSet
where
H: Fn(Extractor) -> Vec + std::marker::Sync,
{
- let mut result: Vec<_> = blobs
+ blobs
.par_iter()
.flat_map(|blob| blob.par_split(|x| *x == b'\n'))
.filter_map(|blob| {
@@ -554,22 +543,97 @@ where
})
.into_iter()
.map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) })
- .collect();
+ .collect()
+}
+
+type WalkEntry = (PathBuf, bool, String);
- // SAFETY: Unstable sort is faster and in this scenario it's also safe because we are
- // guaranteed to have unique candidates.
- result.par_sort_unstable();
+/// Walk the file system synchronously. Used for the initial build where the overhead of spawning
+/// parallel walker threads is not worth it.
+#[tracing::instrument(skip_all)]
+fn walk_synchronous(walker: &mut WalkBuilder) -> Vec {
+ let mut entries = Vec::new();
+
+ for entry in walker.build().filter_map(Result::ok) {
+ let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
+ let path = entry.into_path();
+
+ if is_dir {
+ entries.push((path, true, String::new()));
+ } else {
+ let ext = path
+ .extension()
+ .and_then(|x| x.to_str())
+ .unwrap_or_default()
+ .to_owned();
+ entries.push((path, false, ext));
+ }
+ }
- result
+ entries
}
-/// Create a walker for the given sources to detect all the files that we have to scan.
+/// Walk the file system in parallel. Used in watch mode where the parallel walker overhead is
+/// amortised across many rebuilds and subsequent calls are much faster.
+#[tracing::instrument(skip_all)]
+fn walk_parallel(walker: &mut WalkBuilder) -> Vec {
+ struct FlushOnDrop {
+ local: Vec,
+ shared: Arc>>,
+ }
+
+ impl Drop for FlushOnDrop {
+ fn drop(&mut self) {
+ if !self.local.is_empty() {
+ self.shared.lock().unwrap().append(&mut self.local);
+ }
+ }
+ }
+
+ let collected: Arc>> = Arc::new(Mutex::new(Vec::new()));
+
+ walker.build_parallel().run(|| {
+ let mut buf = FlushOnDrop {
+ local: Vec::with_capacity(256),
+ shared: collected.clone(),
+ };
+
+ Box::new(move |entry| {
+ let Ok(entry) = entry else {
+ return ignore::WalkState::Continue;
+ };
+
+ let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
+ let path = entry.into_path();
+
+ if is_dir {
+ buf.local.push((path, true, String::new()));
+ } else {
+ let ext = path
+ .extension()
+ .and_then(|x| x.to_str())
+ .unwrap_or_default()
+ .to_owned();
+ buf.local.push((path, false, ext));
+ }
+
+ if buf.local.len() >= 256 {
+ buf.shared.lock().unwrap().append(&mut buf.local);
+ }
+
+ ignore::WalkState::Continue
+ })
+ });
+
+ // All threads have finished and flushed their buffers via FlushOnDrop::drop
+ Arc::try_unwrap(collected).unwrap().into_inner().unwrap()
+}
+
+/// Sets up a WalkBuilder with all source roots, gitignore rules, and source pattern matching.
///
-/// The `mtimes` map is used to keep track of the last modified time of each file. This is used to
-/// determine if a file or folder has changed since the last scan and we can skip folders that
-/// haven't changed.
-fn create_walker(sources: Sources) -> Option {
- let mtimes: Arc>> = Default::default();
+/// This is the common setup shared between the full walker (with mtime tracking for re-scans)
+/// and the parallel walker (without mtime tracking for the initial scan).
+fn create_walker(sources: &Sources) -> Option {
let mut other_roots: FxHashSet<&PathBuf> = FxHashSet::default();
let mut first_root: Option<&PathBuf> = None;
let mut ignores: BTreeMap<&PathBuf, BTreeSet> = Default::default();
@@ -717,75 +781,68 @@ fn create_walker(sources: Sources) -> Option {
builder.add_gitignore(ignore);
}
- builder.filter_entry({
- move |entry| {
- let path = entry.path();
-
- // Ensure the entries are matching any of the provided source patterns (this is
- // necessary for manual-patterns that can filter the file extension)
- if path.is_file() {
- let mut matches = false;
- for source in sources.iter() {
- match source {
- SourceEntry::Auto { base } | SourceEntry::External { base } => {
- if path.starts_with(base) {
- matches = true;
- break;
- }
- }
- SourceEntry::Pattern { base, pattern } => {
- let mut pattern = pattern.to_string();
- // Ensure that the pattern is pinned to the base path.
- if !pattern.starts_with("/") {
- pattern = format!("/{pattern}");
- }
-
- // Check if path starts with base, if so, remove the prefix and check the remainder against the pattern
- let remainder = path.strip_prefix(base);
- if remainder.is_ok_and(|remainder| {
- let mut path_str = remainder.to_string_lossy().to_string();
- if !path_str.starts_with("/") {
- path_str = format!("/{path_str}");
- }
- glob_match(pattern, path_str.as_bytes())
- }) {
- matches = true;
- break;
- }
- }
- _ => {}
- }
- }
+ // Pre-compute source matching data to avoid allocations in the hot filter_entry path
+ let auto_bases: Vec = sources
+ .iter()
+ .filter_map(|source| match source {
+ SourceEntry::Auto { base } | SourceEntry::External { base } => Some(base.clone()),
+ _ => None,
+ })
+ .collect();
- if !matches {
- return false;
- }
+ let pattern_sources: Vec<(PathBuf, String)> = sources
+ .iter()
+ .filter_map(|source| match source {
+ SourceEntry::Pattern { base, pattern } => {
+ let normalized = if pattern.starts_with("/") {
+ pattern.to_string()
+ } else {
+ format!("/{pattern}")
+ };
+ Some((base.clone(), normalized))
}
+ _ => None,
+ })
+ .collect();
- let mut mtimes = mtimes.lock().unwrap();
- let current_time = match entry.metadata() {
- Ok(metadata) if metadata.is_file() => {
- if let Ok(time) = metadata.modified() {
- Some(time)
- } else {
- None
- }
- }
- _ => None,
- };
+ // Source pattern matching filter (lock-free, safe for parallel walking)
+ builder.filter_entry(move |entry| {
+ let path = entry.path();
- let previous_time =
- current_time.and_then(|time| mtimes.insert(entry.clone().into_path(), time));
+ // Ensure the entries are matching any of the provided source patterns (this is
+ // necessary for manual-patterns that can filter the file extension)
+ if path.is_file() {
+ let mut matches = false;
- match (current_time, previous_time) {
- (Some(current), Some(prev)) if prev == current => false,
- _ => {
- event!(tracing::Level::INFO, "Discovering {:?}", path);
+ for base in &auto_bases {
+ if path.starts_with(base) {
+ matches = true;
+ break;
+ }
+ }
- true
+ if !matches {
+ for (base, pattern) in &pattern_sources {
+ let remainder = path.strip_prefix(base);
+ if remainder.is_ok_and(|remainder| {
+ let mut path_str = remainder.to_string_lossy().to_string();
+ if !path_str.starts_with("/") {
+ path_str = format!("/{path_str}");
+ }
+ glob_match(pattern, path_str.as_bytes())
+ }) {
+ matches = true;
+ break;
+ }
}
}
+
+ if !matches {
+ return false;
+ }
}
+
+ true
});
Some(builder)
diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts
index 870a1baec3a9..409a9de7cd41 100644
--- a/integrations/cli/index.test.ts
+++ b/integrations/cli/index.test.ts
@@ -752,7 +752,7 @@ describe.each([
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
- generated: '}...',
+ generated: '}\n\n/*# sou...',
})
},
)
@@ -815,7 +815,7 @@ describe.each([
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
- generated: '}...',
+ generated: '}\n\n/*# sou...',
})
},
)
@@ -1118,7 +1118,7 @@ describe.each([
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
- generated: '}...',
+ generated: '}\n\n/*# sou...',
})
// Write to project source files
@@ -1190,7 +1190,7 @@ describe.each([
expect(map.at(7, 0)).toMatchObject({
source: null,
original: '(none)',
- generated: '}...',
+ generated: '}\n\n/*# sou...',
})
// Write to the main CSS file
@@ -1290,7 +1290,7 @@ describe.each([
expect(map.at(10, 0)).toMatchObject({
source: null,
original: '(none)',
- generated: '}...',
+ generated: '}\n\n/*# sou...',
})
},
)
diff --git a/integrations/cli/plugins.test.ts b/integrations/cli/plugins.test.ts
index 0597e247be50..c0f7c39c3a41 100644
--- a/integrations/cli/plugins.test.ts
+++ b/integrations/cli/plugins.test.ts
@@ -117,7 +117,7 @@ test(
await fs.expectFileToContain('dist/out.css', [
//
`::-webkit-date-and-time-value`,
- `[type='checkbox']:indeterminate`,
+ `input:where([type='checkbox']):indeterminate`,
])
// No classes are included even though they are used in the HTML
diff --git a/integrations/oxide/wasm.test.ts b/integrations/oxide/wasm.test.ts
index 1cbdfce6e1fb..ac2231672386 100644
--- a/integrations/oxide/wasm.test.ts
+++ b/integrations/oxide/wasm.test.ts
@@ -1,6 +1,6 @@
import { css, js, json, test } from '../utils'
-// This test runs the wasm build using the `node:wasi` runtine.
+// This test runs the wasm build using the `node:wasi` runtime.
//
// There are currently a known problems that the Node WASI preview implementation does not properly
// handle FS reads on macOS and it does not implement all APIs on Windows. Because of that, this
diff --git a/integrations/package.json b/integrations/package.json
index c6e792fc447f..8c0d83543a30 100644
--- a/integrations/package.json
+++ b/integrations/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"devDependencies": {
- "dedent": "1.7.0",
+ "dedent": "1.7.1",
"fast-glob": "^3.3.3",
"source-map-js": "^1.2.1"
}
diff --git a/integrations/postcss/core-as-postcss-plugin.test.ts b/integrations/postcss/core-as-postcss-plugin.test.ts
index 17545fea32d8..0856210fec98 100644
--- a/integrations/postcss/core-as-postcss-plugin.test.ts
+++ b/integrations/postcss/core-as-postcss-plugin.test.ts
@@ -48,7 +48,7 @@ describe.each(Object.keys(variantConfig))('%s', (variant) => {
},
},
async ({ exec, expect }) => {
- expect(
+ await expect(
exec('pnpm postcss src/index.css --output dist/out.css', undefined, { ignoreStdErr: true }),
).rejects.toThrowError(
`It looks like you're trying to use \`tailwindcss\` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install \`@tailwindcss/postcss\` and update your PostCSS configuration.`,
diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts
index 08ea852f950c..80025f945411 100644
--- a/integrations/postcss/index.test.ts
+++ b/integrations/postcss/index.test.ts
@@ -698,9 +698,7 @@ test(
`)
- await process.onStderr((message) =>
- message.includes('does-not-exist is not exported from package'),
- )
+ await process.onStderr((message) => message.includes('"./does-not-exist" is not exported'))
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
"
@@ -743,9 +741,7 @@ test(
`,
)
- await process.onStderr((message) =>
- message.includes('does-not-exist is not exported from package'),
- )
+ await process.onStderr((message) => message.includes('"./does-not-exist" is not exported'))
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
"
diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts
index 969254a75c8c..7fc0ccdcf14e 100644
--- a/integrations/postcss/next.test.ts
+++ b/integrations/postcss/next.test.ts
@@ -468,7 +468,7 @@ test(
// the CSS
await fetchStyles(url)
- // At this point, no changes should triger a compile step. If we see any
+ // At this point, no changes should trigger a compile step. If we see any
// changes, there is an infinite loop because we (the user) didn't write any
// files to disk.
//
diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts
index 029ed77a89b3..46b657694ca3 100644
--- a/integrations/upgrade/index.test.ts
+++ b/integrations/upgrade/index.test.ts
@@ -175,7 +175,7 @@ test(
`upgrades a v3 project with prefixes to v4`,
{
// Somehow this test takes *way* longer than the rest (but not always?)
- timeout: 120_000,
+ timeout: 180_000,
fs: {
'package.json': json`
{
@@ -603,6 +603,7 @@ test(
test(
'migrates a simple postcss setup',
{
+ timeout: 120_000,
fs: {
'package.json': json`
{
@@ -774,6 +775,7 @@ test(
test(
'migrates a postcss setup using package.json config',
{
+ timeout: 120_000,
fs: {
'package.json': json`
{
@@ -847,6 +849,7 @@ test(
test(
'migrates a postcss setup using a json based config file',
{
+ timeout: 120_000,
fs: {
'package.json': json`
{
diff --git a/integrations/utils.ts b/integrations/utils.ts
index a2fc0d58af9a..c5d3306c4677 100644
--- a/integrations/utils.ts
+++ b/integrations/utils.ts
@@ -498,6 +498,7 @@ async function overwriteVersionsInPackageJson(content: string): Promise
json.pnpm.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg)
json.pnpm.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg)
json.pnpm.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg)
+ json.pnpm.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg)
} else {
json.pnpm.overrides[pkg] = resolveVersion(pkg)
}
diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts
index 43406624c316..f828b5071b60 100644
--- a/integrations/vite/astro.test.ts
+++ b/integrations/vite/astro.test.ts
@@ -1,4 +1,4 @@
-import { candidate, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils'
+import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils'
test(
'dev mode',
@@ -129,3 +129,73 @@ test(
await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`overline`])
},
)
+
+// https://github.com/tailwindlabs/tailwindcss/issues/19677
+test(
+ 'import aliases should work in
+
+
+ Astro
+
+