diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 42645aa49..63f79fa10 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 ee9d3f9ab..aa0793aa3 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 e369c64fc..5ae59e80f 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
@@ -21,8 +27,10 @@ jobs:
- name: Linux
os: namespace-profile-default
+ # Playwright 1.62+ dropped WebKit support for macOS 14, and hangs
+ # instead of failing when launching WebKit on a macos-14 runner.
- name: macOS
- os: macos-14
+ os: macos-15
# Exclude windows and macos from being built on feature branches
run-all:
@@ -41,21 +49,21 @@ jobs:
name: ${{ matrix.runner.name }}
steps:
- - uses: actions/checkout@v4
- - uses: pnpm/action-setup@v4
-
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
- node-version: ${{ matrix.node-version }}
- cache: 'pnpm'
+ persist-credentials: false
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
+
+ - name: Use Node.js ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
- ~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
@@ -64,7 +72,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
./crates/node/*.node
@@ -82,7 +90,7 @@ jobs:
run: rustup target add wasm32-wasip1-threads
- name: Install dependencies
- run: pnpm install
+ run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
@@ -104,9 +112,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
+ uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # 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 b84ba2140..457c38424 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,25 +55,25 @@ jobs:
name: ${{ matrix.runner.name }} / ${{ matrix.integration }}
steps:
- - uses: actions/checkout@v4
- - uses: pnpm/action-setup@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
- node-version: ${{ matrix.node-version }}
- cache: 'pnpm'
+ node-version: ${{ env.NODE_VERSION }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@v4
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
- ~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
@@ -77,7 +82,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@v4
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
./crates/node/*.node
@@ -103,24 +108,18 @@ jobs:
CARGO_PROFILE_RELEASE_LTO: 'off'
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link'
- - name: Test ${{ matrix.integration }} 1/3
- run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 1/3
- env:
- GITHUB_WORKSPACE: ${{ github.workspace }}
-
- - name: Test ${{ matrix.integration }} 2/3
- run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 2/3
- env:
- GITHUB_WORKSPACE: ${{ github.workspace }}
-
- - name: Test ${{ matrix.integration }} 3/3
- run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 3/3
+ - name: Test ${{ matrix.integration }}
+ run: pnpm run test:integrations ./integrations/${{ matrix.integration }}
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
+ uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # 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 be1050b03..24e829afd 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -2,18 +2,29 @@ name: Prepare Release
on:
workflow_dispatch:
+ inputs:
+ dry_run:
+ description: Skip creating the draft GitHub release
+ required: false
+ default: true
+ type: boolean
push:
tags:
- 'v*'
env:
APP_NAME: tailwindcss-oxide
- NODE_VERSION: 20
+ NODE_VERSION: 24
+ PNPM_VERSION: '11.9.0'
OXIDE_LOCATION: ./crates/node
permissions:
contents: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
strategy:
@@ -43,44 +54,40 @@ jobs:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
strip: strip
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
+ build-flags: --use-napi-cross
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- strip: llvm-strip
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
+ strip: aarch64-linux-gnu-strip
+ build-flags: --use-napi-cross
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
- strip: llvm-strip
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
+ strip: arm-linux-gnueabihf-strip
+ build-flags: --use-napi-cross
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
- strip: aarch64-linux-musl-strip
- download: true
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
+ strip-zig: true
+ build-flags: -x
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
strip: strip
- download: true
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
+ build-flags: -x
name: Build ${{ matrix.target }} (oxide)
runs-on: ${{ matrix.os }}
- container: ${{ matrix.container }}
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
- - uses: pnpm/action-setup@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
+ with:
+ version: ${{ env.PNPM_VERSION }}
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.NODE_VERSION }}
- cache: 'pnpm'
+ package-manager-cache: false
- name: Install gcc-arm-linux-gnueabihf
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
@@ -88,62 +95,56 @@ jobs:
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
- # Cargo already skips downloading dependencies if they already exist
- - name: Cache cargo
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/bin/
- ~/.cargo/registry/index/
- ~/.cargo/registry/cache/
- ~/.cargo/git/db/
- target/
- key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
-
- # Cache the `oxide` Rust build
- - name: Cache oxide build
- uses: actions/cache@v4
- with:
- path: |
- ./crates/node/*.node
- ./crates/node/*.wasm
- ./crates/node/index.d.ts
- ./crates/node/index.js
- ./crates/node/browser.js
- ./crates/node/tailwindcss-oxide.wasi-browser.js
- ./crates/node/tailwindcss-oxide.wasi.cjs
- ./crates/node/wasi-worker-browser.mjs
- ./crates/node/wasi-worker.mjs
- key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
-
- - name: Install Node.JS
- uses: actions/setup-node@v4
- with:
- node-version: ${{ env.NODE_VERSION }}
-
- - name: Install Rust (Stable)
- if: ${{ matrix.download }}
+ - name: Install binutils-aarch64-linux-gnu
+ if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
run: |
- rustup default stable
+ sudo apt-get update
+ sudo apt-get install binutils-aarch64-linux-gnu -y
+
+ - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
+ if: ${{ contains(matrix.target, 'musl') }}
+ with:
+ version: 0.14.1
+ use-cache: false
+
+ - name: Install cargo-zigbuild
+ uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2
+ if: ${{ contains(matrix.target, 'musl') }}
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ with:
+ tool: cargo-zigbuild
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
- name: Install dependencies
- run: pnpm install --ignore-scripts --filter=!./playgrounds/*
+ run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
- name: Build release
- run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }}
+ run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
env:
RUST_TARGET: ${{ matrix.target }}
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
- if: ${{ matrix.strip }}
- run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
+ if: ${{ matrix.strip || matrix.strip-zig }}
+ env:
+ STRIP_COMMAND: ${{ matrix.strip }}
+ STRIP_ZIG: ${{ matrix.strip-zig }}
+ run: |
+ if [ "$STRIP_ZIG" = "true" ]; then
+ for file in ${{ env.OXIDE_LOCATION }}/*.node; do
+ zig objcopy --strip-all "$file" "$file.stripped"
+ mv "$file.stripped" "$file"
+ done
+ exit 0
+ fi
+
+ eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node"
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: bindings-${{ matrix.target }}
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -153,9 +154,11 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- name: Build FreeBSD
- uses: cross-platform-actions/action@v0.25.0
+ uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
env:
DEBUG: napi:*
RUSTUP_HOME: /usr/local/rustup
@@ -171,12 +174,12 @@ jobs:
shell: bash
run: |
sudo pkg install -y -f curl node libnghttp2 npm
- sudo npm install -g pnpm@9.6.0 --unsafe-perm=true
+ sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
chmod +x rustup-init
./rustup-init -y --profile minimal
source "$HOME/.cargo/env"
- pnpm install --ignore-scripts --filter=!./playgrounds/* || true
+ pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
echo "~~~~ rustc --version ~~~~"
rustc --version
echo "~~~~ node -v ~~~~"
@@ -187,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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: bindings-x86_64-unknown-freebsd
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -198,18 +201,17 @@ jobs:
name: Build and release Tailwind CSS
permissions:
- contents: write # for softprops/action-gh-release to create GitHub release
- # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
- id-token: write
+ contents: write # Required for creating releases
needs:
- build
- build-freebsd
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 20
+ persist-credentials: false
- run: git fetch --tags -f
@@ -218,51 +220,25 @@ jobs:
run: |
echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- - uses: pnpm/action-setup@v4
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
+ with:
+ version: ${{ env.PNPM_VERSION }}
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.NODE_VERSION }}
- cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
-
- # Cargo already skips downloading dependencies if they already exist
- - name: Cache cargo
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/bin/
- ~/.cargo/registry/index/
- ~/.cargo/registry/cache/
- ~/.cargo/git/db/
- target/
- key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
-
- # Cache the `oxide` Rust build
- - name: Cache oxide build
- uses: actions/cache@v4
- with:
- path: |
- ./crates/node/*.node
- ./crates/node/*.wasm
- ./crates/node/index.d.ts
- ./crates/node/index.js
- ./crates/node/browser.js
- ./crates/node/tailwindcss-oxide.wasi-browser.js
- ./crates/node/tailwindcss-oxide.wasi.cjs
- ./crates/node/wasi-worker-browser.mjs
- ./crates/node/wasi-worker.mjs
- key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
+ package-manager-cache: false
- name: Setup WASM target
run: rustup target add wasm32-wasip1-threads
- name: Install dependencies
- run: pnpm --filter=!./playgrounds/* install
+ run: pnpm --filter=!./playgrounds/* install --frozen-lockfile
- name: Download artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: ${{ env.OXIDE_LOCATION }}
@@ -301,30 +277,31 @@ jobs:
echo "EOF" >> $GITHUB_ENV
- name: Upload standalone artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: tailwindcss-standalone
path: packages/@tailwindcss-standalone/dist/
- name: Upload npm package tarballs
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: npm-package-tarballs
path: dist/*.tgz
- name: Prepare GitHub Release
- uses: softprops/action-gh-release@v2
- with:
- draft: true
- tag_name: ${{ env.TAG_NAME }}
- body: |
- ${{ env.RELEASE_NOTES }}
- files: |
- packages/@tailwindcss-standalone/dist/sha256sums.txt
- packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64
- packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl
- packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64
- packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl
- packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64
- packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64
- packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe
+ if: ${{ !inputs.dry_run }}
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh release create "${TAG_NAME}" \
+ --draft \
+ --title "${TAG_NAME}" \
+ --notes "${RELEASE_NOTES}" \
+ packages/@tailwindcss-standalone/dist/sha256sums.txt \
+ packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64 \
+ packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl \
+ packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64 \
+ packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl \
+ packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64 \
+ packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64 \
+ packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe
diff --git a/.github/workflows/release-insiders.yml b/.github/workflows/release-insiders.yml
deleted file mode 100644
index 53478a227..000000000
--- a/.github/workflows/release-insiders.yml
+++ /dev/null
@@ -1,318 +0,0 @@
-name: Release Insiders
-
-on:
- push:
- branches: [main]
-
-permissions:
- contents: read
-
-env:
- APP_NAME: tailwindcss-oxide
- NODE_VERSION: 20
- OXIDE_LOCATION: ./crates/node
- RELEASE_CHANNEL: insiders
-
-jobs:
- build:
- strategy:
- matrix:
- include:
- # Windows
- - os: windows-latest
- target: x86_64-pc-windows-msvc
- - os: windows-latest
- target: aarch64-pc-windows-msvc
- # macOS
- - os: macos-latest
- target: x86_64-apple-darwin
- strip: strip -x # Must use -x on macOS. This produces larger results on linux.
- - os: macos-latest
- target: aarch64-apple-darwin
- page-size: 14
- strip: strip -x # Must use -x on macOS. This produces larger results on linux.
- # Android
- - os: ubuntu-latest
- target: aarch64-linux-android
- strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
- - os: ubuntu-latest
- target: armv7-linux-androideabi
- strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
- # Linux
- - os: ubuntu-latest
- target: x86_64-unknown-linux-gnu
- strip: strip
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
- - os: ubuntu-latest
- target: aarch64-unknown-linux-gnu
- strip: llvm-strip
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
- - os: ubuntu-latest
- target: armv7-unknown-linux-gnueabihf
- strip: llvm-strip
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
- - os: ubuntu-latest
- target: aarch64-unknown-linux-musl
- strip: aarch64-linux-musl-strip
- download: true
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
- - os: ubuntu-latest
- target: x86_64-unknown-linux-musl
- strip: strip
- download: true
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
-
- name: Build ${{ matrix.target }} (oxide)
- runs-on: ${{ matrix.os }}
- container: ${{ matrix.container }}
- timeout-minutes: 15
- steps:
- - uses: actions/checkout@v4
- - uses: pnpm/action-setup@v4
-
- - name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ env.NODE_VERSION }}
- cache: 'pnpm'
-
- - name: Install gcc-arm-linux-gnueabihf
- if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
- run: |
- sudo apt-get update
- sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
-
- # Cargo already skips downloading dependencies if they already exist
- - name: Cache cargo
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/bin/
- ~/.cargo/registry/index/
- ~/.cargo/registry/cache/
- ~/.cargo/git/db/
- target/
- key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
-
- # Cache the `oxide` Rust build
- - name: Cache oxide build
- uses: actions/cache@v4
- with:
- path: |
- ./crates/node/*.node
- ./crates/node/*.wasm
- ./crates/node/index.d.ts
- ./crates/node/index.js
- ./crates/node/browser.js
- ./crates/node/tailwindcss-oxide.wasi-browser.js
- ./crates/node/tailwindcss-oxide.wasi.cjs
- ./crates/node/wasi-worker-browser.mjs
- ./crates/node/wasi-worker.mjs
- key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
-
- - name: Install Node.JS
- uses: actions/setup-node@v4
- with:
- node-version: ${{ env.NODE_VERSION }}
-
- - name: Install Rust (Stable)
- if: ${{ matrix.download }}
- run: |
- rustup default stable
-
- - name: Setup rust target
- run: rustup target add ${{ matrix.target }}
-
- - name: Install dependencies
- run: pnpm install --ignore-scripts --filter=!./playgrounds/*
-
- - name: Build release
- run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }}
- env:
- RUST_TARGET: ${{ matrix.target }}
- JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
-
- - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
- if: ${{ matrix.strip }}
- run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
-
- - name: Upload artifacts
- uses: actions/upload-artifact@v4
- with:
- name: bindings-${{ matrix.target }}
- path: ${{ env.OXIDE_LOCATION }}/*.node
-
- build-freebsd:
- name: Build x86_64-unknown-freebsd (OXIDE)
- runs-on: ubuntu-latest
- timeout-minutes: 15
- steps:
- - uses: actions/checkout@v4
- - name: Build FreeBSD
- uses: cross-platform-actions/action@v0.25.0
- env:
- DEBUG: napi:*
- RUSTUP_HOME: /usr/local/rustup
- CARGO_HOME: /usr/local/cargo
- RUSTUP_IO_THREADS: 1
- RUST_TARGET: x86_64-unknown-freebsd
- with:
- operating_system: freebsd
- version: '14.0'
- memory: 13G
- cpu_count: 3
- environment_variables: 'DEBUG RUSTUP_IO_THREADS'
- shell: bash
- run: |
- sudo pkg install -y -f curl node libnghttp2 npm
- sudo npm install -g pnpm@9.6.0 --unsafe-perm=true
- curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
- chmod +x rustup-init
- ./rustup-init -y --profile minimal
- source "$HOME/.cargo/env"
- echo "~~~~ rustc --version ~~~~"
- rustc --version
- echo "~~~~ node -v ~~~~"
- node -v
- echo "~~~~ pnpm --version ~~~~"
- pnpm --version
- pnpm install --ignore-scripts --filter=!./playgrounds/* || true
- pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform
- strip -x ${{ env.OXIDE_LOCATION }}/*.node
- ls -la ${{ env.OXIDE_LOCATION }}
- - name: Upload artifacts
- uses: actions/upload-artifact@v4
- with:
- name: bindings-x86_64-unknown-freebsd
- path: ${{ env.OXIDE_LOCATION }}/*.node
-
- release:
- runs-on: macos-14
- timeout-minutes: 15
- name: Build and release Tailwind CSS insiders
-
- permissions:
- contents: write # for softprops/action-gh-release to create GitHub release
- # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
- id-token: write
-
- needs:
- - build
- - build-freebsd
-
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 20
-
- - name: Resolve version
- id: vars
- run: |
- echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
-
- - uses: pnpm/action-setup@v4
-
- - name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ env.NODE_VERSION }}
- cache: 'pnpm'
- registry-url: 'https://registry.npmjs.org'
-
- # Cargo already skips downloading dependencies if they already exist
- - name: Cache cargo
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/bin/
- ~/.cargo/registry/index/
- ~/.cargo/registry/cache/
- ~/.cargo/git/db/
- target/
- key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
-
- # Cache the `oxide` Rust build
- - name: Cache oxide build
- uses: actions/cache@v4
- with:
- path: |
- ./crates/node/*.node
- ./crates/node/*.wasm
- ./crates/node/index.d.ts
- ./crates/node/index.js
- ./crates/node/browser.js
- ./crates/node/tailwindcss-oxide.wasi-browser.js
- ./crates/node/tailwindcss-oxide.wasi.cjs
- ./crates/node/wasi-worker-browser.mjs
- ./crates/node/wasi-worker.mjs
- key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
-
- - name: Setup WASM target
- run: rustup target add wasm32-wasip1-threads
-
- - name: Install dependencies
- run: pnpm --filter=!./playgrounds/* install
-
- - name: Download artifacts
- uses: actions/download-artifact@v4
- with:
- path: ${{ env.OXIDE_LOCATION }}
-
- - name: Move artifacts
- run: |
- cd ${{ env.OXIDE_LOCATION }}
- cp bindings-x86_64-pc-windows-msvc/* ./npm/win32-x64-msvc/
- cp bindings-aarch64-pc-windows-msvc/* ./npm/win32-arm64-msvc/
- cp bindings-x86_64-apple-darwin/* ./npm/darwin-x64/
- cp bindings-aarch64-apple-darwin/* ./npm/darwin-arm64/
- cp bindings-aarch64-linux-android/* ./npm/android-arm64/
- cp bindings-armv7-linux-androideabi/* ./npm/android-arm-eabi/
- cp bindings-aarch64-unknown-linux-gnu/* ./npm/linux-arm64-gnu/
- cp bindings-aarch64-unknown-linux-musl/* ./npm/linux-arm64-musl/
- cp bindings-armv7-unknown-linux-gnueabihf/* ./npm/linux-arm-gnueabihf/
- cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/
- cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
- cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/
-
- - name: 'Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
- run: pnpm run version-packages 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}
-
- - name: Build Tailwind CSS
- run: pnpm run build
-
- - name: Run pre-publish optimizations scripts
- run: node ./scripts/pre-publish-optimizations.mjs
-
- - name: Lock pre-release versions
- run: node ./scripts/lock-pre-release-versions.mjs
-
- - name: Upload npm package tarballs
- uses: actions/upload-artifact@v4
- with:
- name: npm-package-tarballs
- path: dist/*.tgz
-
- - name: Publish
- run: |
- pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
- # The wasm package needs a special npm config that isn't read when pnpm --recursive is used
- pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks; popd;
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
-
- - name: Trigger Tailwind Play update
- uses: actions/github-script@v7
- with:
- github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
- script: |
- await github.rest.actions.createWorkflowDispatch({
- owner: 'tailwindlabs',
- repo: 'upgrades',
- ref: 'main',
- workflow_id: 'upgrade-tailwindcss.yml'
- })
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8b27773b3..2c957d3af 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,18 +1,34 @@
name: Release
on:
+ push:
+ branches: [main]
release:
types: [published]
workflow_dispatch:
+ inputs:
+ channel:
+ description: Release channel to publish
+ required: true
+ default: insiders
+ type: choice
+ options:
+ - insiders
+ - release
permissions:
contents: read
env:
APP_NAME: tailwindcss-oxide
- NODE_VERSION: 20
+ NODE_VERSION: 24
+ PNPM_VERSION: '11.9.0'
OXIDE_LOCATION: ./crates/node
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
strategy:
@@ -42,44 +58,40 @@ jobs:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
strip: strip
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
+ build-flags: --use-napi-cross
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- strip: llvm-strip
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
+ strip: aarch64-linux-gnu-strip
+ build-flags: --use-napi-cross
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
- strip: llvm-strip
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
+ strip: arm-linux-gnueabihf-strip
+ build-flags: --use-napi-cross
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
- strip: aarch64-linux-musl-strip
- download: true
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
+ strip-zig: true
+ build-flags: -x
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
strip: strip
- download: true
- container:
- image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
+ build-flags: -x
name: Build ${{ matrix.target }} (oxide)
runs-on: ${{ matrix.os }}
- container: ${{ matrix.container }}
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
- - uses: pnpm/action-setup@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
+ with:
+ version: ${{ env.PNPM_VERSION }}
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.NODE_VERSION }}
- cache: 'pnpm'
+ package-manager-cache: false
- name: Install gcc-arm-linux-gnueabihf
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
@@ -87,62 +99,56 @@ jobs:
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
- # Cargo already skips downloading dependencies if they already exist
- - name: Cache cargo
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/bin/
- ~/.cargo/registry/index/
- ~/.cargo/registry/cache/
- ~/.cargo/git/db/
- target/
- key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
-
- # Cache the `oxide` Rust build
- - name: Cache oxide build
- uses: actions/cache@v4
- with:
- path: |
- ./crates/node/*.node
- ./crates/node/*.wasm
- ./crates/node/index.d.ts
- ./crates/node/index.js
- ./crates/node/browser.js
- ./crates/node/tailwindcss-oxide.wasi-browser.js
- ./crates/node/tailwindcss-oxide.wasi.cjs
- ./crates/node/wasi-worker-browser.mjs
- ./crates/node/wasi-worker.mjs
- key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
-
- - name: Install Node.JS
- uses: actions/setup-node@v4
- with:
- node-version: ${{ env.NODE_VERSION }}
-
- - name: Install Rust (Stable)
- if: ${{ matrix.download }}
+ - name: Install binutils-aarch64-linux-gnu
+ if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
run: |
- rustup default stable
+ sudo apt-get update
+ sudo apt-get install binutils-aarch64-linux-gnu -y
+
+ - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
+ if: ${{ contains(matrix.target, 'musl') }}
+ with:
+ version: 0.14.1
+ use-cache: false
+
+ - name: Install cargo-zigbuild
+ uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2
+ if: ${{ contains(matrix.target, 'musl') }}
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ with:
+ tool: cargo-zigbuild
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
- name: Install dependencies
- run: pnpm install --ignore-scripts --filter=!./playgrounds/*
+ run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
- name: Build release
- run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }}
+ run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
env:
RUST_TARGET: ${{ matrix.target }}
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
- if: ${{ matrix.strip }}
- run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
+ if: ${{ matrix.strip || matrix.strip-zig }}
+ env:
+ STRIP_COMMAND: ${{ matrix.strip }}
+ STRIP_ZIG: ${{ matrix.strip-zig }}
+ run: |
+ if [ "$STRIP_ZIG" = "true" ]; then
+ for file in ${{ env.OXIDE_LOCATION }}/*.node; do
+ zig objcopy --strip-all "$file" "$file.stripped"
+ mv "$file.stripped" "$file"
+ done
+ exit 0
+ fi
+
+ eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node"
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: bindings-${{ matrix.target }}
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -152,9 +158,11 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- name: Build FreeBSD
- uses: cross-platform-actions/action@v0.25.0
+ uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
env:
DEBUG: napi:*
RUSTUP_HOME: /usr/local/rustup
@@ -170,7 +178,7 @@ jobs:
shell: bash
run: |
sudo pkg install -y -f curl node libnghttp2 npm
- sudo npm install -g pnpm@9.6.0 --unsafe-perm=true
+ sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
chmod +x rustup-init
./rustup-init -y --profile minimal
@@ -181,12 +189,12 @@ jobs:
node -v
echo "~~~~ pnpm --version ~~~~"
pnpm --version
- pnpm install --ignore-scripts --filter=!./playgrounds/* || true
+ pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform
strip -x ${{ env.OXIDE_LOCATION }}/*.node
ls -la ${{ env.OXIDE_LOCATION }}
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: bindings-x86_64-unknown-freebsd
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -194,10 +202,10 @@ jobs:
release:
runs-on: macos-14
timeout-minutes: 15
- name: Build and release Tailwind CSS
+ name: Build and publish Tailwind CSS
permissions:
- contents: write # for softprops/action-gh-release to create GitHub release
+ contents: read
# https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
id-token: write
@@ -206,55 +214,51 @@ jobs:
- build-freebsd
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 20
+ persist-credentials: false
- - uses: pnpm/action-setup@v4
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
+ with:
+ version: ${{ env.PNPM_VERSION }}
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.NODE_VERSION }}
- cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
+ package-manager-cache: false
- # Cargo already skips downloading dependencies if they already exist
- - name: Cache cargo
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/bin/
- ~/.cargo/registry/index/
- ~/.cargo/registry/cache/
- ~/.cargo/git/db/
- target/
- key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+ # npm trusted publishing validates the caller workflow filename, so all npm publishes live here.
+ # This workflow rebuilds the publish artifacts instead of depending on prepare-release.yml.
+ - name: Resolve release metadata
+ env:
+ INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }}
+ run: |
+ if [[ "${{ github.event_name }}" == "release" || "$INPUT_CHANNEL" == "release" ]]; then
+ release_channel=$(node ./scripts/release-channel.js)
- # Cache the `oxide` Rust build
- - name: Cache oxide build
- uses: actions/cache@v4
- with:
- path: |
- ./crates/node/*.node
- ./crates/node/*.wasm
- ./crates/node/index.d.ts
- ./crates/node/index.js
- ./crates/node/browser.js
- ./crates/node/tailwindcss-oxide.wasi-browser.js
- ./crates/node/tailwindcss-oxide.wasi.cjs
- ./crates/node/wasi-worker-browser.mjs
- ./crates/node/wasi-worker.mjs
- key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
+ echo "RELEASE_KIND=release" >> $GITHUB_ENV
+ echo "RELEASE_CHANNEL=$release_channel" >> $GITHUB_ENV
+ echo "FEATURES_ENV=stable" >> $GITHUB_ENV
+ else
+ sha_short=$(git rev-parse --short HEAD)
+
+ echo "RELEASE_KIND=insiders" >> $GITHUB_ENV
+ echo "RELEASE_CHANNEL=insiders" >> $GITHUB_ENV
+ echo "SHA_SHORT=$sha_short" >> $GITHUB_ENV
+ echo "INSIDERS_VERSION=0.0.0-insiders.$sha_short" >> $GITHUB_ENV
+ fi
- name: Setup WASM target
run: rustup target add wasm32-wasip1-threads
- name: Install dependencies
- run: pnpm --filter=!./playgrounds/* install
+ run: pnpm --filter=!./playgrounds/* install --frozen-lockfile
- name: Download artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: ${{ env.OXIDE_LOCATION }}
@@ -274,10 +278,19 @@ jobs:
cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/
+ - name: 'Version based on commit: ${{ env.INSIDERS_VERSION }}'
+ if: env.RELEASE_KIND == 'insiders'
+ run: pnpm run version-packages ${INSIDERS_VERSION}
+
- name: Build Tailwind CSS
+ if: env.RELEASE_KIND == 'insiders'
+ run: pnpm run build
+
+ - name: Build Tailwind CSS
+ if: env.RELEASE_KIND == 'release'
run: pnpm run build
env:
- FEATURES_ENV: stable
+ FEATURES_ENV: ${{ env.FEATURES_ENV }}
- name: Run pre-publish optimizations scripts
run: node ./scripts/pre-publish-optimizations.mjs
@@ -285,22 +298,20 @@ jobs:
- name: Lock pre-release versions
run: node ./scripts/lock-pre-release-versions.mjs
- - name: Calculate environment variables
- run: |
- echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
- echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./packages/tailwindcss/package.json`).version);')" >> $GITHUB_ENV
+ - name: Upload npm package tarballs
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ with:
+ name: npm-package-tarballs
+ path: dist/*.tgz
- name: Publish
run: |
- pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
+ pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${RELEASE_CHANNEL} --no-git-checks
# The wasm package needs a special npm config that isn't read when pnpm --recursive is used
- pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks; popd;
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${RELEASE_CHANNEL} --no-git-checks --config.node-linker=hoisted; popd;
- name: Trigger Tailwind Play update
- if: env.RELEASE_CHANNEL == 'latest'
- uses: actions/github-script@v7
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
script: |
@@ -310,3 +321,38 @@ jobs:
ref: 'main',
workflow_id: 'upgrade-tailwindcss.yml'
})
+
+ notify:
+ if: ${{ always() && (needs.build.result == 'failure' || needs.build-freebsd.result == 'failure' || needs.release.result == 'failure') }}
+ needs:
+ - build
+ - build-freebsd
+ - release
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
+
+ - name: Resolve release label
+ id: release
+ env:
+ INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }}
+ RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
+ run: |
+ if [[ "${{ github.event_name }}" == "release" ]]; then
+ tag_name="${RELEASE_TAG:-${GITHUB_REF_NAME}}"
+ echo "label=release ${tag_name}" >> $GITHUB_OUTPUT
+ elif [[ "$INPUT_CHANNEL" == "release" ]]; then
+ version=$(node -p "require('./packages/tailwindcss/package.json').version")
+ echo "label=release v${version}" >> $GITHUB_OUTPUT
+ else
+ sha_short=$(git rev-parse --short HEAD)
+ echo "label=insiders release 0.0.0-insiders.${sha_short}" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Notify Discord
+ uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
+ with:
+ webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
+ message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) for `${{ steps.release.outputs.label }}` has failed.'
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index ded82e2f6..000000000
--- a/.npmrc
+++ /dev/null
@@ -1 +0,0 @@
-auto-install-peers = true
diff --git a/.prettierignore b/.prettierignore
index 3de4530dd..4f50b932f 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 682b66628..e5e6f6f89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,13 +7,255 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Add `@tailwindcss/turbopack` package to run Tailwind CSS with Next.js ([20367](https://github.com/tailwindlabs/tailwindcss/pull/20367))
+
### Fixed
-- Ensure validation of `source(…)` happens relative to the file it is in ([#19274](https://github.com/tailwindlabs/tailwindcss/pull/19274))
+- Ensure watch mode detects changes to symlinked `@source` files whose real paths aren't otherwise scanned ([#20356](https://github.com/tailwindlabs/tailwindcss/pull/20356))
+- Ensure custom variants using `@scope` wrap the generated utilities instead of nesting inside them ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369))
+- Fix flattening of `@scope` at-rules ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369))
+- Fix standalone declarations in `@scope`, wrap them in `:where(:scope)` ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369))
+- Always emit a space for empty fallback values in CSS variables (e.g. `var(--tw-blur,)` → `var(--tw-blur, )`) ([#20373](https://github.com/tailwindlabs/tailwindcss/pull/20373))
+- Canonicalization: convert arbitrary breakpoint and container query variants to named equivalents (e.g. `max-[64rem]` → `max-lg`) ([#20380](https://github.com/tailwindlabs/tailwindcss/pull/20380))
+- Prevent `@tailwindcss/vite` from crashing on every edit under Vite's experimental `bundledDev` mode ([#20379](https://github.com/tailwindlabs/tailwindcss/pull/20379))
+- Ensure `@tailwindcss/oxide` falls back to WASM on platforms without native bindings ([#20383](https://github.com/tailwindlabs/tailwindcss/pull/20383))
+- Detect classes in Ruby percent literals using angle brackets or custom delimiters (e.g. `%w`, `%w|flex|`), including in Slim and Haml templates ([#20387](https://github.com/tailwindlabs/tailwindcss/pull/20387))
+- Preserve whitespace in `--default(…)` values in custom functional utilities (e.g. `--default(box alphabetic)` no longer becomes `boxalphabetic`) ([#20392](https://github.com/tailwindlabs/tailwindcss/pull/20392))
+- Don't scan gitignored directories (e.g. `node_modules` and `.git`) when the project uses a safelist-style `.gitignore` (e.g. `/*` followed by `!/…` negations) ([#20397](https://github.com/tailwindlabs/tailwindcss/pull/20397))
+- Ensure root `theme('…')` namespace lookups in JavaScript plugins and config files return the full namespace object instead of the value of its `DEFAULT` key ([#20399](https://github.com/tailwindlabs/tailwindcss/pull/20399))
+- Skip ignored directories entirely when computing watch globs (`scanner.globs`), instead of walking their full contents on every rebuild ([#20408](https://github.com/tailwindlabs/tailwindcss/pull/20408))
+- Oxide: drop invalid UTF-8 candidates ([#20389](https://github.com/tailwindlabs/tailwindcss/pull/20389))
+- `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414))
+- Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417))
+- Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419))
+- Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420))
+- Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434))
+- Don't generate CSS for candidates containing an empty additional modifier (e.g. `bg-red-500/50/` and `group-hover/foo//bar:flex`) ([#20466](https://github.com/tailwindlabs/tailwindcss/pull/20466))
+- Sort `min-*`, `max-*`, and container query variants with decimal values numerically (e.g. `min-[40.25rem]` before `min-[40.5rem]`) ([#20512](https://github.com/tailwindlabs/tailwindcss/pull/20512))
+- Ensure CSS comments ending with `\*/` are closed correctly instead of swallowing the CSS that follows (e.g. `/* C:\temp\*/`) ([#20508](https://github.com/tailwindlabs/tailwindcss/pull/20508))
+- Improve style invalidation performance of `group-*` and `peer-*` variants ([#20513](https://github.com/tailwindlabs/tailwindcss/pull/20513))
+
+## [4.3.3] - 2026-07-16
+
+### Fixed
+
+- Support `--watch --poll[=ms]` in `@tailwindcss/cli` when filesystem events are unreliable or unavailable ([#20297](https://github.com/tailwindlabs/tailwindcss/pull/20297))
+- Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298))
+- Prevent Preflight from overriding Firefox's native `iframe:focus-visible` outline styles ([#20292](https://github.com/tailwindlabs/tailwindcss/pull/20292))
+- Ensure `theme('colors.foo')` in JS plugins resolves correctly when both `--color-foo` and `--color-foo-bar` exist ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299))
+- Ensure fractional opacity modifiers work with named shadow sizes like `shadow-sm/12.5`, `text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and `inset-shadow-sm/12.5` ([#20302](https://github.com/tailwindlabs/tailwindcss/pull/20302))
+- Parse selectors like `[data-foo]div` as two selectors instead of one ([#20303](https://github.com/tailwindlabs/tailwindcss/pull/20303))
+- Ensure `@tailwindcss/postcss` rebuilds when a preprocessor like Sass changes the input CSS without changing the input file on disk ([#20310](https://github.com/tailwindlabs/tailwindcss/pull/20310))
+- Ensure CSS nesting is handled even when Lightning CSS isn't run, such as in `@tailwindcss/browser` and Tailwind Play ([#20124](https://github.com/tailwindlabs/tailwindcss/pull/20124))
+- Prevent achromatic theme colors from shifting hue when mixed in polar color spaces like `oklch` ([#20314](https://github.com/tailwindlabs/tailwindcss/pull/20314))
+- Ensure `--spacing(0)` is optimized to `0px` instead of `0` so it remains a `` when used in `calc(…)` ([#20319](https://github.com/tailwindlabs/tailwindcss/pull/20319))
+- Load `@parcel/watcher` only when needed in `@tailwindcss/cli --watch` mode, so one-off builds and `--watch --poll` work when `@parcel/watcher` can't be loaded ([#20325](https://github.com/tailwindlabs/tailwindcss/pull/20325))
+- Use explicit platform fonts instead of `system-ui` and `ui-sans-serif` so CJK text respects the page's `lang` attribute on Windows ([#20318](https://github.com/tailwindlabs/tailwindcss/pull/20318))
+- Prevent `@tailwindcss/upgrade` from rewriting ignored files when run from a subdirectory ([#20329](https://github.com/tailwindlabs/tailwindcss/pull/20329))
+- Ensure earlier `@source` rules pointing to nested files are scanned when later `@source` rules point to files in parent folders ([#20335](https://github.com/tailwindlabs/tailwindcss/pull/20335))
+- Prevent `@tailwindcss/vite` from triggering full page reloads when scanned files are processed by Vite but haven't been loaded as modules yet ([#20336](https://github.com/tailwindlabs/tailwindcss/pull/20336))
+
+## [4.3.2] - 2026-06-26
+
+### Fixed
+
+- Support bare spacing values for `auto-rows-*` and `auto-cols-*` utilities (e.g. `auto-rows-12` and `auto-cols-16`) ([#20229](https://github.com/tailwindlabs/tailwindcss/pull/20229))
+- Prevent `@tailwindcss/cli` in `--watch` mode from crashing on Windows when `@source` points to a directory that doesn't exist ([#20242](https://github.com/tailwindlabs/tailwindcss/pull/20242))
+- Prevent `@tailwindcss/vite` from crashing in Deno v2.8.x when `context.parentURL` is not a valid URL ([#20245](https://github.com/tailwindlabs/tailwindcss/pull/20245))
+- Ensure `@tailwindcss/cli` in `--watch` mode rebuilds when the input CSS file changes in an ignored directory ([#20246](https://github.com/tailwindlabs/tailwindcss/pull/20246))
+- Allow `@variant` rules used in `addBase(…)` to use custom variants defined later ([#20247](https://github.com/tailwindlabs/tailwindcss/pull/20247))
+- Prevent `@tailwindcss/vite` from crashing during HMR when scanned files or directories are deleted ([#20259](https://github.com/tailwindlabs/tailwindcss/pull/20259))
+- Generate `font-size` instead of `color` declarations for `text-[--spacing(…)]` ([#20260](https://github.com/tailwindlabs/tailwindcss/pull/20260))
+- Prevent `@source` patterns from scanning unrelated sibling files and folders ([#20263](https://github.com/tailwindlabs/tailwindcss/pull/20263))
+- Extract class candidates adjacent to Template Toolkit delimiters like `%]…[%` in `.tt`, `.tt2`, and `.tx` files ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269))
+- Extract class candidates from conditional Maud syntax like `p.text-black[condition]` ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269))
+- Prevent `@position-try` rules from triggering unknown at-rule warnings when optimizing CSS ([#20277](https://github.com/tailwindlabs/tailwindcss/pull/20277))
+- Support class suggestions for named opacity modifiers from `--opacity` theme values ([#20287](https://github.com/tailwindlabs/tailwindcss/pull/20287))
+- Prevent type errors in `@tailwindcss/postcss` when used with newer PostCSS patch releases ([#20289](https://github.com/tailwindlabs/tailwindcss/pull/20289))
+
+## [4.3.1] - 2026-06-12
### Added
-- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
+- Add `--silent` option to suppress output in `@tailwindcss/cli` ([#20100](https://github.com/tailwindlabs/tailwindcss/pull/20100))
+
+### Fixed
+
+- Remove deprecation warnings by using `Module#registerHooks` instead of `Module#register` on Node 26+ ([#20028](https://github.com/tailwindlabs/tailwindcss/pull/20028))
+- Canonicalization: don't crash when plugin utilities throw for unsupported values ([#20052](https://github.com/tailwindlabs/tailwindcss/pull/20052))
+- Allow `@apply` to be used with CSS mixins ([#19427](https://github.com/tailwindlabs/tailwindcss/pull/19427))
+- Ensure `not-*` correctly negates `@container` queries, including `style(…)` queries ([#20059](https://github.com/tailwindlabs/tailwindcss/pull/20059))
+- Ensure `drop-shadow-*` color utilities work with custom shadow values containing `calc(…)` ([#20080](https://github.com/tailwindlabs/tailwindcss/pull/20080))
+- Fix 'Sourcemap is likely to be incorrect' warnings when using `@tailwindcss/vite` ([#20103](https://github.com/tailwindlabs/tailwindcss/pull/20103))
+- Ensure `@tailwindcss/webpack` can be installed in Rspack projects without requiring `webpack` as a peer dependency ([#20027](https://github.com/tailwindlabs/tailwindcss/pull/20027))
+- Canonicalization: don't suggest invalid `calc(…)` expressions (e.g. `px-[calc(1rem+0px)]` → `px-[calc(1rem+0)]`) ([#20127](https://github.com/tailwindlabs/tailwindcss/pull/20127))
+- Canonicalization: avoid suggesting large spacing-scale values for arbitrary lengths (e.g. `left-[99999px]` → `left-[99999px]`, not `left-24999.75`) ([#20130](https://github.com/tailwindlabs/tailwindcss/pull/20130))
+- Ensure `@tailwindcss/cli` in `--watch` mode recovers when a tracked dependency is deleted and restored ([#20137](https://github.com/tailwindlabs/tailwindcss/pull/20137))
+- Ensure standalone `@tailwindcss/cli` binaries are ignored when scanning for class candidates ([#20139](https://github.com/tailwindlabs/tailwindcss/pull/20139))
+- Ensure class candidates are extracted from Twig `addClass(…)` and `removeClass(…)` calls ([#20198](https://github.com/tailwindlabs/tailwindcss/pull/20198))
+- Don't crash in the Ruby or Vue preprocessors when scanning files containing invalid UTF-8 bytes ([#19588](https://github.com/tailwindlabs/tailwindcss/pull/19588))
+- Allow `@variant` to be used inside `addBase` ([#19480](https://github.com/tailwindlabs/tailwindcss/pull/19480))
+- Ensure `@source` globs with symlinks are preserved ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
+- Ensure later `@source` rules can re-include files excluded by earlier `@source not` rules ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
+- Upgrade: don't migrate empty class rules to invalid `@utility` rules ([#20205](https://github.com/tailwindlabs/tailwindcss/pull/20205))
+- Ensure transitions between `inset-shadow-none` and other inset shadows work correctly ([#20208](https://github.com/tailwindlabs/tailwindcss/pull/20208))
+- Ensure explicitly referenced `@source` directories are scanned even when ignored by git ([#20214](https://github.com/tailwindlabs/tailwindcss/pull/20214))
+- Ensure `@source` globs ending in `**/*` preserve dynamic path segments to avoid scanning too many files ([#20217](https://github.com/tailwindlabs/tailwindcss/pull/20217))
+- Canonicalization: don't fold `calc(…)` divisions when the result would require high precision (e.g. `w-[calc(100%/3.5)]` → `w-[calc(100%/3.5)]`, not `w-[28.571428571428573%]`) ([#20221](https://github.com/tailwindlabs/tailwindcss/pull/20221))
+- Serve ESM type declarations to ESM importers of `@tailwindcss/postcss` ([#20228](https://github.com/tailwindlabs/tailwindcss/pull/20228))
+
+### Changed
+
+- Generate `0` instead of `calc(var(--spacing) * 0)` for spacing utilities like `m-0` and `left-0` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196))
+- Generate `var(--spacing)` instead of `calc(var(--spacing) * 1)` for spacing utilities like `m-1` and `left-1` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196))
+
+## [4.3.0] - 2026-05-08
+
+### Added
+
+- Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
+- Add `scrollbar-{auto,thin,none}` utilities for `scrollbar-width`, and `scrollbar-thumb-*` / `scrollbar-track-*` color utilities for `scrollbar-color` ([#19981](https://github.com/tailwindlabs/tailwindcss/pull/19981), [#20019](https://github.com/tailwindlabs/tailwindcss/pull/20019))
+- Add `scrollbar-gutter-*` utilities ([#20018](https://github.com/tailwindlabs/tailwindcss/pull/20018))
+- Add `zoom-*` utilities ([#20020](https://github.com/tailwindlabs/tailwindcss/pull/20020))
+- Add `tab-*` utilities ([#20022](https://github.com/tailwindlabs/tailwindcss/pull/20022))
+- Allow using `@variant` with stacked variants (e.g. `@variant hover:focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
+- Allow using `@variant` with compound variants (e.g. `@variant hover, focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
+- Support `--default(…)` in `--value(…)` and `--modifier(…)` for functional `@utility` definitions ([#19989](https://github.com/tailwindlabs/tailwindcss/pull/19989))
+
+### Fixed
+
+- Ensure `@plugin` resolves package JavaScript entries instead of browser CSS entries when using `@tailwindcss/vite` ([#19949](https://github.com/tailwindlabs/tailwindcss/pull/19949))
+- Fix relative `@import` and `@plugin` paths resolving from the wrong directory when using `@tailwindcss/vite` ([#19965](https://github.com/tailwindlabs/tailwindcss/pull/19965))
+- Ensure CSS files containing `@variant` are processed by `@tailwindcss/vite` ([#19966](https://github.com/tailwindlabs/tailwindcss/pull/19966))
+- Resolve imports relative to `base` when `result.opts.from` is not provided when using `@tailwindcss/postcss` ([#19980](https://github.com/tailwindlabs/tailwindcss/pull/19980))
+- Canonicalization: preserve significant `_` whitespace in arbitrary values ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986))
+- Canonicalization: add parentheses when removing whitespace from arbitrary values would hurt readability (e.g. `w-[calc(100%---spacing(60))]` → `w-[calc(100%-(--spacing(60)))]`) ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986))
+- Canonicalization: preserve the original unit in arbitrary values instead of normalizing to base units (e.g. `-mt-[20in]` → `mt-[-20in]`, not `mt-[-1920px]`) ([#19988](https://github.com/tailwindlabs/tailwindcss/pull/19988))
+- Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#19991](https://github.com/tailwindlabs/tailwindcss/pull/19991))
+- Upgrade: don’t migrate inline `style` attributes (e.g. `style="flex-grow: 1"` → `style="flex-grow: 1"`, not `style="grow: 1"`) ([#19918](https://github.com/tailwindlabs/tailwindcss/pull/19918))
+- Allow multiple `@utility` definitions with the same name but different value types ([#19777](https://github.com/tailwindlabs/tailwindcss/pull/19777))
+- Export missing `PluginWithConfig` type from `tailwindcss/plugin` to fix errors when inferring plugin config types ([#19707](https://github.com/tailwindlabs/tailwindcss/pull/19707))
+- Ensure `start` and `end` legacy utilities without values do not generate CSS ([#20003](https://github.com/tailwindlabs/tailwindcss/pull/20003))
+- Ensure `--value(…)` is required in functional `@utility` definitions ([#20005](https://github.com/tailwindlabs/tailwindcss/pull/20005))
+- Canonicalization: preserve required whitespace around operators in negated arbitrary values (e.g. `-left-[(var(--a)+var(--b))]`) ([#20011](https://github.com/tailwindlabs/tailwindcss/pull/20011))
+
+## [4.2.4] - 2026-04-21
+
+### Fixed
+
+- Ensure imports in `@import` and `@plugin` still resolve correctly when using Vite aliases in `@tailwindcss/vite` ([#19947](https://github.com/tailwindlabs/tailwindcss/pull/19947))
+
+## [4.2.3] - 2026-04-20
+
+### Fixed
+
+- Canonicalization: improve canonicalization for `tracking-*` utilities by preferring non-negative utilities (e.g. `-tracking-tighter` → `tracking-wider`) ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827))
+- Fix crash due to invalid characters in candidate (exceeding valid unicode code point range) ([#19829](https://github.com/tailwindlabs/tailwindcss/pull/19829))
+- Ensure query params in imports are considered unique resources when using `@tailwindcss/webpack` ([#19723](https://github.com/tailwindlabs/tailwindcss/pull/19723))
+- Canonicalization: collapse arbitrary values into shorthand utilities (e.g. `px-[1.2rem] py-[1.2rem]` → `p-[1.2rem]`) ([#19837](https://github.com/tailwindlabs/tailwindcss/pull/19837))
+- Canonicalization: collapse `border-{t,b}-*` into `border-y-*`, `border-{l,r}-*` into `border-x-*`, and `border-{t,r,b,l}-*` into `border-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
+- Canonicalization: collapse `scroll-m{t,b}-*` into `scroll-my-*`, `scroll-m{l,r}-*` into `scroll-mx-*`, and `scroll-m{t,r,b,l}-*` into `scroll-m-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
+- Canonicalization: collapse `scroll-p{t,b}-*` into `scroll-py-*`, `scroll-p{l,r}-*` into `scroll-px-*`, and `scroll-p{t,r,b,l}-*` into `scroll-p-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
+- Canonicalization: collapse `overflow-{x,y}-*` into `overflow-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
+- Canonicalization: collapse `overscroll-{x,y}-*` into `overscroll-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
+- Read from `--placeholder-color` instead of `--background-color` for `placeholder-*` utilities ([#19843](https://github.com/tailwindlabs/tailwindcss/pull/19843))
+- Upgrade: ensure files are not emptied out when killing the upgrade process while it's running ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
+- Upgrade: use `config.content` when migrating from Tailwind CSS v3 to Tailwind CSS v4 ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
+- Upgrade: never migrate files that are ignored by git ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
+- Add `.env` and `.env.*` to default ignored content files ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
+- Canonicalization: migrate `overflow-ellipsis` into `text-ellipsis` ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
+- Canonicalization: migrate `start-full` → `inset-s-full`, `start-auto` → `inset-s-auto`, `start-px` → `inset-s-px`, and `start-` → `inset-s-` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
+- Canonicalization: migrate `end-full` → `inset-e-full`, `end-auto` → `inset-e-auto`, `end-px` → `inset-e-px`, and `end-` → `inset-e-` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
+- Canonicalization: move the `-` sign inside the arbitrary value `-left-[9rem]` → `left-[-9rem]` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858))
+- Canonicalization: move the `-` sign outside the arbitrary value `ml-[calc(-1*var(--width))]` → `-ml-(--width)` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858))
+- Improve performance when scanning JSONL / NDJSON files ([#19862](https://github.com/tailwindlabs/tailwindcss/pull/19862))
+- Support `NODE_PATH` environment variable in standalone CLI ([#19617](https://github.com/tailwindlabs/tailwindcss/pull/19617))
+
+## [4.2.2] - 2026-03-18
+
+### Fixed
+
+- Don't crash when candidates contain prototype properties like `row-constructor` ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725))
+- Canonicalize `calc(var(--spacing)*…)` expressions into `--spacing(…)` ([#19769](https://github.com/tailwindlabs/tailwindcss/pull/19769))
+- Fix crash in canonicalization step when handling utilities containing `@property` at-rules (e.g. `shadow-sm border`) ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727))
+- Skip full reload for server only modules scanned by client CSS when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745))
+- Add support for Vite 8 in `@tailwindcss/vite` ([#19790](https://github.com/tailwindlabs/tailwindcss/pull/19790))
+- Improve canonicalization for bare values exceeding default spacing scale suggestions (e.g. `w-1234 h-1234` → `size-1234`) ([#19809](https://github.com/tailwindlabs/tailwindcss/pull/19809))
+- Fix canonicalization resulting in empty list (e.g. `w-5 h-5 size-5` → `''` instead of `size-5`) ([#19812](https://github.com/tailwindlabs/tailwindcss/pull/19812))
+- Resolve tsconfig paths to allow for `@import '@/path/to/file';` when using `@tailwindcss/vite` ([#19803](https://github.com/tailwindlabs/tailwindcss/pull/19803))
+
+## [4.2.1] - 2026-02-23
+
+### Fixed
+
+- Allow trailing dash in functional utility names for backwards compatibility ([#19696](https://github.com/tailwindlabs/tailwindcss/pull/19696))
+- Properly detect classes containing `.` characters within curly braces in MDX files ([#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/19623))
+
+### 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))
+- Include filename and line numbers in CSS parse errors ([#19282](https://github.com/tailwindlabs/tailwindcss/pull/19282))
+- Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
+- Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
+- Support environment API in `@tailwindcss/vite` ([#18970](https://github.com/tailwindlabs/tailwindcss/pull/18970))
+- Preserve case of theme keys from JS configs and plugins ([#19337](https://github.com/tailwindlabs/tailwindcss/pull/19337))
+- Write source maps correctly on the CLI when using `--watch` ([#19373](https://github.com/tailwindlabs/tailwindcss/pull/19373))
+- Handle special defaults (like `ringColor.DEFAULT`) in JS configs ([#19348](https://github.com/tailwindlabs/tailwindcss/pull/19348))
+- Improve backwards compatibility for `content` theme key from JS configs ([#19381](https://github.com/tailwindlabs/tailwindcss/pull/19381))
+- Upgrade: Handle `future` and `experimental` config keys ([#19344](https://github.com/tailwindlabs/tailwindcss/pull/19344))
+- Try to canonicalize any arbitrary utility to a bare value ([#19379](https://github.com/tailwindlabs/tailwindcss/pull/19379))
+- Validate candidates similarly to Oxide ([#19397](https://github.com/tailwindlabs/tailwindcss/pull/19397))
+- Canonicalization: combine `text-*` and `leading-*` classes ([#19396](https://github.com/tailwindlabs/tailwindcss/pull/19396))
+- Correctly handle duplicate CLI arguments ([#19416](https://github.com/tailwindlabs/tailwindcss/pull/19416))
+- 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))
+
+## [3.4.19] - 2025-12-10
+
+### Fixed
+
+- Don’t break `sibling-*()` functions when used inside `calc(…)` ([#19335](https://github.com/tailwindlabs/tailwindcss/pull/19335))
## [4.1.17] - 2025-11-06
@@ -71,6 +313,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
@@ -1403,16 +1655,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
@@ -3881,11 +4123,23 @@ No release notes
- Everything!
-[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.17...HEAD
+[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.3...HEAD
+[4.3.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.2...v4.3.3
+[4.3.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.1...v4.3.2
+[4.3.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.0...v4.3.1
+[4.3.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.4...v4.3.0
+[4.2.4]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.3...v4.2.4
+[4.2.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.2...v4.2.3
+[4.2.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.1...v4.2.2
+[4.2.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.0...v4.2.1
+[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
@@ -3943,7 +4197,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/Cargo.lock b/Cargo.lock
index 162cf37dd..73fd9e995 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -40,7 +40,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
dependencies = [
"memchr",
- "regex-automata 0.4.8",
+ "regex-automata 0.4.18",
"serde",
]
@@ -60,10 +60,21 @@ dependencies = [
]
[[package]]
-name = "convert_case"
-version = "0.8.0"
+name = "console"
+version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
+checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
dependencies = [
"unicode-segmentation",
]
@@ -104,19 +115,9 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "ctor"
-version = "0.5.0"
+version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb"
-dependencies = [
- "ctor-proc-macro",
- "dtor",
-]
-
-[[package]]
-name = "ctor-proc-macro"
-version = "0.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
+checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f"
[[package]]
name = "diff"
@@ -124,21 +125,6 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
-[[package]]
-name = "dtor"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934"
-dependencies = [
- "dtor-proc-macro",
-]
-
-[[package]]
-name = "dtor-proc-macro"
-version = "0.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
-
[[package]]
name = "dunce"
version = "1.0.5"
@@ -151,6 +137,12 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
[[package]]
name = "errno"
version = "0.3.9"
@@ -177,15 +169,103 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
-name = "globset"
-version = "0.4.17"
+name = "futures"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "globset"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
dependencies = [
"aho-corasick",
"bstr",
"log",
- "regex-automata 0.4.8",
+ "regex-automata 0.4.18",
"regex-syntax 0.8.5",
]
@@ -210,7 +290,7 @@ dependencies = [
"globset",
"log",
"memchr",
- "regex-automata 0.4.8",
+ "regex-automata 0.4.18",
"same-file",
"walkdir",
"winapi-util",
@@ -218,7 +298,7 @@ dependencies = [
[[package]]
name = "ignore"
-version = "0.4.24"
+version = "0.4.33"
dependencies = [
"bstr",
"crossbeam-channel",
@@ -227,12 +307,24 @@ dependencies = [
"globset",
"log",
"memchr",
- "regex-automata 0.4.8",
+ "regex-automata 0.4.18",
"same-file",
"walkdir",
"winapi-util",
]
+[[package]]
+name = "insta"
+version = "1.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82"
+dependencies = [
+ "console",
+ "once_cell",
+ "similar",
+ "tempfile",
+]
+
[[package]]
name = "itertools"
version = "0.11.0"
@@ -256,12 +348,12 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libloading"
-version = "0.8.5"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
+checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
dependencies = [
"cfg-if",
- "windows-targets",
+ "windows-link",
]
[[package]]
@@ -299,12 +391,13 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "napi"
-version = "3.3.0"
+version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7"
+checksum = "de33522036981030a75c231829566bc63414e08101a6f5ff4ac6cef19c8e0941"
dependencies = [
"bitflags",
"ctor",
+ "futures",
"napi-build",
"napi-sys",
"nohash-hasher",
@@ -313,15 +406,15 @@ dependencies = [
[[package]]
name = "napi-build"
-version = "2.2.3"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14"
+checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
[[package]]
name = "napi-derive"
-version = "3.2.5"
+version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0"
+checksum = "a49c513341a61a16a10af6efcce46b30d0822ba2d4fb197d24d33dfc199c78d5"
dependencies = [
"convert_case",
"ctor",
@@ -333,9 +426,9 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
-version = "2.2.0"
+version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a"
+checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd"
dependencies = [
"convert_case",
"proc-macro2",
@@ -346,9 +439,9 @@ dependencies = [
[[package]]
name = "napi-sys"
-version = "3.0.0"
+version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d"
+checksum = "85fbf1fa9f1babfe396d74bbbf52b3643770243e8f5b0b46715d4caf7f0dfc9a"
dependencies = [
"libloading",
]
@@ -381,9 +474,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.19.0"
+version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "overload"
@@ -418,18 +511,18 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.28"
+version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
-version = "1.10.0"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
@@ -437,9 +530,9 @@ dependencies = [
[[package]]
name = "rayon-core"
-version = "1.12.1"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
@@ -453,7 +546,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.4.8",
+ "regex-automata 0.4.18",
"regex-syntax 0.8.5",
]
@@ -468,9 +561,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.8"
+version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
@@ -538,6 +631,18 @@ dependencies = [
"lazy_static",
]
+[[package]]
+name = "similar"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
[[package]]
name = "smallvec"
version = "1.10.0"
@@ -546,9 +651,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "syn"
-version = "2.0.18"
+version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
+checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
@@ -576,7 +681,8 @@ dependencies = [
"dunce",
"fast-glob",
"globwalk",
- "ignore 0.4.24",
+ "ignore 0.4.33",
+ "insta",
"log",
"pretty_assertions",
"rayon",
@@ -738,6 +844,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -756,6 +868,15 @@ dependencies = [
"windows-targets",
]
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "windows-targets"
version = "0.52.6"
diff --git a/README.md b/README.md
index 7d21bd883..5f532607d 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/ignore/Cargo.toml b/crates/ignore/Cargo.toml
index bd0352d15..d0af38ba9 100644
--- a/crates/ignore/Cargo.toml
+++ b/crates/ignore/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "ignore"
-version = "0.4.24" #:version
+version = "0.4.33" #:version
authors = ["Andrew Gallant "]
description = """
A fast library for efficiently matching ignore files such as `.gitignore`
@@ -12,7 +12,10 @@ repository = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore"
readme = "README.md"
keywords = ["glob", "ignore", "gitignore", "pattern", "file"]
license = "Unlicense OR MIT"
+# CHANGED: Use an explicit edition instead of `edition.workspace = true` since this crate is
+# vendored into the Tailwind CSS workspace.
edition = "2024"
+rust-version = "1.88"
[lib]
name = "ignore"
@@ -20,15 +23,17 @@ bench = false
[dependencies]
crossbeam-deque = "0.8.3"
-globset = "0.4.17"
+# CHANGED: Use the published globset crate instead of a path dependency.
+globset = "0.4.20"
log = "0.4.20"
memchr = "2.6.3"
same-file = "1.0.6"
walkdir = "2.4.0"
+# CHANGED: Added `dunce` to canonicalize paths without UNC prefixes on Windows.
dunce = "1.0.5"
[dependencies.regex-automata]
-version = "0.4.0"
+version = "0.4.18"
default-features = false
features = ["std", "perf", "syntax", "meta", "nfa", "hybrid", "dfa-onepass"]
diff --git a/crates/ignore/examples/walk.rs b/crates/ignore/examples/walk.rs
index 9c627dc3e..c61d0515e 100644
--- a/crates/ignore/examples/walk.rs
+++ b/crates/ignore/examples/walk.rs
@@ -18,9 +18,7 @@ fn main() {
let stdout_thread = std::thread::spawn(move || {
let mut stdout = std::io::BufWriter::new(std::io::stdout());
for dent in rx {
- stdout
- .write_all(&Vec::from_path_lossy(dent.path()))
- .unwrap();
+ stdout.write_all(&Vec::from_path_lossy(dent.path())).unwrap();
stdout.write_all(b"\n").unwrap();
}
});
diff --git a/crates/ignore/src/default_types.rs b/crates/ignore/src/default_types.rs
index 4e060b76a..6b5bba0f0 100644
--- a/crates/ignore/src/default_types.rs
+++ b/crates/ignore/src/default_types.rs
@@ -47,6 +47,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
(&["cml"], &["*.cml"]),
(&["coffeescript"], &["*.coffee"]),
(&["config"], &["*.cfg", "*.conf", "*.config", "*.ini"]),
+ (&["container"], &["*Containerfile*", "*Dockerfile*"]),
(&["coq"], &["*.v"]),
(&["cpp"], &[
"*.[ChH]", "*.cc", "*.[ch]pp", "*.[ch]xx", "*.hh", "*.inl",
@@ -109,6 +110,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
(&["hbs"], &["*.hbs"]),
(&["hs"], &["*.hs", "*.lhs"]),
(&["html"], &["*.htm", "*.html", "*.ejs"]),
+ (&["hurl"], &["*.hurl"]),
(&["hy"], &["*.hy"]),
(&["idris"], &["*.idr", "*.lidr"]),
(&["janet"], &["*.janet"]),
@@ -185,6 +187,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
(&["mint"], &["*.mint"]),
(&["mk"], &["mkfile"]),
(&["ml"], &["*.ml"]),
+ (&["mojo"], &["*.mojo"]),
(&["motoko"], &["*.mo"]),
(&["msbuild"], &[
"*.csproj", "*.fsproj", "*.vcxproj", "*.proj", "*.props", "*.targets",
@@ -206,11 +209,12 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
"*.php", "*.php3", "*.php4", "*.php5", "*.php7", "*.php8",
"*.pht", "*.phtml"
]),
+ (&["pkgbuild"], &["PKGBUILD"]),
(&["po"], &["*.po"]),
(&["pod"], &["*.pod"]),
(&["postscript"], &["*.eps", "*.ps"]),
(&["prolog"], &["*.pl", "*.pro", "*.prolog", "*.P"]),
- (&["protobuf"], &["*.proto"]),
+ (&["proto", "protobuf"], &["*.proto"]),
(&["ps"], &["*.cdxml", "*.ps1", "*.ps1xml", "*.psd1", "*.psm1"]),
(&["puppet"], &["*.epp", "*.erb", "*.pp", "*.rb"]),
(&["purs"], &["*.purs"]),
@@ -231,6 +235,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
(&["red"], &["*.r", "*.red", "*.reds"]),
(&["rescript"], &["*.res", "*.resi"]),
(&["robot"], &["*.robot"]),
+ (&["rocq"], &["*.v"]),
(&["rst"], &["*.rst"]),
(&["ruby"], &[
// Idiomatic files
@@ -274,6 +279,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
(&["spark"], &["*.spark"]),
(&["spec"], &["*.spec"]),
(&["sql"], &["*.sql", "*.psql"]),
+ (&["ssa"], &["*.ssa"]),
(&["stylus"], &["*.styl"]),
(&["sv"], &["*.v", "*.vg", "*.sv", "*.svh", "*.h"]),
(&["svelte"], &["*.svelte", "*.svelte.ts"]),
@@ -359,4 +365,14 @@ mod tests {
previous_name = name;
}
}
+
+ #[test]
+ fn default_types_aliases_are_sorted() {
+ for (aliases, _) in DEFAULT_TYPES.iter() {
+ assert!(
+ aliases.is_sorted(),
+ "this alias list is not sorted: {aliases:?}",
+ );
+ }
+ }
}
diff --git a/crates/ignore/src/dir.rs b/crates/ignore/src/dir.rs
index 11b58f8ca..6bee724c8 100644
--- a/crates/ignore/src/dir.rs
+++ b/crates/ignore/src/dir.rs
@@ -16,7 +16,7 @@
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
- fs::{File, FileType},
+ fs::{self, File, FileType},
io::{self, BufRead},
path::{Path, PathBuf},
sync::{Arc, RwLock, Weak},
@@ -25,7 +25,7 @@ use std::{
use crate::{
gitignore::{self, Gitignore, GitignoreBuilder},
overrides::{self, Override},
- pathutil::{is_hidden, strip_prefix},
+ pathutil::{is_hidden_entry, strip_prefix},
types::{self, Types},
walk::DirEntry,
{Error, Match, PartialErrorBuilder},
@@ -91,7 +91,25 @@ struct IgnoreOptions {
/// Ignore is a matcher useful for recursively walking one or more directories.
#[derive(Clone, Debug)]
-pub(crate) struct Ignore(Arc);
+pub(crate) struct Ignore {
+ inner: Arc,
+ // Parent matchers are cached independently of the path being walked, but
+ // matching them still needs the canonicalized path originally passed to
+ // `add_parents`. For example, when walking `/tmp/project/src`, parent
+ // matchers use `/tmp/project/src` to rewrite `/tmp/project/src/foo.py`
+ // before matching it against ignore files from `/tmp/project` and its
+ // ancestors.
+ //
+ // For ripgrep itself, this means that `rg pat src tests` must rewrite
+ // `src/foo` relative to `.../src`, and not whatever root was prepared
+ // first.
+ //
+ // See: https://github.com/BurntSushi/ripgrep/pull/3420
+ // See: https://github.com/BurntSushi/ripgrep/issues/3376
+ // See: https://github.com/BurntSushi/ripgrep/issues/3419
+ // See: https://github.com/BurntSushi/ripgrep/issues/3320
+ absolute_base: Option>,
+}
#[derive(Clone, Debug)]
struct IgnoreInner {
@@ -112,12 +130,9 @@ struct IgnoreInner {
///
/// If this is the root directory or there are otherwise no more
/// directories to match, then `parent` is `None`.
- parent: Option,
+ parent: Option>,
/// Whether this is an absolute parent matcher, as added by add_parent.
is_absolute_parent: bool,
- /// The absolute base path of this matcher. Populated only if parent
- /// directories are added.
- absolute_base: Option>,
/// The directory that gitignores should be interpreted relative to.
///
/// Usually this is the directory containing the gitignore file. But in
@@ -152,34 +167,36 @@ struct IgnoreInner {
impl Ignore {
/// Return the directory path of this matcher.
+ #[cfg(test)]
pub(crate) fn path(&self) -> &Path {
- &self.0.dir
+ &self.inner.dir
}
/// Return true if this matcher has no parent.
pub(crate) fn is_root(&self) -> bool {
- self.0.parent.is_none()
- }
-
- /// Returns true if this matcher was added via the `add_parents` method.
- pub(crate) fn is_absolute_parent(&self) -> bool {
- self.0.is_absolute_parent
+ self.inner.parent.is_none()
}
/// Return this matcher's parent, if one exists.
pub(crate) fn parent(&self) -> Option {
- self.0.parent.clone()
+ self.inner.parent.as_ref().map(|parent| Ignore {
+ inner: parent.clone(),
+ absolute_base: self.absolute_base.clone(),
+ })
}
/// Create a new `Ignore` matcher with the parent directories of `dir`.
///
/// Note that this can only be called on an `Ignore` matcher with no
/// parents (i.e., `is_root` returns `true`). This will panic otherwise.
- pub(crate) fn add_parents>(&self, path: P) -> (Ignore, Option) {
- if !self.0.opts.parents
- && !self.0.opts.git_ignore
- && !self.0.opts.git_exclude
- && !self.0.opts.git_global
+ pub(crate) fn add_parents>(
+ &self,
+ path: P,
+ ) -> (Ignore, Option) {
+ if !self.inner.opts.parents
+ && !self.inner.opts.git_ignore
+ && !self.inner.opts.git_exclude
+ && !self.inner.opts.git_global
{
// If we never need info from parent directories, then don't do
// anything.
@@ -209,25 +226,34 @@ impl Ignore {
let mut errs = PartialErrorBuilder::default();
let mut ig = self.clone();
for parent in parents.into_iter().rev() {
- let mut compiled = self.0.compiled.write().unwrap();
+ let mut compiled = self.inner.compiled.write().unwrap();
if let Some(weak) = compiled.get(parent.as_os_str()) {
if let Some(prebuilt) = weak.upgrade() {
- ig = Ignore(prebuilt);
+ ig = Ignore {
+ inner: prebuilt,
+ absolute_base: Some(absolute_base.clone()),
+ };
continue;
}
}
let (mut igtmp, err) = ig.add_child_path(parent);
errs.maybe_push(err);
igtmp.is_absolute_parent = true;
- igtmp.absolute_base = Some(absolute_base.clone());
- igtmp.has_git = if self.0.opts.require_git && self.0.opts.git_ignore {
- parent.join(".git").exists() || parent.join(".jj").exists()
- } else {
- false
- };
+ igtmp.has_git =
+ if self.inner.opts.require_git && self.inner.opts.git_ignore {
+ parent.join(".git").exists() || parent.join(".jj").exists()
+ } else {
+ false
+ };
let ig_arc = Arc::new(igtmp);
- ig = Ignore(ig_arc.clone());
- compiled.insert(parent.as_os_str().to_os_string(), Arc::downgrade(&ig_arc));
+ ig = Ignore {
+ inner: ig_arc.clone(),
+ absolute_base: Some(absolute_base.clone()),
+ };
+ compiled.insert(
+ parent.as_os_str().to_os_string(),
+ Arc::downgrade(&ig_arc),
+ );
}
(ig, errs.into_error_option())
}
@@ -240,60 +266,161 @@ impl Ignore {
/// returned if it exists.
///
/// Note that all I/O errors are completely ignored.
- pub(crate) fn add_child>(&self, dir: P) -> (Ignore, Option) {
+ pub(crate) fn add_child>(
+ &self,
+ dir: P,
+ ) -> (Ignore, Option) {
let (ig, err) = self.add_child_path(dir.as_ref());
- (Ignore(Arc::new(ig)), err)
+ (
+ Ignore {
+ inner: Arc::new(ig),
+ absolute_base: self.absolute_base.clone(),
+ },
+ err,
+ )
+ }
+
+ /// Like add_child, but uses successful read_dir entries to reduce
+ /// probing when discovering ignore files.
+ pub(crate) fn add_child_with_entries>(
+ &self,
+ dir: P,
+ entries: &[fs::DirEntry],
+ ) -> (Ignore, Option) {
+ let files = self.collect_ignore_files(entries);
+ let (ig, err) = self.add_child_path_with_found_ignore_files(
+ dir.as_ref(),
+ Some(&files),
+ );
+ (
+ Ignore {
+ inner: Arc::new(ig),
+ absolute_base: self.absolute_base.clone(),
+ },
+ err,
+ )
}
/// Like add_child, but takes a full path and returns an IgnoreInner.
fn add_child_path(&self, dir: &Path) -> (IgnoreInner, Option) {
- let git_type =
- if self.0.opts.require_git && (self.0.opts.git_ignore || self.0.opts.git_exclude) {
- dir.join(".git").metadata().ok().map(|md| md.file_type())
- } else {
- None
- };
- let has_git = git_type.is_some() || dir.join(".jj").exists();
+ self.add_child_path_with_found_ignore_files(dir, None)
+ }
+
+ fn collect_ignore_files(
+ &self,
+ entries: &[fs::DirEntry],
+ ) -> IgnoreFilesFound {
+ let custom_ignore_filenames = &self.inner.custom_ignore_filenames;
+ let mut files = IgnoreFilesFound {
+ has_ignore: false,
+ has_git_ignore: false,
+ has_git_dir: false,
+ has_jj_dir: false,
+ custom_ignore_files: vec![false; custom_ignore_filenames.len()],
+ };
+ for entry in entries {
+ let file_name = entry.file_name();
+ if file_name == OsStr::new(".ignore") {
+ files.has_ignore = true;
+ } else if file_name == OsStr::new(".gitignore") {
+ files.has_git_ignore = true;
+ } else if file_name == OsStr::new(".git") {
+ files.has_git_dir = true;
+ } else if file_name == OsStr::new(".jj") {
+ files.has_jj_dir = true;
+ }
+ for (i, name) in custom_ignore_filenames.iter().enumerate() {
+ if file_name == name.as_os_str() {
+ files.custom_ignore_files[i] = true;
+ }
+ }
+ }
+ files
+ }
+
+ fn add_child_path_with_found_ignore_files(
+ &self,
+ dir: &Path,
+ ignore_files_list: Option<&IgnoreFilesFound>,
+ ) -> (IgnoreInner, Option) {
+ let check_vcs_dir = self.inner.opts.require_git
+ && (self.inner.opts.git_ignore || self.inner.opts.git_exclude);
+ let git_type = if check_vcs_dir
+ && ignore_files_list.is_none_or(|i| i.has_git_dir)
+ {
+ dir.join(".git").metadata().ok().map(|md| md.file_type())
+ } else {
+ None
+ };
+ let has_jj = check_vcs_dir
+ && ignore_files_list.is_none_or(|i| i.has_jj_dir)
+ && dir.join(".jj").exists();
+ let has_git = check_vcs_dir && (git_type.is_some() || has_jj);
let mut errs = PartialErrorBuilder::default();
- let custom_ig_matcher = if self.0.custom_ignore_filenames.is_empty() {
+ let custom_ig_matcher = if self
+ .inner
+ .custom_ignore_filenames
+ .is_empty()
+ {
Gitignore::empty()
} else {
- let (m, err) = create_gitignore(
- &dir,
- &dir,
- &self.0.custom_ignore_filenames,
- self.0.opts.ignore_case_insensitive,
- );
- errs.maybe_push(err);
- m
+ let custom_ignore_names: Vec<&OsString> = match ignore_files_list {
+ None => self.inner.custom_ignore_filenames.iter().collect(),
+ Some(m) => self
+ .inner
+ .custom_ignore_filenames
+ .iter()
+ .zip(m.custom_ignore_files.iter())
+ .filter(|&(_, &matched)| matched)
+ .map(|(name, _)| name)
+ .collect(),
+ };
+ if custom_ignore_names.is_empty() {
+ Gitignore::empty()
+ } else {
+ let (m, err) = create_gitignore(
+ &dir,
+ &dir,
+ &custom_ignore_names,
+ self.inner.opts.ignore_case_insensitive,
+ );
+ errs.maybe_push(err);
+ m
+ }
};
- let ig_matcher = if !self.0.opts.ignore {
+ let ig_matcher = if !self.inner.opts.ignore
+ || !ignore_files_list.is_none_or(|i| i.has_ignore)
+ {
Gitignore::empty()
} else {
let (m, err) = create_gitignore(
&dir,
&dir,
&[".ignore"],
- self.0.opts.ignore_case_insensitive,
+ self.inner.opts.ignore_case_insensitive,
);
errs.maybe_push(err);
m
};
- let gi_matcher = if !self.0.opts.git_ignore {
+ let gi_matcher = if !self.inner.opts.git_ignore
+ || !ignore_files_list.is_none_or(|i| i.has_git_ignore)
+ {
Gitignore::empty()
} else {
let (m, err) = create_gitignore(
&dir,
&dir,
&[".gitignore"],
- self.0.opts.ignore_case_insensitive,
+ self.inner.opts.ignore_case_insensitive,
);
errs.maybe_push(err);
m
};
- let gi_exclude_matcher = if !self.0.opts.git_exclude {
+ let gi_exclude_matcher = if !self.inner.opts.git_exclude
+ || !ignore_files_list.is_none_or(|i| i.has_git_dir)
+ {
Gitignore::empty()
} else {
match resolve_git_commondir(dir, git_type) {
@@ -302,7 +429,7 @@ impl Ignore {
&dir,
&git_dir,
&["info/exclude"],
- self.0.opts.ignore_case_insensitive,
+ self.inner.opts.ignore_case_insensitive,
);
errs.maybe_push(err);
m
@@ -314,32 +441,38 @@ impl Ignore {
}
};
let ig = IgnoreInner {
- compiled: self.0.compiled.clone(),
+ compiled: self.inner.compiled.clone(),
dir: dir.to_path_buf(),
- overrides: self.0.overrides.clone(),
- types: self.0.types.clone(),
- parent: Some(self.clone()),
+ overrides: self.inner.overrides.clone(),
+ types: self.inner.types.clone(),
+ parent: Some(self.inner.clone()),
is_absolute_parent: false,
- absolute_base: self.0.absolute_base.clone(),
- global_gitignores_relative_to: self.0.global_gitignores_relative_to.clone(),
- explicit_ignores: self.0.explicit_ignores.clone(),
- custom_ignore_filenames: self.0.custom_ignore_filenames.clone(),
+ global_gitignores_relative_to: self
+ .inner
+ .global_gitignores_relative_to
+ .clone(),
+ explicit_ignores: self.inner.explicit_ignores.clone(),
+ custom_ignore_filenames: self
+ .inner
+ .custom_ignore_filenames
+ .clone(),
custom_ignore_matcher: custom_ig_matcher,
ignore_matcher: ig_matcher,
- git_global_matcher: self.0.git_global_matcher.clone(),
+ git_global_matcher: self.inner.git_global_matcher.clone(),
git_ignore_matcher: gi_matcher,
git_exclude_matcher: gi_exclude_matcher,
has_git,
- opts: self.0.opts,
+ opts: self.inner.opts,
};
(ig, errs.into_error_option())
}
/// Returns true if at least one type of ignore rule should be matched.
fn has_any_ignore_rules(&self) -> bool {
- let opts = self.0.opts;
- let has_custom_ignore_files = !self.0.custom_ignore_filenames.is_empty();
- let has_explicit_ignores = !self.0.explicit_ignores.is_empty();
+ let opts = self.inner.opts;
+ let has_custom_ignore_files =
+ !self.inner.custom_ignore_filenames.is_empty();
+ let has_explicit_ignores = !self.inner.explicit_ignores.is_empty();
opts.ignore
|| opts.git_global
@@ -350,9 +483,12 @@ impl Ignore {
}
/// Like `matched`, but works with a directory entry instead.
- pub(crate) fn matched_dir_entry<'a>(&'a self, dent: &DirEntry) -> Match> {
+ pub(crate) fn matched_dir_entry<'a>(
+ &'a self,
+ dent: &DirEntry,
+ ) -> Match> {
let m = self.matched(dent.path(), dent.is_dir());
- if m.is_none() && self.0.opts.hidden && is_hidden(dent) {
+ if m.is_none() && self.inner.opts.hidden && is_hidden_entry(dent) {
return Match::Ignore(IgnoreMatch::hidden());
}
m
@@ -362,7 +498,11 @@ impl Ignore {
/// ignored or not.
///
/// The match contains information about its origin.
- fn matched<'a, P: AsRef>(&'a self, path: P, is_dir: bool) -> Match> {
+ pub(crate) fn matched<'a, P: AsRef>(
+ &'a self,
+ path: P,
+ is_dir: bool,
+ ) -> Match> {
// We need to be careful with our path. If it has a leading ./, then
// strip it because it causes nothing but trouble.
let mut path = path.as_ref();
@@ -373,9 +513,9 @@ impl Ignore {
// regardless of whether it's whitelist/ignore, then we quit and
// return that result immediately. Overrides have the highest
// precedence.
- if !self.0.overrides.is_empty() {
+ if !self.inner.overrides.is_empty() {
let mat = self
- .0
+ .inner
.overrides
.matched(path, is_dir)
.map(IgnoreMatch::overrides);
@@ -392,8 +532,9 @@ impl Ignore {
whitelisted = mat;
}
}
- if !self.0.types.is_empty() {
- let mat = self.0.types.matched(path, is_dir).map(IgnoreMatch::types);
+ if !self.inner.types.is_empty() {
+ let mat =
+ self.inner.types.matched(path, is_dir).map(IgnoreMatch::types);
if mat.is_ignore() {
return mat;
} else if mat.is_whitelist() {
@@ -405,98 +546,143 @@ impl Ignore {
/// Performs matching only on the ignore files for this directory and
/// all parent directories.
- fn matched_ignore<'a>(&'a self, path: &Path, is_dir: bool) -> Match> {
- let (mut m_custom_ignore, mut m_ignore, mut m_gi, mut m_gi_exclude, mut m_explicit) = (
- Match::None,
- Match::None,
- Match::None,
- Match::None,
- Match::None,
- );
- let any_git = !self.0.opts.require_git || self.parents().any(|ig| ig.0.has_git);
+ pub(crate) fn matched_ignore<'a>(
+ &'a self,
+ path: &Path,
+ is_dir: bool,
+ ) -> Match> {
+ let (
+ mut m_custom_ignore,
+ mut m_ignore,
+ mut m_gi,
+ mut m_gi_exclude,
+ mut m_explicit,
+ ) = (Match::None, Match::None, Match::None, Match::None, Match::None);
+ let any_git = !self.inner.opts.require_git
+ || self.parents().any(|ig| ig.inner.has_git);
let mut saw_git = false;
- for ig in self.parents().take_while(|ig| !ig.0.is_absolute_parent) {
+ for ig in self.parents().take_while(|ig| !ig.inner.is_absolute_parent)
+ {
if m_custom_ignore.is_none() {
- m_custom_ignore =
- ig.0.custom_ignore_matcher
- .matched(path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_custom_ignore = ig
+ .inner
+ .custom_ignore_matcher
+ .matched(path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if m_ignore.is_none() {
- m_ignore =
- ig.0.ignore_matcher
- .matched(path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_ignore = ig
+ .inner
+ .ignore_matcher
+ .matched(path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if any_git && !saw_git && m_gi.is_none() {
- m_gi =
- ig.0.git_ignore_matcher
- .matched(path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_gi = ig
+ .inner
+ .git_ignore_matcher
+ .matched(path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if any_git && !saw_git && m_gi_exclude.is_none() {
- m_gi_exclude =
- ig.0.git_exclude_matcher
- .matched(path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_gi_exclude = ig
+ .inner
+ .git_exclude_matcher
+ .matched(path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
- saw_git = saw_git || ig.0.has_git;
+ saw_git = saw_git || ig.inner.has_git;
}
- if self.0.opts.parents {
- if let Some(_) = self.absolute_base() {
- // CHANGED: We removed a code path that rewrote the `path` to be relative to
- // `self.absolute_base()` because it assumed that the every path is inside the base
- // which is not the case for us as we use `WalkBuilder#add` to add roots outside of the
- // base.
- for ig in self.parents().skip_while(|ig| !ig.0.is_absolute_parent) {
+ if self.inner.opts.parents {
+ if let Some(abs_parent_path) = self.absolute_base() {
+ // What we want to do here is take the absolute base path of
+ // this directory and join it with the path we're searching.
+ // The main issue we want to avoid is accidentally duplicating
+ // directory components, so we try to strip any common prefix
+ // off of `path`. Overall, this seems a little ham-fisted, but
+ // it does fix a nasty bug. It should do fine until we overhaul
+ // this crate.
+ let path = abs_parent_path.join(
+ self.parents()
+ .take_while(|ig| !ig.inner.is_absolute_parent)
+ .last()
+ .map_or(path, |ig| {
+ // This is a weird special case when ripgrep users
+ // search with just a `.`, as some tools do
+ // automatically (like consult). In this case, if
+ // we don't bail out now, the code below will strip
+ // a leading `.` from `path`, which might mangle
+ // a hidden file name!
+ if ig.inner.dir.as_path() == Path::new(".") {
+ return path;
+ }
+ let without_dot_slash = strip_if_is_prefix(
+ "./",
+ ig.inner.dir.as_path(),
+ );
+ let relative_base =
+ strip_if_is_prefix(without_dot_slash, path);
+ strip_if_is_prefix("/", relative_base)
+ }),
+ );
+
+ for ig in self
+ .parents()
+ .skip_while(|ig| !ig.inner.is_absolute_parent)
+ {
if m_custom_ignore.is_none() {
- m_custom_ignore =
- ig.0.custom_ignore_matcher
- .matched(&path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_custom_ignore = ig
+ .inner
+ .custom_ignore_matcher
+ .matched(&path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if m_ignore.is_none() {
- m_ignore =
- ig.0.ignore_matcher
- .matched(&path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_ignore = ig
+ .inner
+ .ignore_matcher
+ .matched(&path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if any_git && !saw_git && m_gi.is_none() {
- m_gi =
- ig.0.git_ignore_matcher
- .matched(&path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_gi = ig
+ .inner
+ .git_ignore_matcher
+ .matched(&path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if any_git && !saw_git && m_gi_exclude.is_none() {
- m_gi_exclude =
- ig.0.git_exclude_matcher
- .matched(&path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_gi_exclude = ig
+ .inner
+ .git_exclude_matcher
+ .matched(&path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
- saw_git = saw_git || ig.0.has_git;
+ saw_git = saw_git || ig.inner.has_git;
}
}
}
- for gi in self.0.explicit_ignores.iter().rev() {
- // CHANGED: We need to make sure that the explicit gitignore rules apply to the path
- //
- // path = Is the current file/folder we are traversing
- // gi.path() = Is the path of the custom gitignore file
- //
- // E.g.: If we have a custom rule for `/src/utils` with `**/*`, and we are looking at
- // just `/src`, then the `**/*` rules do not apply to this folder, so we can
- // ignore the current custom gitignore file.
- //
- if !path.starts_with(gi.path()) {
- continue;
- }
+ for gi in self.inner.explicit_ignores.iter().rev() {
if !m_explicit.is_none() {
break;
}
+ // CHANGED: We need to make sure that the explicit gitignore rules
+ // apply to the path
+ //
+ // path = Is the current file/folder we are traversing
+ // gi.path() = Is the path of the custom gitignore file
+ //
+ // E.g.: If we have a custom rule for `/src/utils` with `**/*`, and
+ // we are looking at just `/src`, then the `**/*` rules do
+ // not apply to this folder, so we can ignore the current
+ // custom gitignore file.
+ if !path.starts_with(gi.path()) {
+ continue;
+ }
m_explicit = gi.matched(&path, is_dir).map(IgnoreMatch::gitignore);
}
let m_global = if any_git {
- self.0
+ self.inner
.git_global_matcher
.matched(&path, is_dir)
.map(IgnoreMatch::gitignore)
@@ -504,59 +690,90 @@ impl Ignore {
Match::None
};
- // CHANGED: We added logic to configure an order in which the ignore files are respected and
- // allowed a whitelist in a later file to overrule a block on an earlier file.
+ // CHANGED: We added logic to configure an order in which the ignore
+ // files are respected. Explicitly added ignores (via
+ // `WalkBuilder::add_gitignore`) take precedence over all ignore files
+ // found on disk, and the first source with a definitive answer wins.
let order = [
// Manually added ignores
- &m_explicit,
+ m_explicit,
// .custom-ignore
- &m_custom_ignore,
+ m_custom_ignore,
// .ignore
- &m_ignore,
+ m_ignore,
// .gitignore
- &m_gi,
+ m_gi,
// .git/info/exclude
- &m_gi_exclude,
+ m_gi_exclude,
// Global gitignore
- &m_global,
+ m_global,
];
-
for check in order.into_iter() {
- if check.is_none() {
- continue;
+ if !check.is_none() {
+ return check;
}
-
- return check.clone();
}
-
- m_explicit
+ Match::None
}
/// Returns an iterator over parent ignore matchers, including this one.
pub(crate) fn parents(&self) -> Parents<'_> {
- Parents(Some(self))
+ Parents(Some(IgnoreRef { inner: &self.inner }))
}
/// Returns the first absolute path of the first absolute parent, if
/// one exists.
fn absolute_base(&self) -> Option<&Path> {
- self.0.absolute_base.as_ref().map(|p| &***p)
+ self.absolute_base.as_ref().map(|p| &***p)
+ }
+}
+
+/// State for tracking what kinds of files ripgrep is interested in for a
+/// given directory.
+///
+/// This is computed over the entire set of files in a directory instead of
+/// trying to stat each file individually. If a file is present, it's only then
+/// that we stat it for more information, instead of relying on the stat to
+/// determine its existence.
+#[derive(Debug)]
+struct IgnoreFilesFound {
+ has_ignore: bool,
+ has_git_ignore: bool,
+ has_git_dir: bool,
+ has_jj_dir: bool,
+ custom_ignore_files: Vec,
+}
+
+#[derive(Clone, Copy)]
+pub(crate) struct IgnoreRef<'a> {
+ inner: &'a IgnoreInner,
+}
+
+impl IgnoreRef<'_> {
+ pub(crate) fn path(&self) -> &Path {
+ &self.inner.dir
+ }
+
+ pub(crate) fn is_absolute_parent(&self) -> bool {
+ self.inner.is_absolute_parent
}
}
/// An iterator over all parents of an ignore matcher, including itself.
-///
-/// The lifetime `'a` refers to the lifetime of the initial `Ignore` matcher.
-pub(crate) struct Parents<'a>(Option<&'a Ignore>);
+pub(crate) struct Parents<'a>(Option>);
impl<'a> Iterator for Parents<'a> {
- type Item = &'a Ignore;
+ type Item = IgnoreRef<'a>;
- fn next(&mut self) -> Option<&'a Ignore> {
+ fn next(&mut self) -> Option> {
match self.0.take() {
None => None,
Some(ig) => {
- self.0 = ig.0.parent.as_ref();
+ self.0 = ig
+ .inner
+ .parent
+ .as_deref()
+ .map(|inner| IgnoreRef { inner });
Some(ig)
}
}
@@ -645,33 +862,42 @@ impl IgnoreBuilder {
}
gi
} else {
- log::debug!("ignoring global gitignore file because CWD is not known");
+ log::debug!(
+ "ignoring global gitignore file because CWD is not known"
+ );
Gitignore::empty()
};
- Ignore(Arc::new(IgnoreInner {
- compiled: Arc::new(RwLock::new(HashMap::new())),
- dir: self.dir.clone(),
- overrides: self.overrides.clone(),
- types: self.types.clone(),
- parent: None,
- is_absolute_parent: true,
+ Ignore {
+ inner: Arc::new(IgnoreInner {
+ compiled: Arc::new(RwLock::new(HashMap::new())),
+ dir: self.dir.clone(),
+ overrides: self.overrides.clone(),
+ types: self.types.clone(),
+ parent: None,
+ is_absolute_parent: true,
+ global_gitignores_relative_to,
+ explicit_ignores: Arc::new(self.explicit_ignores.clone()),
+ custom_ignore_filenames: Arc::new(
+ self.custom_ignore_filenames.clone(),
+ ),
+ custom_ignore_matcher: Gitignore::empty(),
+ ignore_matcher: Gitignore::empty(),
+ git_global_matcher: Arc::new(git_global_matcher),
+ git_ignore_matcher: Gitignore::empty(),
+ git_exclude_matcher: Gitignore::empty(),
+ has_git: false,
+ opts: self.opts,
+ }),
absolute_base: None,
- global_gitignores_relative_to,
- explicit_ignores: Arc::new(self.explicit_ignores.clone()),
- custom_ignore_filenames: Arc::new(self.custom_ignore_filenames.clone()),
- custom_ignore_matcher: Gitignore::empty(),
- ignore_matcher: Gitignore::empty(),
- git_global_matcher: Arc::new(git_global_matcher),
- git_ignore_matcher: Gitignore::empty(),
- git_exclude_matcher: Gitignore::empty(),
- has_git: false,
- opts: self.opts,
- }))
+ }
}
/// Set the current directory used for matching global gitignores.
- pub(crate) fn current_dir(&mut self, cwd: impl Into) -> &mut IgnoreBuilder {
+ pub(crate) fn current_dir(
+ &mut self,
+ cwd: impl Into,
+ ) -> &mut IgnoreBuilder {
self.global_gitignores_relative_to = Some(cwd.into());
self
}
@@ -681,7 +907,10 @@ impl IgnoreBuilder {
/// By default, no override matcher is used.
///
/// This overrides any previous setting.
- pub(crate) fn overrides(&mut self, overrides: Override) -> &mut IgnoreBuilder {
+ pub(crate) fn overrides(
+ &mut self,
+ overrides: Override,
+ ) -> &mut IgnoreBuilder {
self.overrides = Arc::new(overrides);
self
}
@@ -712,8 +941,7 @@ impl IgnoreBuilder {
&mut self,
file_name: S,
) -> &mut IgnoreBuilder {
- self.custom_ignore_filenames
- .push(file_name.as_ref().to_os_string());
+ self.custom_ignore_filenames.push(file_name.as_ref().to_os_string());
self
}
@@ -725,6 +953,11 @@ impl IgnoreBuilder {
self
}
+ /// Whether ignoring hidden files is enabled or not.
+ pub(crate) fn is_hidden(&self) -> bool {
+ self.opts.hidden
+ }
+
/// Enables reading `.ignore` files.
///
/// `.ignore` files have the same semantics as `gitignore` files and are
@@ -795,7 +1028,10 @@ impl IgnoreBuilder {
/// Process ignore files case insensitively
///
/// This is disabled by default.
- pub(crate) fn ignore_case_insensitive(&mut self, yes: bool) -> &mut IgnoreBuilder {
+ pub(crate) fn ignore_case_insensitive(
+ &mut self,
+ yes: bool,
+ ) -> &mut IgnoreBuilder {
self.opts.ignore_case_insensitive = yes;
self
}
@@ -854,7 +1090,10 @@ pub(crate) fn create_gitignore>(
/// them when multiple repositories are searched.
///
/// Some I/O errors are ignored.
-fn resolve_git_commondir(dir: &Path, git_type: Option) -> Result> {
+fn resolve_git_commondir(
+ dir: &Path,
+ git_type: Option,
+) -> Result> {
let git_dir_path = || dir.join(".git");
let git_dir = git_dir_path();
if !git_type.map_or(false, |ft| ft.is_file()) {
@@ -899,15 +1138,20 @@ fn resolve_git_commondir(dir: &Path, git_type: Option) -> Result + ?Sized>(prefix: &'a P, path: &'a Path) -> &'a Path {
+fn strip_if_is_prefix<'a, P: AsRef + ?Sized>(
+ prefix: &'a P,
+ path: &'a Path,
+) -> &'a Path {
strip_prefix(prefix, path).map_or(path, |p| p)
}
#[cfg(test)]
mod tests {
- use std::{io::Write, path::Path};
+ use std::{io::Write, path::Path, sync::Arc};
- use crate::{Error, dir::IgnoreBuilder, gitignore::Gitignore, tests::TempDir};
+ use crate::{
+ Error, dir::IgnoreBuilder, gitignore::Gitignore, tests::TempDir,
+ };
fn wfile>(path: P, contents: &str) {
let mut file = std::fs::File::create(path).unwrap();
@@ -936,11 +1180,11 @@ mod tests {
let (gi, err) = Gitignore::new(td.path().join("not-an-ignore"));
assert!(err.is_none());
- let (ig, err) = IgnoreBuilder::new()
- .add_ignore(gi)
- .build()
- .add_child(td.path());
+ let (ig, err) =
+ IgnoreBuilder::new().add_ignore(gi).build().add_child(td.path());
assert!(err.is_none());
+ // CHANGED: Explicit ignores only apply to paths inside the directory
+ // of the ignore file, so we have to match against full paths.
assert!(ig.matched(td.path().join("foo"), false).is_ignore());
assert!(ig.matched(td.path().join("bar"), false).is_whitelist());
assert!(ig.matched(td.path().join("baz"), false).is_none());
@@ -1211,15 +1455,152 @@ mod tests {
let (ig2, err) = ig1.add_child("src");
assert!(err.is_none());
- // CHANGED: These test cases do not make sense for us as we never call the Ignore with
- // relative paths.
- assert!(ig1.matched("llvm", true).is_ignore());
- assert!(ig2.matched("llvm", true).is_ignore());
+ assert!(ig1.matched("llvm", true).is_none());
+ assert!(ig2.matched("llvm", true).is_none());
assert!(ig2.matched("src/llvm", true).is_none());
assert!(ig2.matched("foo", false).is_ignore());
assert!(ig2.matched("src/foo", false).is_ignore());
}
+ #[test]
+ fn absolute_parent_matchers_are_cached_across_roots() {
+ let td = tmpdir();
+ mkdirp(td.path().join(".git"));
+ mkdirp(td.path().join("src/build"));
+ mkdirp(td.path().join("tests/build"));
+ wfile(td.path().join(".gitignore"), "tests/**/build/\n");
+
+ let ig0 = IgnoreBuilder::new().build();
+ let (src_parents, err) = ig0.add_parents(td.path().join("src"));
+ assert!(err.is_none());
+ let (src, err) = src_parents.add_child(td.path().join("src"));
+ assert!(err.is_none());
+ let (tests_parents, err) = ig0.add_parents(td.path().join("tests"));
+ assert!(err.is_none());
+ let (tests, err) = tests_parents.add_child(td.path().join("tests"));
+ assert!(err.is_none());
+
+ assert!(Arc::ptr_eq(&src_parents.inner, &tests_parents.inner));
+ assert!(src.matched("build", true).is_none());
+ assert!(tests.matched("build", true).is_ignore());
+ }
+
+ /// Parent matchers are shared across search roots, but path rewriting for
+ /// absolute parents must use each root's own base path. Otherwise a rule
+ /// like `src/invalid` is matched against the wrong absolute path when
+ /// `src` is searched before a sibling root (e.g. `tests`).
+ ///
+ /// Paths passed to `matched` use the same relative layout as `Walk` when
+ /// roots are given as relative directory names.
+ ///
+ /// Regression for: https://github.com/BurntSushi/ripgrep/issues/3376
+ /// and https://github.com/BurntSushi/ripgrep/issues/3419
+ #[test]
+ fn multi_root_gitignore_order_independent() {
+ let td = tmpdir();
+ let cwd = std::env::current_dir().unwrap();
+ // Use paths relative to CWD like the CLI walk does for `rg pat src tests`.
+ let root = td.path().strip_prefix(&cwd).unwrap_or(td.path());
+ let src_root = root.join("src");
+ let tests_root = root.join("tests");
+
+ mkdirp(td.path().join(".git"));
+ mkdirp(td.path().join("src"));
+ mkdirp(td.path().join("tests"));
+ wfile(td.path().join(".gitignore"), "src/invalid\n");
+ wfile(td.path().join("src/invalid"), "x");
+ wfile(td.path().join("src/valid"), "x");
+ wfile(td.path().join("tests/valid"), "x");
+
+ let ig0 = IgnoreBuilder::new().build();
+
+ // Historically buggy order: search `src` first, then `tests`.
+ let (src_parents, err) = ig0.add_parents(&src_root);
+ assert!(err.is_none());
+ let (src, err) = src_parents.add_child(&src_root);
+ assert!(err.is_none());
+ let (tests_parents, err) = ig0.add_parents(&tests_root);
+ assert!(err.is_none());
+ let (tests, err) = tests_parents.add_child(&tests_root);
+ assert!(err.is_none());
+
+ assert!(Arc::ptr_eq(&src_parents.inner, &tests_parents.inner));
+ // Each root must carry its own absolute_base even though inners are shared.
+ assert_ne!(
+ src.absolute_base.as_ref().unwrap().as_path(),
+ tests.absolute_base.as_ref().unwrap().as_path()
+ );
+ assert!(
+ src.matched(src_root.join("invalid"), false).is_ignore(),
+ "parent .gitignore must apply for the src root even when \
+ another root was prepared in the same process"
+ );
+ assert!(src.matched(src_root.join("valid"), false).is_none());
+ assert!(tests.matched(tests_root.join("valid"), false).is_none());
+
+ // Reverse order should behave the same way.
+ let ig0 = IgnoreBuilder::new().build();
+ let (tests_parents, err) = ig0.add_parents(&tests_root);
+ assert!(err.is_none());
+ let (tests, err) = tests_parents.add_child(&tests_root);
+ assert!(err.is_none());
+ let (src_parents, err) = ig0.add_parents(&src_root);
+ assert!(err.is_none());
+ let (src, err) = src_parents.add_child(&src_root);
+ assert!(err.is_none());
+
+ assert!(src.matched(src_root.join("invalid"), false).is_ignore());
+ assert!(src.matched(src_root.join("valid"), false).is_none());
+ assert!(tests.matched(tests_root.join("valid"), false).is_none());
+ }
+
+ /// Same multi-root / order issue for non-git ignore files (e.g. `.rgignore`
+ /// via custom ignore names).
+ ///
+ /// Regression for: https://github.com/BurntSushi/ripgrep/issues/3320
+ #[test]
+ fn multi_root_custom_ignore_order_independent() {
+ let td = tmpdir();
+ let cwd = std::env::current_dir().unwrap();
+ let root = td.path().strip_prefix(&cwd).unwrap_or(td.path());
+ let alpha_root = root.join("alpha");
+ let beta_root = root.join("beta");
+
+ mkdirp(td.path().join("alpha"));
+ mkdirp(td.path().join("beta"));
+ wfile(td.path().join(".rgignore"), "beta/**/*.svg\n");
+ wfile(td.path().join("alpha/a.txt"), "x");
+ wfile(td.path().join("beta/x.svg"), "x");
+
+ let ig0 = IgnoreBuilder::new()
+ .add_custom_ignore_filename(".rgignore")
+ .ignore(false)
+ .git_ignore(false)
+ .git_global(false)
+ .git_exclude(false)
+ .build();
+
+ let (alpha_parents, err) = ig0.add_parents(&alpha_root);
+ assert!(err.is_none());
+ let (alpha, err) = alpha_parents.add_child(&alpha_root);
+ assert!(err.is_none());
+ let (beta_parents, err) = ig0.add_parents(&beta_root);
+ assert!(err.is_none());
+ let (beta, err) = beta_parents.add_child(&beta_root);
+ assert!(err.is_none());
+
+ assert_ne!(
+ alpha.absolute_base.as_ref().unwrap().as_path(),
+ beta.absolute_base.as_ref().unwrap().as_path()
+ );
+ assert!(alpha.matched(alpha_root.join("a.txt"), false).is_none());
+ assert!(
+ beta.matched(beta_root.join("x.svg"), false).is_ignore(),
+ "parent .rgignore must apply for the beta root regardless of \
+ which root was set up first"
+ );
+ }
+
#[test]
fn git_info_exclude_in_linked_worktree() {
let td = tmpdir();
@@ -1227,16 +1608,14 @@ mod tests {
mkdirp(git_dir.join("info"));
wfile(git_dir.join("info/exclude"), "ignore_me");
mkdirp(git_dir.join("worktrees/linked-worktree"));
- let commondir_path = || git_dir.join("worktrees/linked-worktree/commondir");
+ let commondir_path =
+ || git_dir.join("worktrees/linked-worktree/commondir");
mkdirp(td.path().join("linked-worktree"));
let worktree_git_dir_abs = format!(
"gitdir: {}",
git_dir.join("worktrees/linked-worktree").to_str().unwrap(),
);
- wfile(
- td.path().join("linked-worktree/.git"),
- &worktree_git_dir_abs,
- );
+ wfile(td.path().join("linked-worktree/.git"), &worktree_git_dir_abs);
// relative commondir
wfile(commondir_path(), "../..");
diff --git a/crates/ignore/src/gitignore.rs b/crates/ignore/src/gitignore.rs
index f822d8390..8824139b5 100644
--- a/crates/ignore/src/gitignore.rs
+++ b/crates/ignore/src/gitignore.rs
@@ -102,7 +102,9 @@ impl Gitignore {
///
/// Note that I/O errors are ignored. For more granular control over
/// errors, use `GitignoreBuilder`.
- pub fn new>(gitignore_path: P) -> (Gitignore, Option) {
+ pub fn new>(
+ gitignore_path: P,
+ ) -> (Gitignore, Option) {
let path = gitignore_path.as_ref();
let parent = path.parent().unwrap_or(Path::new("/"));
let mut builder = GitignoreBuilder::new(parent);
@@ -123,6 +125,17 @@ impl Gitignore {
/// The global config file path is specified by git's `core.excludesFile`
/// config option.
///
+ /// # Behavior
+ ///
+ /// This routine does its best to discover any global git exclude files.
+ /// This will try to parse out the `excludesFile` config option in your
+ /// global git configuration, if necessary.
+ ///
+ /// The specific things this routine tries (which are subject to change
+ /// based on how git behaves) are:
+ ///
+ ///
+ ///
/// Git's config file location is `$HOME/.gitconfig`. If `$HOME/.gitconfig`
/// does not exist or does not specify `core.excludesFile`, then
/// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not
@@ -145,7 +158,8 @@ impl Gitignore {
num_ignores: 0,
num_whitelists: 0,
matches: None,
- // CHANGED: Add a flag to have Gitignore rules that apply only to files.
+ // CHANGED: Add a flag to have Gitignore rules that apply only to
+ // files.
only_on_files: false,
}
}
@@ -190,7 +204,11 @@ impl Gitignore {
/// determined by a common suffix of the directory containing this
/// gitignore) is stripped. If there is no common suffix/prefix overlap,
/// then `path` is assumed to be relative to this matcher.
- pub fn matched>(&self, path: P, is_dir: bool) -> Match<&Glob> {
+ pub fn matched>(
+ &self,
+ path: P,
+ is_dir: bool,
+ ) -> Match<&Glob> {
if self.is_empty() {
return Match::None;
}
@@ -243,11 +261,16 @@ impl Gitignore {
}
/// Like matched, but takes a path that has already been stripped.
- fn matched_stripped>(&self, path: P, is_dir: bool) -> Match<&Glob> {
+ fn matched_stripped>(
+ &self,
+ path: P,
+ is_dir: bool,
+ ) -> Match<&Glob> {
if self.is_empty() {
return Match::None;
}
- // CHANGED: Rules marked as only_on_files can not match against directories.
+ // CHANGED: Rules marked as only_on_files can not match against
+ // directories.
if self.only_on_files && is_dir {
return Match::None;
}
@@ -270,7 +293,10 @@ impl Gitignore {
/// Strips the given path such that it's suitable for matching with this
/// gitignore matcher.
- fn strip<'a, P: 'a + AsRef + ?Sized>(&'a self, path: &'a P) -> &'a Path {
+ fn strip<'a, P: 'a + AsRef + ?Sized>(
+ &'a self,
+ path: &'a P,
+ ) -> &'a Path {
let mut path = path.as_ref();
// A leading ./ is completely superfluous. We also strip it from
// our gitignore root path, so we need to strip it from our candidate
@@ -326,7 +352,8 @@ impl GitignoreBuilder {
globs: vec![],
case_insensitive: false,
allow_unclosed_class: true,
- // CHANGED: Add a flag to have Gitignore rules that apply only to files.
+ // CHANGED: Add a flag to have Gitignore rules that apply only to
+ // files.
only_on_files: false,
}
}
@@ -337,18 +364,21 @@ impl GitignoreBuilder {
pub fn build(&self) -> Result {
let nignore = self.globs.iter().filter(|g| !g.is_whitelist()).count();
let nwhite = self.globs.iter().filter(|g| g.is_whitelist()).count();
- let set = self.builder.build().map_err(|err| Error::Glob {
- glob: None,
- err: err.to_string(),
- })?;
+ let set = self
+ .builder
+ .build()
+ .map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
Ok(Gitignore {
set,
root: self.root.clone(),
globs: self.globs.clone(),
num_ignores: nignore as u64,
num_whitelists: nwhite as u64,
- matches: Some(Arc::new(Pool::new(|| vec![]))),
- // CHANGED: Add a flag to have Gitignore rules that apply only to files.
+ matches: Some(Arc::new(
+ Pool::with_available_parallelism_capacity(|| vec![]),
+ )),
+ // CHANGED: Add a flag to have Gitignore rules that apply only to
+ // files.
only_on_files: self.only_on_files,
})
}
@@ -411,11 +441,8 @@ impl GitignoreBuilder {
// Match Git's handling of .gitignore files that begin with the Unicode BOM
const UTF8_BOM: &str = "\u{feff}";
- let line = if i == 0 {
- line.trim_start_matches(UTF8_BOM)
- } else {
- &line
- };
+ let line =
+ if i == 0 { line.trim_start_matches(UTF8_BOM) } else { &line };
if let Err(err) = self.add_line(Some(path.to_path_buf()), &line) {
errs.push(err.tagged(path, lineno));
@@ -537,7 +564,10 @@ impl GitignoreBuilder {
/// affected.
///
/// This is disabled by default.
- pub fn case_insensitive(&mut self, yes: bool) -> Result<&mut GitignoreBuilder, Error> {
+ pub fn case_insensitive(
+ &mut self,
+ yes: bool,
+ ) -> Result<&mut GitignoreBuilder, Error> {
// TODO: This should not return a `Result`. Fix this in the next semver
// release.
self.case_insensitive = yes;
@@ -556,7 +586,10 @@ impl GitignoreBuilder {
/// modes since the glob parser becomes more permissive. You might want to
/// enable this when compatibility (e.g., with POSIX glob implementations)
/// is more important than good error messages.
- pub fn allow_unclosed_class(&mut self, yes: bool) -> &mut GitignoreBuilder {
+ pub fn allow_unclosed_class(
+ &mut self,
+ yes: bool,
+ ) -> &mut GitignoreBuilder {
self.allow_unclosed_class = yes;
self
}
@@ -576,32 +609,56 @@ impl GitignoreBuilder {
///
/// Note that the file path returned may not exist.
pub fn gitconfig_excludes_path() -> Option {
- // git supports $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config. Notably,
- // both can be active at the same time, where $HOME/.gitconfig takes
- // precedent. So if $HOME/.gitconfig defines a `core.excludesFile`, then
- // we're done.
- match gitconfig_home_contents().and_then(|x| parse_excludes_file(&x)) {
- Some(path) => return Some(path),
- None => {}
+ // When GIT_CONFIG_GLOBAL is set, it replaces both $HOME/.gitconfig and
+ // $XDG_CONFIG_HOME/git/config (per git 2.32+). Otherwise, git supports
+ // $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config simultaneously, where
+ // $HOME/.gitconfig takes precedent.
+ gitconfig_global_env_contents()
+ .and_then(|x| parse_excludes_file(&x))
+ .or_else(|| {
+ gitconfig_home_contents().and_then(|x| parse_excludes_file(&x))
+ })
+ .or_else(|| {
+ gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x))
+ })
+ // System-level config has the lowest priority for core.excludesFile.
+ // GIT_CONFIG_SYSTEM overrides the default /etc/gitconfig path.
+ .or_else(|| {
+ gitconfig_system_contents().and_then(|x| parse_excludes_file(&x))
+ })
+ .or_else(excludes_file_default)
+}
+
+/// Returns the file contents of git's global config file from the path
+/// specified by the `GIT_CONFIG_GLOBAL` environment variable.
+fn gitconfig_global_env_contents() -> Option> {
+ let path = std::env::var_os("GIT_CONFIG_GLOBAL").map(PathBuf::from)?;
+ if path.as_os_str().is_empty() {
+ return None;
}
- match gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x)) {
- Some(path) => return Some(path),
- None => {}
- }
- excludes_file_default()
+ let mut file = BufReader::new(File::open(path).ok()?);
+ let mut contents = vec![];
+ file.read_to_end(&mut contents).ok().map(|_| contents)
+}
+
+/// Returns the file contents of git's system-level config file.
+///
+/// Checks `GIT_CONFIG_SYSTEM` first, then falls back to `/etc/gitconfig`.
+fn gitconfig_system_contents() -> Option> {
+ let path = std::env::var_os("GIT_CONFIG_SYSTEM")
+ .map(PathBuf::from)
+ .filter(|x| !x.as_os_str().is_empty())
+ .unwrap_or_else(|| PathBuf::from("/etc/gitconfig"));
+ let mut file = BufReader::new(File::open(path).ok()?);
+ let mut contents = vec![];
+ file.read_to_end(&mut contents).ok().map(|_| contents)
}
/// Returns the file contents of git's global config file, if one exists, in
/// the user's home directory.
fn gitconfig_home_contents() -> Option> {
- let home = match home_dir() {
- None => return None,
- Some(home) => home,
- };
- let mut file = match File::open(home.join(".gitconfig")) {
- Err(_) => return None,
- Ok(file) => BufReader::new(file),
- };
+ let home = home_dir()?;
+ let mut file = BufReader::new(File::open(home.join(".gitconfig")).ok()?);
let mut contents = vec![];
file.read_to_end(&mut contents).ok().map(|_| contents)
}
@@ -610,19 +667,11 @@ fn gitconfig_home_contents() -> Option> {
/// the user's XDG_CONFIG_HOME directory.
fn gitconfig_xdg_contents() -> Option> {
let path = std::env::var_os("XDG_CONFIG_HOME")
- .and_then(|x| {
- if x.is_empty() {
- None
- } else {
- Some(PathBuf::from(x))
- }
- })
+ .map(PathBuf::from)
+ .filter(|x| !x.as_os_str().is_empty())
.or_else(|| home_dir().map(|p| p.join(".config")))
- .map(|x| x.join("git/config"));
- let mut file = match path.and_then(|p| File::open(p).ok()) {
- None => return None,
- Some(file) => BufReader::new(file),
- };
+ .map(|x| x.join("git/config"))?;
+ let mut file = BufReader::new(File::open(path).ok()?);
let mut contents = vec![];
file.read_to_end(&mut contents).ok().map(|_| contents)
}
@@ -632,13 +681,8 @@ fn gitconfig_xdg_contents() -> Option> {
/// Specifically, this respects XDG_CONFIG_HOME.
fn excludes_file_default() -> Option {
std::env::var_os("XDG_CONFIG_HOME")
- .and_then(|x| {
- if x.is_empty() {
- None
- } else {
- Some(PathBuf::from(x))
- }
- })
+ .map(PathBuf::from)
+ .filter(|x| !x.as_os_str().is_empty())
.or_else(|| home_dir().map(|p| p.join(".config")))
.map(|x| x.join("git/ignore"))
}
@@ -667,9 +711,7 @@ fn parse_excludes_file(data: &[u8]) -> Option {
re.captures(data, &mut caps);
let span = caps.get_group(1)?;
let candidate = &data[span];
- std::str::from_utf8(candidate)
- .ok()
- .map(|s| PathBuf::from(expand_tilde(s)))
+ std::str::from_utf8(candidate).ok().map(|s| PathBuf::from(expand_tilde(s)))
}
/// Expands ~ in file paths to the value of $HOME.
@@ -831,7 +873,10 @@ mod tests {
fn parse_excludes_file4() {
let data = bytes("[core]\nexcludesFile = \"~/foo/bar\"");
let got = super::parse_excludes_file(&data);
- assert_eq!(path_string(got.unwrap()), super::expand_tilde("~/foo/bar"));
+ assert_eq!(
+ path_string(got.unwrap()),
+ super::expand_tilde("~/foo/bar")
+ );
}
#[test]
diff --git a/crates/ignore/src/incremental.rs b/crates/ignore/src/incremental.rs
new file mode 100644
index 000000000..5030b0d00
--- /dev/null
+++ b/crates/ignore/src/incremental.rs
@@ -0,0 +1,1286 @@
+use std::{
+ collections::HashMap,
+ path::{Path, PathBuf},
+ sync::OnceLock,
+};
+
+use crate::{
+ Error, Match, PartialErrorBuilder, dir::Ignore, pathutil::is_hidden_path,
+};
+
+/// A cached matcher for checking paths against hierarchical ignore files.
+///
+/// An `IncrementalIgnore` is built from a [`crate::WalkBuilder`]. Unlike a
+/// recursive walk, it can check individual paths while still respecting the
+/// ignore files in every relevant parent directory. Matchers for directories
+/// are compiled on first use and then retained for later queries.
+/// Each matcher corresponds to exactly one root configured on the builder,
+/// and paths passed to it are interpreted relative to that root.
+/// A matcher for the special `-` root representing standard input is inert
+/// and always returns a non-match.
+///
+/// The matcher checks path-based filters in the same precedence order as
+/// a traversal. This includes glob overrides, `.ignore`, `.gitignore`,
+/// `.git/info/exclude`, global and explicitly added ignore files, custom
+/// ignore file names and file type selections. It does not apply filters that
+/// require a directory entry or other traversal state, such as custom entry
+/// predicates. Hidden-file detection, minimum and maximum depth limits and the
+/// maximum file size are applied.
+///
+/// A matcher is a snapshot at directory granularity. Once the ignore files in
+/// a directory have been loaded, edits to those files are not observed. Build
+/// a new matcher to reload them.
+///
+/// # Warning
+///
+/// The incremental path checking here necessarily needs to do a lot more work
+/// per path matched. Callers should _not_ use this to run directory traversal.
+/// This is intended to avoid the work of re-traversing an entire directory
+/// tree when only a few changes are detected. (For example, in response to
+/// file additions or deletions.)
+///
+/// # Example
+///
+/// ```rust,no_run
+/// use ignore::WalkBuilder;
+///
+/// let mut builder = WalkBuilder::new(".");
+/// builder.add_custom_ignore_filename(".rgignore");
+/// let mut matchers = builder.build_matchers();
+/// let matcher = &mut matchers[0];
+///
+/// if matcher.matched("src/generated.rs", false).is_ignore() {
+/// println!("ignored");
+/// }
+/// ```
+#[derive(Clone, Debug)]
+pub struct IncrementalIgnore {
+ /// The root exactly as it was given to `WalkBuilder`.
+ root: PathBuf,
+ /// The normalized root used only by the opt-in normalization routine.
+ normalized_root: OnceLock