diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 63f79fa10..42645aa49 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/tailwindlabs/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/tailwindcss/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 aa0793aa3..ee9d3f9ab 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/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md
+https://github.com/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md
-->
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5ae59e80f..e369c64fc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,18 +8,12 @@ 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
@@ -27,10 +21,8 @@ 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-15
+ os: macos-14
# Exclude windows and macos from being built on feature branches
run-all:
@@ -49,21 +41,21 @@ jobs:
name: ${{ matrix.runner.name }}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- persist-credentials: false
- - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
- - name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
with:
- node-version: ${{ env.NODE_VERSION }}
+ node-version: ${{ matrix.node-version }}
+ cache: 'pnpm'
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@v4
with:
path: |
+ ~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
@@ -72,7 +64,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@v4
with:
path: |
./crates/node/*.node
@@ -90,7 +82,7 @@ jobs:
run: rustup target add wasm32-wasip1-threads
- name: Install dependencies
- run: pnpm install --frozen-lockfile
+ run: pnpm install
- name: Build
run: pnpm run build
@@ -112,13 +104,9 @@ 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
- uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
+ if: failure() && github.ref == 'refs/heads/main'
+ uses: discord-actions/message@v2
with:
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
- message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
+ message: 'The [most recent build](<${{ 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 457c38424..b84ba2140 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -8,18 +8,13 @@ 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
@@ -55,25 +50,25 @@ jobs:
name: ${{ matrix.runner.name }} / ${{ matrix.integration }}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- persist-credentials: false
- - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
- run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- - name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
with:
- node-version: ${{ env.NODE_VERSION }}
+ node-version: ${{ matrix.node-version }}
+ cache: 'pnpm'
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@v4
with:
path: |
+ ~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
@@ -82,7 +77,7 @@ jobs:
# Cache the `oxide` Rust build
- name: Cache oxide build
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@v4
with:
path: |
./crates/node/*.node
@@ -108,18 +103,24 @@ jobs:
CARGO_PROFILE_RELEASE_LTO: 'off'
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link'
- - name: Test ${{ matrix.integration }}
- run: pnpm run test:integrations ./integrations/${{ matrix.integration }}
+ - 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
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
- uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
+ if: failure() && github.ref == 'refs/heads/main'
+ uses: discord-actions/message@v2
with:
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
- message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
+ message: 'The [most recent build](<${{ 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 24e829afd..be1050b03 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -2,29 +2,18 @@ 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: 24
- PNPM_VERSION: '11.9.0'
+ NODE_VERSION: 20
OXIDE_LOCATION: ./crates/node
permissions:
contents: read
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
jobs:
build:
strategy:
@@ -54,40 +43,44 @@ jobs:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
strip: strip
- build-flags: --use-napi-cross
+ container:
+ image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- strip: aarch64-linux-gnu-strip
- build-flags: --use-napi-cross
+ 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: arm-linux-gnueabihf-strip
- build-flags: --use-napi-cross
+ 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-zig: true
- build-flags: -x
+ 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
- build-flags: -x
+ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- persist-credentials: false
- - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- with:
- version: ${{ env.PNPM_VERSION }}
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- package-manager-cache: false
+ cache: 'pnpm'
- name: Install gcc-arm-linux-gnueabihf
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
@@ -95,56 +88,62 @@ jobs:
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
- - name: Install binutils-aarch64-linux-gnu
- if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
+ # 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: |
- 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
+ rustup default stable
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
- name: Install dependencies
- run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
+ run: pnpm install --ignore-scripts --filter=!./playgrounds/*
- name: Build release
- run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
+ 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 || 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"
+ if: ${{ matrix.strip }}
+ run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
- name: Upload artifacts
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.target }}
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -154,11 +153,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- persist-credentials: false
+ - uses: actions/checkout@v4
- name: Build FreeBSD
- uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
+ uses: cross-platform-actions/action@v0.25.0
env:
DEBUG: napi:*
RUSTUP_HOME: /usr/local/rustup
@@ -174,12 +171,12 @@ jobs:
shell: bash
run: |
sudo pkg install -y -f curl node libnghttp2 npm
- sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
+ 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"
- pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
+ pnpm install --ignore-scripts --filter=!./playgrounds/* || true
echo "~~~~ rustc --version ~~~~"
rustc --version
echo "~~~~ node -v ~~~~"
@@ -190,7 +187,7 @@ jobs:
strip -x ${{ env.OXIDE_LOCATION }}/*.node
ls -la ${{ env.OXIDE_LOCATION }}
- name: Upload artifacts
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@v4
with:
name: bindings-x86_64-unknown-freebsd
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -201,17 +198,18 @@ jobs:
name: Build and release Tailwind CSS
permissions:
- contents: write # Required for creating releases
+ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: actions/checkout@v4
with:
fetch-depth: 20
- persist-credentials: false
- run: git fetch --tags -f
@@ -220,25 +218,51 @@ jobs:
run: |
echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- with:
- version: ${{ env.PNPM_VERSION }}
+ - uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ uses: actions/setup-node@v4
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') }}
+
+ # 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 --frozen-lockfile
+ run: pnpm --filter=!./playgrounds/* install
- name: Download artifacts
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ uses: actions/download-artifact@v4
with:
path: ${{ env.OXIDE_LOCATION }}
@@ -277,31 +301,30 @@ jobs:
echo "EOF" >> $GITHUB_ENV
- name: Upload standalone artifacts
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@v4
with:
name: tailwindcss-standalone
path: packages/@tailwindcss-standalone/dist/
- name: Upload npm package tarballs
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@v4
with:
name: npm-package-tarballs
path: dist/*.tgz
- name: Prepare GitHub Release
- 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
+ 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
diff --git a/.github/workflows/release-insiders.yml b/.github/workflows/release-insiders.yml
new file mode 100644
index 000000000..53478a227
--- /dev/null
+++ b/.github/workflows/release-insiders.yml
@@ -0,0 +1,318 @@
+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 2c957d3af..8b27773b3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,34 +1,18 @@
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: 24
- PNPM_VERSION: '11.9.0'
+ NODE_VERSION: 20
OXIDE_LOCATION: ./crates/node
-concurrency:
- group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
- cancel-in-progress: true
-
jobs:
build:
strategy:
@@ -58,40 +42,44 @@ jobs:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
strip: strip
- build-flags: --use-napi-cross
+ container:
+ image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- strip: aarch64-linux-gnu-strip
- build-flags: --use-napi-cross
+ 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: arm-linux-gnueabihf-strip
- build-flags: --use-napi-cross
+ 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-zig: true
- build-flags: -x
+ 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
- build-flags: -x
+ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- persist-credentials: false
- - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- with:
- version: ${{ env.PNPM_VERSION }}
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- package-manager-cache: false
+ cache: 'pnpm'
- name: Install gcc-arm-linux-gnueabihf
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
@@ -99,56 +87,62 @@ jobs:
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
- - name: Install binutils-aarch64-linux-gnu
- if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
+ # 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: |
- 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
+ rustup default stable
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
- name: Install dependencies
- run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
+ run: pnpm install --ignore-scripts --filter=!./playgrounds/*
- name: Build release
- run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
+ 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 || 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"
+ if: ${{ matrix.strip }}
+ run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
- name: Upload artifacts
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.target }}
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -158,11 +152,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- persist-credentials: false
+ - uses: actions/checkout@v4
- name: Build FreeBSD
- uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
+ uses: cross-platform-actions/action@v0.25.0
env:
DEBUG: napi:*
RUSTUP_HOME: /usr/local/rustup
@@ -178,7 +170,7 @@ jobs:
shell: bash
run: |
sudo pkg install -y -f curl node libnghttp2 npm
- sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
+ 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
@@ -189,12 +181,12 @@ jobs:
node -v
echo "~~~~ pnpm --version ~~~~"
pnpm --version
- pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
+ 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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@v4
with:
name: bindings-x86_64-unknown-freebsd
path: ${{ env.OXIDE_LOCATION }}/*.node
@@ -202,10 +194,10 @@ jobs:
release:
runs-on: macos-14
timeout-minutes: 15
- name: Build and publish Tailwind CSS
+ name: Build and release Tailwind CSS
permissions:
- contents: read
+ 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
@@ -214,51 +206,55 @@ jobs:
- build-freebsd
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: actions/checkout@v4
with:
fetch-depth: 20
- persist-credentials: false
- - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- with:
- version: ${{ env.PNPM_VERSION }}
+ - uses: pnpm/action-setup@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+ uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
+ cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- package-manager-cache: false
- # 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)
+ # 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') }}
- 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
+ # 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 --frozen-lockfile
+ run: pnpm --filter=!./playgrounds/* install
- name: Download artifacts
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
+ uses: actions/download-artifact@v4
with:
path: ${{ env.OXIDE_LOCATION }}
@@ -278,19 +274,10 @@ 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: ${{ env.FEATURES_ENV }}
+ FEATURES_ENV: stable
- name: Run pre-publish optimizations scripts
run: node ./scripts/pre-publish-optimizations.mjs
@@ -298,20 +285,22 @@ jobs:
- name: Lock pre-release versions
run: node ./scripts/lock-pre-release-versions.mjs
- - name: Upload npm package tarballs
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
- with:
- name: npm-package-tarballs
- path: dist/*.tgz
+ - 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: Publish
run: |
- pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${RELEASE_CHANNEL} --no-git-checks
+ 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 ${RELEASE_CHANNEL} --no-git-checks --config.node-linker=hoisted; popd;
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ if: env.RELEASE_CHANNEL == 'latest'
+ uses: actions/github-script@v7
with:
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
script: |
@@ -321,38 +310,3 @@ 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
new file mode 100644
index 000000000..ded82e2f6
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+auto-install-peers = true
diff --git a/.prettierignore b/.prettierignore
index 4f50b932f..3de4530dd 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,6 +4,5 @@ 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 e5e6f6f89..6f0181e87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,262 +7,14 @@ 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 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
-
-- 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
-
### Fixed
- Substitute `@variant` inside legacy JS APIs ([#19263](https://github.com/tailwindlabs/tailwindcss/pull/19263))
-- Prevent occasional crash on Windows when loaded into a worker thread ([#19242](https://github.com/tailwindlabs/tailwindcss/pull/19242))
+- Handle utilities with `1` keys in a JS config ([#19254](https://github.com/tailwindlabs/tailwindcss/pull/19254))
+
+### Added
+
+- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
## [4.1.16] - 2025-10-23
@@ -313,16 +65,6 @@ 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
@@ -1655,6 +1397,16 @@ 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
@@ -4123,23 +3875,10 @@ No release notes
- Everything!
-[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
+[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.16...HEAD
[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
@@ -4197,6 +3936,7 @@ 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 73fd9e995..162cf37dd 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.18",
+ "regex-automata 0.4.8",
"serde",
]
@@ -59,22 +59,11 @@ dependencies = [
"syn",
]
-[[package]]
-name = "console"
-version = "0.16.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
-dependencies = [
- "encode_unicode",
- "libc",
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "convert_case"
-version = "0.11.0"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
+checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
"unicode-segmentation",
]
@@ -115,9 +104,19 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "ctor"
-version = "1.0.12"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f"
+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"
[[package]]
name = "diff"
@@ -125,6 +124,21 @@ 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"
@@ -137,12 +151,6 @@ 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"
@@ -168,104 +176,16 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
-[[package]]
-name = "futures"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-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"
+version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
+checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305"
dependencies = [
"aho-corasick",
"bstr",
"log",
- "regex-automata 0.4.18",
+ "regex-automata 0.4.8",
"regex-syntax 0.8.5",
]
@@ -290,7 +210,7 @@ dependencies = [
"globset",
"log",
"memchr",
- "regex-automata 0.4.18",
+ "regex-automata 0.4.8",
"same-file",
"walkdir",
"winapi-util",
@@ -298,7 +218,7 @@ dependencies = [
[[package]]
name = "ignore"
-version = "0.4.33"
+version = "0.4.24"
dependencies = [
"bstr",
"crossbeam-channel",
@@ -307,24 +227,12 @@ dependencies = [
"globset",
"log",
"memchr",
- "regex-automata 0.4.18",
+ "regex-automata 0.4.8",
"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"
@@ -348,12 +256,12 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libloading"
-version = "0.9.0"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
+checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
- "windows-link",
+ "windows-targets",
]
[[package]]
@@ -391,13 +299,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "napi"
-version = "3.11.0"
+version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de33522036981030a75c231829566bc63414e08101a6f5ff4ac6cef19c8e0941"
+checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7"
dependencies = [
"bitflags",
"ctor",
- "futures",
"napi-build",
"napi-sys",
"nohash-hasher",
@@ -406,15 +313,15 @@ dependencies = [
[[package]]
name = "napi-build"
-version = "2.3.2"
+version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
+checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14"
[[package]]
name = "napi-derive"
-version = "3.6.0"
+version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a49c513341a61a16a10af6efcce46b30d0822ba2d4fb197d24d33dfc199c78d5"
+checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0"
dependencies = [
"convert_case",
"ctor",
@@ -426,9 +333,9 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
-version = "6.1.1"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd"
+checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a"
dependencies = [
"convert_case",
"proc-macro2",
@@ -439,9 +346,9 @@ dependencies = [
[[package]]
name = "napi-sys"
-version = "3.3.0"
+version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85fbf1fa9f1babfe396d74bbbf52b3643770243e8f5b0b46715d4caf7f0dfc9a"
+checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d"
dependencies = [
"libloading",
]
@@ -474,9 +381,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.21.4"
+version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "overload"
@@ -511,18 +418,18 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.45"
+version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
-version = "1.12.0"
+version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
@@ -530,9 +437,9 @@ dependencies = [
[[package]]
name = "rayon-core"
-version = "1.13.0"
+version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
@@ -546,7 +453,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.4.18",
+ "regex-automata 0.4.8",
"regex-syntax 0.8.5",
]
@@ -561,9 +468,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.18"
+version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"memchr",
@@ -631,18 +538,6 @@ 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"
@@ -651,9 +546,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "syn"
-version = "2.0.87"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
+checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
dependencies = [
"proc-macro2",
"quote",
@@ -681,8 +576,7 @@ dependencies = [
"dunce",
"fast-glob",
"globwalk",
- "ignore 0.4.33",
- "insta",
+ "ignore 0.4.24",
"log",
"pretty_assertions",
"rayon",
@@ -844,12 +738,6 @@ 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"
@@ -868,15 +756,6 @@ 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 5f532607d..7d21bd883 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/tailwindlabs/tailwindcss/discussions)
+[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
## Contributing
-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**.
+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**.
diff --git a/crates/ignore/Cargo.toml b/crates/ignore/Cargo.toml
index d0af38ba9..bd0352d15 100644
--- a/crates/ignore/Cargo.toml
+++ b/crates/ignore/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "ignore"
-version = "0.4.33" #:version
+version = "0.4.24" #:version
authors = ["Andrew Gallant "]
description = """
A fast library for efficiently matching ignore files such as `.gitignore`
@@ -12,10 +12,7 @@ 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"
@@ -23,17 +20,15 @@ bench = false
[dependencies]
crossbeam-deque = "0.8.3"
-# CHANGED: Use the published globset crate instead of a path dependency.
-globset = "0.4.20"
+globset = "0.4.17"
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.18"
+version = "0.4.0"
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 c61d0515e..9c627dc3e 100644
--- a/crates/ignore/examples/walk.rs
+++ b/crates/ignore/examples/walk.rs
@@ -18,7 +18,9 @@ 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 6b5bba0f0..4e060b76a 100644
--- a/crates/ignore/src/default_types.rs
+++ b/crates/ignore/src/default_types.rs
@@ -47,7 +47,6 @@ 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",
@@ -110,7 +109,6 @@ 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"]),
@@ -187,7 +185,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
(&["mint"], &["*.mint"]),
(&["mk"], &["mkfile"]),
(&["ml"], &["*.ml"]),
- (&["mojo"], &["*.mojo"]),
(&["motoko"], &["*.mo"]),
(&["msbuild"], &[
"*.csproj", "*.fsproj", "*.vcxproj", "*.proj", "*.props", "*.targets",
@@ -209,12 +206,11 @@ 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"]),
- (&["proto", "protobuf"], &["*.proto"]),
+ (&["protobuf"], &["*.proto"]),
(&["ps"], &["*.cdxml", "*.ps1", "*.ps1xml", "*.psd1", "*.psm1"]),
(&["puppet"], &["*.epp", "*.erb", "*.pp", "*.rb"]),
(&["purs"], &["*.purs"]),
@@ -235,7 +231,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
(&["red"], &["*.r", "*.red", "*.reds"]),
(&["rescript"], &["*.res", "*.resi"]),
(&["robot"], &["*.robot"]),
- (&["rocq"], &["*.v"]),
(&["rst"], &["*.rst"]),
(&["ruby"], &[
// Idiomatic files
@@ -279,7 +274,6 @@ 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"]),
@@ -365,14 +359,4 @@ 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 6bee724c8..11b58f8ca 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::{self, File, FileType},
+ fs::{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_entry, strip_prefix},
+ pathutil::{is_hidden, strip_prefix},
types::{self, Types},
walk::DirEntry,
{Error, Match, PartialErrorBuilder},
@@ -91,25 +91,7 @@ struct IgnoreOptions {
/// Ignore is a matcher useful for recursively walking one or more directories.
#[derive(Clone, Debug)]
-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>,
-}
+pub(crate) struct Ignore(Arc);
#[derive(Clone, Debug)]
struct IgnoreInner {
@@ -130,9 +112,12 @@ 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
@@ -167,36 +152,34 @@ struct IgnoreInner {
impl Ignore {
/// Return the directory path of this matcher.
- #[cfg(test)]
pub(crate) fn path(&self) -> &Path {
- &self.inner.dir
+ &self.0.dir
}
/// Return true if this matcher has no parent.
pub(crate) fn is_root(&self) -> bool {
- self.inner.parent.is_none()
+ 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
}
/// Return this matcher's parent, if one exists.
pub(crate) fn parent(&self) -> Option {
- self.inner.parent.as_ref().map(|parent| Ignore {
- inner: parent.clone(),
- absolute_base: self.absolute_base.clone(),
- })
+ self.0.parent.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.inner.opts.parents
- && !self.inner.opts.git_ignore
- && !self.inner.opts.git_exclude
- && !self.inner.opts.git_global
+ 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
{
// If we never need info from parent directories, then don't do
// anything.
@@ -226,34 +209,25 @@ impl Ignore {
let mut errs = PartialErrorBuilder::default();
let mut ig = self.clone();
for parent in parents.into_iter().rev() {
- let mut compiled = self.inner.compiled.write().unwrap();
+ let mut compiled = self.0.compiled.write().unwrap();
if let Some(weak) = compiled.get(parent.as_os_str()) {
if let Some(prebuilt) = weak.upgrade() {
- ig = Ignore {
- inner: prebuilt,
- absolute_base: Some(absolute_base.clone()),
- };
+ ig = Ignore(prebuilt);
continue;
}
}
let (mut igtmp, err) = ig.add_child_path(parent);
errs.maybe_push(err);
igtmp.is_absolute_parent = true;
- 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 {
- inner: ig_arc.clone(),
- absolute_base: Some(absolute_base.clone()),
+ 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
};
- compiled.insert(
- parent.as_os_str().to_os_string(),
- Arc::downgrade(&ig_arc),
- );
+ 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, errs.into_error_option())
}
@@ -266,161 +240,60 @@ 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 {
- 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,
- )
+ (Ignore(Arc::new(ig)), err)
}
/// Like add_child, but takes a full path and returns an IgnoreInner.
fn add_child_path(&self, dir: &Path) -> (IgnoreInner, Option) {
- 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 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();
let mut errs = PartialErrorBuilder::default();
- let custom_ig_matcher = if self
- .inner
- .custom_ignore_filenames
- .is_empty()
- {
+ let custom_ig_matcher = if self.0.custom_ignore_filenames.is_empty() {
Gitignore::empty()
} else {
- 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 (m, err) = create_gitignore(
+ &dir,
+ &dir,
+ &self.0.custom_ignore_filenames,
+ self.0.opts.ignore_case_insensitive,
+ );
+ errs.maybe_push(err);
+ m
};
- let ig_matcher = if !self.inner.opts.ignore
- || !ignore_files_list.is_none_or(|i| i.has_ignore)
- {
+ let ig_matcher = if !self.0.opts.ignore {
Gitignore::empty()
} else {
let (m, err) = create_gitignore(
&dir,
&dir,
&[".ignore"],
- self.inner.opts.ignore_case_insensitive,
+ self.0.opts.ignore_case_insensitive,
);
errs.maybe_push(err);
m
};
- let gi_matcher = if !self.inner.opts.git_ignore
- || !ignore_files_list.is_none_or(|i| i.has_git_ignore)
- {
+ let gi_matcher = if !self.0.opts.git_ignore {
Gitignore::empty()
} else {
let (m, err) = create_gitignore(
&dir,
&dir,
&[".gitignore"],
- self.inner.opts.ignore_case_insensitive,
+ self.0.opts.ignore_case_insensitive,
);
errs.maybe_push(err);
m
};
- let gi_exclude_matcher = if !self.inner.opts.git_exclude
- || !ignore_files_list.is_none_or(|i| i.has_git_dir)
- {
+ let gi_exclude_matcher = if !self.0.opts.git_exclude {
Gitignore::empty()
} else {
match resolve_git_commondir(dir, git_type) {
@@ -429,7 +302,7 @@ impl Ignore {
&dir,
&git_dir,
&["info/exclude"],
- self.inner.opts.ignore_case_insensitive,
+ self.0.opts.ignore_case_insensitive,
);
errs.maybe_push(err);
m
@@ -441,38 +314,32 @@ impl Ignore {
}
};
let ig = IgnoreInner {
- compiled: self.inner.compiled.clone(),
+ compiled: self.0.compiled.clone(),
dir: dir.to_path_buf(),
- overrides: self.inner.overrides.clone(),
- types: self.inner.types.clone(),
- parent: Some(self.inner.clone()),
+ overrides: self.0.overrides.clone(),
+ types: self.0.types.clone(),
+ parent: Some(self.clone()),
is_absolute_parent: false,
- 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(),
+ 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(),
custom_ignore_matcher: custom_ig_matcher,
ignore_matcher: ig_matcher,
- git_global_matcher: self.inner.git_global_matcher.clone(),
+ git_global_matcher: self.0.git_global_matcher.clone(),
git_ignore_matcher: gi_matcher,
git_exclude_matcher: gi_exclude_matcher,
has_git,
- opts: self.inner.opts,
+ opts: self.0.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.inner.opts;
- let has_custom_ignore_files =
- !self.inner.custom_ignore_filenames.is_empty();
- let has_explicit_ignores = !self.inner.explicit_ignores.is_empty();
+ 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();
opts.ignore
|| opts.git_global
@@ -483,12 +350,9 @@ 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.inner.opts.hidden && is_hidden_entry(dent) {
+ if m.is_none() && self.0.opts.hidden && is_hidden(dent) {
return Match::Ignore(IgnoreMatch::hidden());
}
m
@@ -498,11 +362,7 @@ impl Ignore {
/// ignored or not.
///
/// The match contains information about its origin.
- pub(crate) fn matched<'a, P: AsRef>(
- &'a self,
- path: P,
- is_dir: bool,
- ) -> Match> {
+ 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();
@@ -513,9 +373,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.inner.overrides.is_empty() {
+ if !self.0.overrides.is_empty() {
let mat = self
- .inner
+ .0
.overrides
.matched(path, is_dir)
.map(IgnoreMatch::overrides);
@@ -532,9 +392,8 @@ impl Ignore {
whitelisted = mat;
}
}
- if !self.inner.types.is_empty() {
- let mat =
- self.inner.types.matched(path, is_dir).map(IgnoreMatch::types);
+ if !self.0.types.is_empty() {
+ let mat = self.0.types.matched(path, is_dir).map(IgnoreMatch::types);
if mat.is_ignore() {
return mat;
} else if mat.is_whitelist() {
@@ -546,143 +405,98 @@ impl Ignore {
/// Performs matching only on the ignore files for this directory and
/// all parent directories.
- 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);
+ 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);
let mut saw_git = false;
- for ig in self.parents().take_while(|ig| !ig.inner.is_absolute_parent)
- {
+ for ig in self.parents().take_while(|ig| !ig.0.is_absolute_parent) {
if m_custom_ignore.is_none() {
- m_custom_ignore = ig
- .inner
- .custom_ignore_matcher
- .matched(path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_custom_ignore =
+ ig.0.custom_ignore_matcher
+ .matched(path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if m_ignore.is_none() {
- m_ignore = ig
- .inner
- .ignore_matcher
- .matched(path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_ignore =
+ ig.0.ignore_matcher
+ .matched(path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if any_git && !saw_git && m_gi.is_none() {
- m_gi = ig
- .inner
- .git_ignore_matcher
- .matched(path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_gi =
+ ig.0.git_ignore_matcher
+ .matched(path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if any_git && !saw_git && m_gi_exclude.is_none() {
- m_gi_exclude = ig
- .inner
- .git_exclude_matcher
- .matched(path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_gi_exclude =
+ ig.0.git_exclude_matcher
+ .matched(path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
- saw_git = saw_git || ig.inner.has_git;
+ saw_git = saw_git || ig.0.has_git;
}
- 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 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 m_custom_ignore.is_none() {
- m_custom_ignore = ig
- .inner
- .custom_ignore_matcher
- .matched(&path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_custom_ignore =
+ ig.0.custom_ignore_matcher
+ .matched(&path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if m_ignore.is_none() {
- m_ignore = ig
- .inner
- .ignore_matcher
- .matched(&path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_ignore =
+ ig.0.ignore_matcher
+ .matched(&path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if any_git && !saw_git && m_gi.is_none() {
- m_gi = ig
- .inner
- .git_ignore_matcher
- .matched(&path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_gi =
+ ig.0.git_ignore_matcher
+ .matched(&path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
if any_git && !saw_git && m_gi_exclude.is_none() {
- m_gi_exclude = ig
- .inner
- .git_exclude_matcher
- .matched(&path, is_dir)
- .map(IgnoreMatch::gitignore);
+ m_gi_exclude =
+ ig.0.git_exclude_matcher
+ .matched(&path, is_dir)
+ .map(IgnoreMatch::gitignore);
}
- saw_git = saw_git || ig.inner.has_git;
+ saw_git = saw_git || ig.0.has_git;
}
}
}
- 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
+ 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
+ // 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.
//
- // 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;
}
+ if !m_explicit.is_none() {
+ break;
+ }
m_explicit = gi.matched(&path, is_dir).map(IgnoreMatch::gitignore);
}
let m_global = if any_git {
- self.inner
+ self.0
.git_global_matcher
.matched(&path, is_dir)
.map(IgnoreMatch::gitignore)
@@ -690,90 +504,59 @@ impl Ignore {
Match::None
};
- // 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.
+ // 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.
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() {
- return check;
+ if check.is_none() {
+ continue;
}
+
+ return check.clone();
}
- Match::None
+
+ m_explicit
}
/// Returns an iterator over parent ignore matchers, including this one.
pub(crate) fn parents(&self) -> Parents<'_> {
- Parents(Some(IgnoreRef { inner: &self.inner }))
+ Parents(Some(self))
}
/// Returns the first absolute path of the first absolute parent, if
/// one exists.
fn absolute_base(&self) -> Option<&Path> {
- 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
+ self.0.absolute_base.as_ref().map(|p| &***p)
}
}
/// An iterator over all parents of an ignore matcher, including itself.
-pub(crate) struct Parents<'a>(Option>);
+///
+/// The lifetime `'a` refers to the lifetime of the initial `Ignore` matcher.
+pub(crate) struct Parents<'a>(Option<&'a Ignore>);
impl<'a> Iterator for Parents<'a> {
- type Item = IgnoreRef<'a>;
+ type Item = &'a Ignore;
- fn next(&mut self) -> Option> {
+ fn next(&mut self) -> Option<&'a Ignore> {
match self.0.take() {
None => None,
Some(ig) => {
- self.0 = ig
- .inner
- .parent
- .as_deref()
- .map(|inner| IgnoreRef { inner });
+ self.0 = ig.0.parent.as_ref();
Some(ig)
}
}
@@ -862,42 +645,33 @@ 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 {
- 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,
- }),
+ 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,
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
}
@@ -907,10 +681,7 @@ 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
}
@@ -941,7 +712,8 @@ 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
}
@@ -953,11 +725,6 @@ 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
@@ -1028,10 +795,7 @@ 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
}
@@ -1090,10 +854,7 @@ 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()) {
@@ -1138,20 +899,15 @@ fn resolve_git_commondir(
/// Strips `prefix` from `path` if it's a prefix, otherwise returns `path`
/// unchanged.
-fn strip_if_is_prefix<'a, P: AsRef + ?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, sync::Arc};
+ use std::{io::Write, path::Path};
- 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();
@@ -1180,11 +936,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());
@@ -1455,152 +1211,15 @@ mod tests {
let (ig2, err) = ig1.add_child("src");
assert!(err.is_none());
- assert!(ig1.matched("llvm", true).is_none());
- assert!(ig2.matched("llvm", true).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!(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();
@@ -1608,14 +1227,16 @@ 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 8824139b5..f822d8390 100644
--- a/crates/ignore/src/gitignore.rs
+++ b/crates/ignore/src/gitignore.rs
@@ -102,9 +102,7 @@ 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);
@@ -125,17 +123,6 @@ 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
@@ -158,8 +145,7 @@ 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,
}
}
@@ -204,11 +190,7 @@ 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;
}
@@ -261,16 +243,11 @@ 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;
}
@@ -293,10 +270,7 @@ 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
@@ -352,8 +326,7 @@ 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,
}
}
@@ -364,21 +337,18 @@ 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::with_available_parallelism_capacity(|| vec![]),
- )),
- // CHANGED: Add a flag to have Gitignore rules that apply only to
- // files.
+ matches: Some(Arc::new(Pool::new(|| vec![]))),
+ // CHANGED: Add a flag to have Gitignore rules that apply only to files.
only_on_files: self.only_on_files,
})
}
@@ -441,8 +411,11 @@ 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));
@@ -564,10 +537,7 @@ 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;
@@ -586,10 +556,7 @@ 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
}
@@ -609,56 +576,32 @@ impl GitignoreBuilder {
///
/// Note that the file path returned may not exist.
pub fn gitconfig_excludes_path() -> Option {
- // 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;
+ // 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 => {}
}
- 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)
+ match gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x)) {
+ Some(path) => return Some(path),
+ None => {}
+ }
+ excludes_file_default()
}
/// 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 = home_dir()?;
- let mut file = BufReader::new(File::open(home.join(".gitconfig")).ok()?);
+ 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 mut contents = vec![];
file.read_to_end(&mut contents).ok().map(|_| contents)
}
@@ -667,11 +610,19 @@ 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")
- .map(PathBuf::from)
- .filter(|x| !x.as_os_str().is_empty())
+ .and_then(|x| {
+ if x.is_empty() {
+ None
+ } else {
+ Some(PathBuf::from(x))
+ }
+ })
.or_else(|| home_dir().map(|p| p.join(".config")))
- .map(|x| x.join("git/config"))?;
- let mut file = BufReader::new(File::open(path).ok()?);
+ .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),
+ };
let mut contents = vec![];
file.read_to_end(&mut contents).ok().map(|_| contents)
}
@@ -681,8 +632,13 @@ fn gitconfig_xdg_contents() -> Option> {
/// Specifically, this respects XDG_CONFIG_HOME.
fn excludes_file_default() -> Option {
std::env::var_os("XDG_CONFIG_HOME")
- .map(PathBuf::from)
- .filter(|x| !x.as_os_str().is_empty())
+ .and_then(|x| {
+ if x.is_empty() {
+ None
+ } else {
+ Some(PathBuf::from(x))
+ }
+ })
.or_else(|| home_dir().map(|p| p.join(".config")))
.map(|x| x.join("git/ignore"))
}
@@ -711,7 +667,9 @@ 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.
@@ -873,10 +831,7 @@ 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
deleted file mode 100644
index 5030b0d00..000000000
--- a/crates/ignore/src/incremental.rs
+++ /dev/null
@@ -1,1286 +0,0 @@
-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