Compare commits
3 commits
main
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec06099222 | ||
|
|
027097d94e | ||
|
|
dd23e0f029 |
285 changed files with 20992 additions and 47760 deletions
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -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
|
||||
|
||||
-->
|
||||
|
||||
|
|
|
|||
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
|
|
@ -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.'
|
||||
|
|
|
|||
51
.github/workflows/integration-tests.yml
vendored
51
.github/workflows/integration-tests.yml
vendored
|
|
@ -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.'
|
||||
|
|
|
|||
219
.github/workflows/prepare-release.yml
vendored
219
.github/workflows/prepare-release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
318
.github/workflows/release-insiders.yml
vendored
Normal file
318
.github/workflows/release-insiders.yml
vendored
Normal file
|
|
@ -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'
|
||||
})
|
||||
270
.github/workflows/release.yml
vendored
270
.github/workflows/release.yml
vendored
|
|
@ -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.'
|
||||
|
|
|
|||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
auto-install-peers = true
|
||||
|
|
@ -4,6 +4,5 @@ pnpm-lock.yaml
|
|||
target/
|
||||
crates/node/index.d.ts
|
||||
crates/node/index.js
|
||||
crates/ignore/
|
||||
.next
|
||||
.fingerprint
|
||||
|
|
|
|||
294
CHANGELOG.md
294
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<flex>`, `%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 `<length>` 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-<number>` → `inset-s-<number>` 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-<number>` → `inset-e-<number>` 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
|
||||
|
|
|
|||
247
Cargo.lock
generated
247
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -13,10 +13,10 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=main" alt="Build Status"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=next" alt="Build Status"></a>
|
||||
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
|
||||
<a href="https://github.com/tailwindcss/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ignore"
|
||||
version = "0.4.33" #:version
|
||||
version = "0.4.24" #:version
|
||||
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
||||
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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -102,9 +102,7 @@ impl Gitignore {
|
|||
///
|
||||
/// Note that I/O errors are ignored. For more granular control over
|
||||
/// errors, use `GitignoreBuilder`.
|
||||
pub fn new<P: AsRef<Path>>(
|
||||
gitignore_path: P,
|
||||
) -> (Gitignore, Option<Error>) {
|
||||
pub fn new<P: AsRef<Path>>(gitignore_path: P) -> (Gitignore, Option<Error>) {
|
||||
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<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
pub fn matched<P: AsRef<Path>>(&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<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
fn matched_stripped<P: AsRef<Path>>(&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<Path> + ?Sized>(
|
||||
&'a self,
|
||||
path: &'a P,
|
||||
) -> &'a Path {
|
||||
fn strip<'a, P: 'a + AsRef<Path> + ?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<Gitignore, Error> {
|
||||
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<PathBuf> {
|
||||
// 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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
|||
/// the user's XDG_CONFIG_HOME directory.
|
||||
fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
|||
/// Specifically, this respects XDG_CONFIG_HOME.
|
||||
fn excludes_file_default() -> Option<PathBuf> {
|
||||
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<PathBuf> {
|
|||
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]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -48,16 +48,13 @@ See the documentation for `WalkBuilder` for many other options.
|
|||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use crate::incremental::{IncrementalIgnore, IncrementalMatch};
|
||||
pub use crate::walk::{
|
||||
DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder,
|
||||
WalkParallel, WalkState,
|
||||
DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder, WalkParallel, WalkState,
|
||||
};
|
||||
|
||||
mod default_types;
|
||||
mod dir;
|
||||
pub mod gitignore;
|
||||
mod incremental;
|
||||
pub mod overrides;
|
||||
mod pathutil;
|
||||
pub mod types;
|
||||
|
|
@ -123,31 +120,34 @@ impl Clone for Error {
|
|||
fn clone(&self) -> Error {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => Error::Partial(errs.clone()),
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
Error::WithLineNumber { line, err: err.clone() }
|
||||
}
|
||||
Error::WithPath { ref path, ref err } => {
|
||||
Error::WithPath { path: path.clone(), err: err.clone() }
|
||||
}
|
||||
Error::WithDepth { depth, ref err } => {
|
||||
Error::WithDepth { depth, err: err.clone() }
|
||||
}
|
||||
Error::Loop { ref ancestor, ref child } => Error::Loop {
|
||||
Error::WithLineNumber { line, ref err } => Error::WithLineNumber {
|
||||
line,
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::WithPath { ref path, ref err } => Error::WithPath {
|
||||
path: path.clone(),
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::WithDepth { depth, ref err } => Error::WithDepth {
|
||||
depth,
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::Loop {
|
||||
ref ancestor,
|
||||
ref child,
|
||||
} => Error::Loop {
|
||||
ancestor: ancestor.clone(),
|
||||
child: child.clone(),
|
||||
},
|
||||
Error::Io(ref err) => match err.raw_os_error() {
|
||||
Some(e) => Error::Io(std::io::Error::from_raw_os_error(e)),
|
||||
None => {
|
||||
Error::Io(std::io::Error::new(err.kind(), err.to_string()))
|
||||
}
|
||||
None => Error::Io(std::io::Error::new(err.kind(), err.to_string())),
|
||||
},
|
||||
Error::Glob { ref glob, ref err } => {
|
||||
Error::Glob { glob: glob.clone(), err: err.clone() }
|
||||
}
|
||||
Error::UnrecognizedFileType(ref err) => {
|
||||
Error::UnrecognizedFileType(err.clone())
|
||||
}
|
||||
Error::Glob { ref glob, ref err } => Error::Glob {
|
||||
glob: glob.clone(),
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::UnrecognizedFileType(ref err) => Error::UnrecognizedFileType(err.clone()),
|
||||
Error::InvalidDefinition => Error::InvalidDefinition,
|
||||
}
|
||||
}
|
||||
|
|
@ -269,14 +269,19 @@ impl Error {
|
|||
|
||||
/// Turn an error into a tagged error with the given depth.
|
||||
fn with_depth(self, depth: usize) -> Error {
|
||||
Error::WithDepth { depth, err: Box::new(self) }
|
||||
Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(self),
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn an error into a tagged error with the given file path and line
|
||||
/// number. If path is empty, then it is omitted from the error.
|
||||
fn tagged<P: AsRef<Path>>(self, path: P, lineno: u64) -> Error {
|
||||
let errline =
|
||||
Error::WithLineNumber { line: lineno, err: Box::new(self) };
|
||||
let errline = Error::WithLineNumber {
|
||||
line: lineno,
|
||||
err: Box::new(self),
|
||||
};
|
||||
if path.as_ref().as_os_str().is_empty() {
|
||||
return errline;
|
||||
}
|
||||
|
|
@ -296,12 +301,12 @@ impl Error {
|
|||
};
|
||||
}
|
||||
let path = err.path().map(|p| p.to_path_buf());
|
||||
let mut ig_err = Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(Error::Io(std::io::Error::from(err))),
|
||||
};
|
||||
let mut ig_err = Error::Io(std::io::Error::from(err));
|
||||
if let Some(path) = path {
|
||||
ig_err = Error::WithPath { path, err: Box::new(ig_err) };
|
||||
ig_err = Error::WithPath {
|
||||
path,
|
||||
err: Box::new(ig_err),
|
||||
};
|
||||
}
|
||||
ig_err
|
||||
}
|
||||
|
|
@ -328,8 +333,7 @@ impl std::fmt::Display for Error {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => {
|
||||
let msgs: Vec<String> =
|
||||
errs.iter().map(|err| err.to_string()).collect();
|
||||
let msgs: Vec<String> = errs.iter().map(|err| err.to_string()).collect();
|
||||
write!(f, "{}", msgs.join("\n"))
|
||||
}
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
|
|
@ -339,7 +343,10 @@ impl std::fmt::Display for Error {
|
|||
write!(f, "{}: {}", path.display(), err)
|
||||
}
|
||||
Error::WithDepth { ref err, .. } => err.fmt(f),
|
||||
Error::Loop { ref ancestor, ref child } => write!(
|
||||
Error::Loop {
|
||||
ref ancestor,
|
||||
ref child,
|
||||
} => write!(
|
||||
f,
|
||||
"File system loop found: \
|
||||
{} points to an ancestor {}",
|
||||
|
|
@ -347,8 +354,14 @@ impl std::fmt::Display for Error {
|
|||
ancestor.display()
|
||||
),
|
||||
Error::Io(ref err) => err.fmt(f),
|
||||
Error::Glob { glob: None, ref err } => write!(f, "{}", err),
|
||||
Error::Glob { glob: Some(ref glob), ref err } => {
|
||||
Error::Glob {
|
||||
glob: None,
|
||||
ref err,
|
||||
} => write!(f, "{}", err),
|
||||
Error::Glob {
|
||||
glob: Some(ref glob),
|
||||
ref err,
|
||||
} => {
|
||||
write!(f, "error parsing glob '{}': {}", glob, err)
|
||||
}
|
||||
Error::UnrecognizedFileType(ref ty) => {
|
||||
|
|
@ -494,8 +507,7 @@ mod tests {
|
|||
};
|
||||
|
||||
/// A convenient result type alias.
|
||||
pub(crate) type Result<T> =
|
||||
std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
pub(crate) type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
macro_rules! err {
|
||||
($($tt:tt)*) => {
|
||||
|
|
@ -533,9 +545,8 @@ mod tests {
|
|||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
fs::create_dir_all(&path).map_err(|e| {
|
||||
err!("failed to create {}: {}", path.display(), e)
|
||||
})?;
|
||||
fs::create_dir_all(&path)
|
||||
.map_err(|e| err!("failed to create {}: {}", path.display(), e))?;
|
||||
return Ok(TempDir(path));
|
||||
}
|
||||
Err(err!("failed to create temp dir after {} tries", TRIES))
|
||||
|
|
|
|||
|
|
@ -94,11 +94,7 @@ impl Override {
|
|||
/// given) is stripped. If there is no common suffix/prefix overlap, then
|
||||
/// `path` is assumed to reside in the same directory as the root path for
|
||||
/// this set of overrides.
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
pub fn matched<'a, P: AsRef<Path>>(&'a self, path: P, is_dir: bool) -> Match<Glob<'a>> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
|
|
@ -150,10 +146,7 @@ impl OverrideBuilder {
|
|||
/// affected.
|
||||
///
|
||||
/// This is disabled by default.
|
||||
pub fn case_insensitive(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> Result<&mut OverrideBuilder, Error> {
|
||||
pub fn case_insensitive(&mut self, yes: bool) -> Result<&mut OverrideBuilder, Error> {
|
||||
// TODO: This should not return a `Result`. Fix this in the next semver
|
||||
// release.
|
||||
self.builder.case_insensitive(yes)?;
|
||||
|
|
@ -283,8 +276,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn default_case_sensitive() {
|
||||
let ov =
|
||||
OverrideBuilder::new(ROOT).add("*.html").unwrap().build().unwrap();
|
||||
let ov = OverrideBuilder::new(ROOT)
|
||||
.add("*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(ov.matched("foo.html", false).is_whitelist());
|
||||
assert!(ov.matched("foo.HTML", false).is_ignore());
|
||||
assert!(ov.matched("foo.htm", false).is_ignore());
|
||||
|
|
|
|||
|
|
@ -2,89 +2,55 @@ use std::{ffi::OsStr, path::Path};
|
|||
|
||||
use crate::walk::DirEntry;
|
||||
|
||||
/// Returns true if and only if this path is considered to be hidden.
|
||||
/// Returns true if and only if this entry is considered to be hidden.
|
||||
///
|
||||
/// # Platform behavior
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Unix, this implements a more optimized check.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
if let Some(name) = file_name(dent.path()) {
|
||||
name.as_bytes().get(0) == Some(&b'.')
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if this entry is considered to be hidden.
|
||||
///
|
||||
/// This returns true if one of the following is true:
|
||||
/// On Windows, this returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
///
|
||||
/// ## All other platforms
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
pub(crate) fn is_hidden_path(dent: &Path) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(path: &Path) -> bool {
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(path: &Path) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
if let Ok(md) = path.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
// This looks like we're doing an extra stat call, but on Windows, the
|
||||
// directory traverser reuses the metadata retrieved from each directory
|
||||
// entry and stores it on the DirEntry itself. So this is "free."
|
||||
if let Ok(md) = dent.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
if let Some(name) = file_name(dent.path()) {
|
||||
name.to_str().map(|s| s.starts_with(".")).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if this directory entry is considered to be
|
||||
/// hidden.
|
||||
///
|
||||
/// # Platform behavior
|
||||
///
|
||||
/// ## Windows
|
||||
///
|
||||
/// This returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
///
|
||||
/// ## All other platforms
|
||||
/// Returns true if and only if this entry is considered to be hidden.
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
pub(crate) fn is_hidden_entry(dent: &DirEntry) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
// This looks like we're doing an extra stat call, but on Windows, the
|
||||
// directory traverser reuses the metadata retrieved from each directory
|
||||
// entry and stores it on the DirEntry itself. So this is "free."
|
||||
if let Ok(md) = dent.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
}
|
||||
|
||||
/// Returns true if and only if this path is considered to be hidden from only
|
||||
/// the path itself.
|
||||
///
|
||||
/// This has the same behavior on all platforms.
|
||||
fn is_hidden_path_only(path: &Path) -> bool {
|
||||
if let Some(name) = file_name(path) {
|
||||
name.as_encoded_bytes().starts_with(b".")
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
||||
if let Some(name) = file_name(dent.path()) {
|
||||
name.to_str().map(|s| s.starts_with(".")).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
@ -93,79 +59,83 @@ fn is_hidden_path_only(path: &Path) -> bool {
|
|||
/// Strip `prefix` from the `path` and return the remainder.
|
||||
///
|
||||
/// If `path` doesn't have a prefix `prefix`, then return `None`.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn strip_prefix<'a, P: AsRef<Path> + ?Sized>(
|
||||
prefix: &'a P,
|
||||
path: &'a Path,
|
||||
) -> Option<&'a Path> {
|
||||
#[cfg(unix)]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
let prefix = prefix.as_os_str().as_bytes();
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if prefix.len() > path.len() || prefix != &path[0..prefix.len()] {
|
||||
None
|
||||
} else {
|
||||
Some(&Path::new(OsStr::from_bytes(&path[prefix.len()..])))
|
||||
}
|
||||
let prefix = prefix.as_ref().as_os_str().as_bytes();
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if prefix.len() > path.len() || prefix != &path[0..prefix.len()] {
|
||||
None
|
||||
} else {
|
||||
Some(&Path::new(OsStr::from_bytes(&path[prefix.len()..])))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
path.strip_prefix(prefix).ok()
|
||||
}
|
||||
|
||||
imp(prefix.as_ref(), path)
|
||||
/// Strip `prefix` from the `path` and return the remainder.
|
||||
///
|
||||
/// If `path` doesn't have a prefix `prefix`, then return `None`.
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) fn strip_prefix<'a, P: AsRef<Path> + ?Sized>(
|
||||
prefix: &'a P,
|
||||
path: &'a Path,
|
||||
) -> Option<&'a Path> {
|
||||
path.strip_prefix(prefix).ok()
|
||||
}
|
||||
|
||||
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||
/// the empty string.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
memchr::memchr(b'/', path.as_ref().as_os_str().as_encoded_bytes())
|
||||
.is_none()
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
path.as_ref()
|
||||
.parent()
|
||||
.map(|p| p.as_os_str().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use memchr::memchr;
|
||||
|
||||
let path = path.as_ref().as_os_str().as_bytes();
|
||||
memchr(b'/', path).is_none()
|
||||
}
|
||||
|
||||
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||
/// the empty string.
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||
path.as_ref()
|
||||
.parent()
|
||||
.map(|p| p.as_os_str().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The final component of the path, if it is a normal file.
|
||||
///
|
||||
/// If the path terminates in `.`, `..`, or consists solely of a root of
|
||||
/// prefix, this will return `None`.
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(
|
||||
path: &'a P,
|
||||
) -> Option<&'a OsStr> {
|
||||
#[cfg(unix)]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
/// If the path terminates in ., .., or consists solely of a root of prefix,
|
||||
/// file_name will return None.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(path: &'a P) -> Option<&'a OsStr> {
|
||||
use memchr::memrchr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use memchr::memrchr;
|
||||
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
} else if path.len() == 1 && path[0] == b'.' {
|
||||
return None;
|
||||
} else if path.last() == Some(&b'.') {
|
||||
return None;
|
||||
} else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {
|
||||
return None;
|
||||
}
|
||||
let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);
|
||||
Some(OsStr::from_bytes(&path[last_slash..]))
|
||||
let path = path.as_ref().as_os_str().as_bytes();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
} else if path.len() == 1 && path[0] == b'.' {
|
||||
return None;
|
||||
} else if path.last() == Some(&b'.') {
|
||||
return None;
|
||||
} else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
path.file_name()
|
||||
}
|
||||
|
||||
imp(path.as_ref())
|
||||
let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);
|
||||
Some(OsStr::from_bytes(&path[last_slash..]))
|
||||
}
|
||||
|
||||
/// The final component of the path, if it is a normal file.
|
||||
///
|
||||
/// If the path terminates in ., .., or consists solely of a root of prefix,
|
||||
/// file_name will return None.
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(path: &'a P) -> Option<&'a OsStr> {
|
||||
path.as_ref().file_name()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,12 +204,8 @@ impl<T> Selection<T> {
|
|||
|
||||
fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Selection<U> {
|
||||
match self {
|
||||
Selection::Select(name, inner) => {
|
||||
Selection::Select(name, f(inner))
|
||||
}
|
||||
Selection::Negate(name, inner) => {
|
||||
Selection::Negate(name, f(inner))
|
||||
}
|
||||
Selection::Select(name, inner) => Selection::Select(name, f(inner)),
|
||||
Selection::Negate(name, inner) => Selection::Negate(name, f(inner)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -231,9 +227,7 @@ impl Types {
|
|||
has_selected: false,
|
||||
glob_to_selection: vec![],
|
||||
set: GlobSetBuilder::new().build().unwrap(),
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
matches: Arc::new(Pool::new(|| vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,11 +254,7 @@ impl Types {
|
|||
/// The path is considered ignored if it matches a negated file type.
|
||||
/// If at least one file type is selected and `path` doesn't match, then
|
||||
/// the path is also considered ignored.
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
pub fn matched<'a, P: AsRef<Path>>(&'a self, path: P, is_dir: bool) -> Match<Glob<'a>> {
|
||||
// File types don't apply to directories, and we can't do anything
|
||||
// if our glob set is empty.
|
||||
if is_dir || self.set.is_empty() {
|
||||
|
|
@ -316,7 +306,10 @@ impl TypesBuilder {
|
|||
/// of default type definitions can be added with `add_defaults`, and
|
||||
/// additional type definitions can be added with `select` and `negate`.
|
||||
pub fn new() -> TypesBuilder {
|
||||
TypesBuilder { types: HashMap::new(), selections: vec![] }
|
||||
TypesBuilder {
|
||||
types: HashMap::new(),
|
||||
selections: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the current set of file type definitions *and* selections into
|
||||
|
|
@ -350,18 +343,17 @@ impl TypesBuilder {
|
|||
}
|
||||
selections.push(selection.clone().map(move |_| def));
|
||||
}
|
||||
let set = build_set
|
||||
.build()
|
||||
.map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
|
||||
let set = build_set.build().map_err(|err| Error::Glob {
|
||||
glob: None,
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
Ok(Types {
|
||||
defs,
|
||||
selections,
|
||||
has_selected,
|
||||
glob_to_selection,
|
||||
set,
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
matches: Arc::new(Pool::new(|| vec![])),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -385,10 +377,12 @@ impl TypesBuilder {
|
|||
pub fn select(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
self.selections
|
||||
.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
self.selections
|
||||
.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
@ -399,10 +393,12 @@ impl TypesBuilder {
|
|||
pub fn negate(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
self.selections
|
||||
.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
self.selections
|
||||
.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
@ -457,10 +453,7 @@ impl TypesBuilder {
|
|||
3 => {
|
||||
let name = parts[0];
|
||||
let types_string = parts[2];
|
||||
if name.is_empty()
|
||||
|| parts[1] != "include"
|
||||
|| types_string.is_empty()
|
||||
{
|
||||
if name.is_empty() || parts[1] != "include" || types_string.is_empty() {
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
let types = types_string.split(',');
|
||||
|
|
@ -470,8 +463,7 @@ impl TypesBuilder {
|
|||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
for type_name in types {
|
||||
let globs =
|
||||
self.types.get(type_name).unwrap().globs.clone();
|
||||
let globs = self.types.get(type_name).unwrap().globs.clone();
|
||||
for glob in globs {
|
||||
self.add(name, &glob)?;
|
||||
}
|
||||
|
|
@ -557,9 +549,30 @@ mod tests {
|
|||
|
||||
matched!(not, matchnot1, types(), vec!["rust"], vec![], "index.html");
|
||||
matched!(not, matchnot2, types(), vec![], vec!["rust"], "main.rs");
|
||||
matched!(not, matchnot3, types(), vec!["foo"], vec!["rust"], "main.rs");
|
||||
matched!(not, matchnot4, types(), vec!["rust"], vec!["foo"], "main.rs");
|
||||
matched!(not, matchnot5, types(), vec!["rust"], vec!["foo"], "main.foo");
|
||||
matched!(
|
||||
not,
|
||||
matchnot3,
|
||||
types(),
|
||||
vec!["foo"],
|
||||
vec!["rust"],
|
||||
"main.rs"
|
||||
);
|
||||
matched!(
|
||||
not,
|
||||
matchnot4,
|
||||
types(),
|
||||
vec!["rust"],
|
||||
vec!["foo"],
|
||||
"main.rs"
|
||||
);
|
||||
matched!(
|
||||
not,
|
||||
matchnot5,
|
||||
types(),
|
||||
vec!["rust"],
|
||||
vec!["foo"],
|
||||
"main.foo"
|
||||
);
|
||||
matched!(not, matchnot6, types(), vec!["combo"], vec![], "leftpad.js");
|
||||
matched!(not, matchnot7, types(), vec!["py"], vec![], "index.html");
|
||||
matched!(not, matchnot8, types(), vec!["python"], vec![], "doc.md");
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ use {
|
|||
use crate::{
|
||||
Error, PartialErrorBuilder,
|
||||
dir::{Ignore, IgnoreBuilder},
|
||||
// CHANGED: Also import `Gitignore` for `WalkBuilder::add_gitignore`.
|
||||
gitignore::{Gitignore, GitignoreBuilder},
|
||||
incremental::{IncrementalIgnore, IncrementalIgnoreOptions},
|
||||
overrides::Override,
|
||||
types::Types,
|
||||
};
|
||||
|
|
@ -106,15 +104,24 @@ impl DirEntry {
|
|||
}
|
||||
|
||||
fn new_stdin() -> DirEntry {
|
||||
DirEntry { dent: DirEntryInner::Stdin, err: None }
|
||||
DirEntry {
|
||||
dent: DirEntryInner::Stdin,
|
||||
err: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_walkdir(dent: walkdir::DirEntry, err: Option<Error>) -> DirEntry {
|
||||
DirEntry { dent: DirEntryInner::Walkdir(dent), err }
|
||||
DirEntry {
|
||||
dent: DirEntryInner::Walkdir(dent),
|
||||
err,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_raw(dent: DirEntryRaw, err: Option<Error>) -> DirEntry {
|
||||
DirEntry { dent: DirEntryInner::Raw(dent), err }
|
||||
DirEntry {
|
||||
dent: DirEntryInner::Raw(dent),
|
||||
err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,11 +187,9 @@ impl DirEntryInner {
|
|||
));
|
||||
Err(err.with_path("<stdin>"))
|
||||
}
|
||||
Walkdir(ref x) => x.metadata().map_err(|err| {
|
||||
Error::Io(io::Error::from(err))
|
||||
.with_depth(x.depth())
|
||||
.with_path(x.path())
|
||||
}),
|
||||
Walkdir(ref x) => x
|
||||
.metadata()
|
||||
.map_err(|err| Error::Io(io::Error::from(err)).with_path(x.path())),
|
||||
Raw(ref x) => x.metadata(),
|
||||
}
|
||||
}
|
||||
|
|
@ -303,9 +308,7 @@ impl DirEntryRaw {
|
|||
} else {
|
||||
fs::symlink_metadata(&self.path)
|
||||
}
|
||||
.map_err(|err| {
|
||||
Error::Io(err).with_depth(self.depth).with_path(&self.path)
|
||||
})
|
||||
.map_err(|err| Error::Io(io::Error::from(err)).with_path(&self.path))
|
||||
}
|
||||
|
||||
fn file_type(&self) -> FileType {
|
||||
|
|
@ -313,7 +316,9 @@ impl DirEntryRaw {
|
|||
}
|
||||
|
||||
fn file_name(&self) -> &OsStr {
|
||||
self.path.file_name().unwrap_or_else(|| self.path.as_os_str())
|
||||
self.path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| self.path.as_os_str())
|
||||
}
|
||||
|
||||
fn depth(&self) -> usize {
|
||||
|
|
@ -325,13 +330,13 @@ impl DirEntryRaw {
|
|||
self.ino
|
||||
}
|
||||
|
||||
fn from_entry(
|
||||
depth: usize,
|
||||
ent: &fs::DirEntry,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
fn from_entry(depth: usize, ent: &fs::DirEntry) -> Result<DirEntryRaw, Error> {
|
||||
let ty = ent.file_type().map_err(|err| {
|
||||
let err = Error::Io(err).with_depth(depth).with_path(ent.path());
|
||||
Error::WithDepth { depth, err: Box::new(err) }
|
||||
let err = Error::Io(io::Error::from(err)).with_path(ent.path());
|
||||
Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(err),
|
||||
}
|
||||
})?;
|
||||
DirEntryRaw::from_entry_os(depth, ent, ty)
|
||||
}
|
||||
|
|
@ -343,8 +348,11 @@ impl DirEntryRaw {
|
|||
ty: fs::FileType,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
let md = ent.metadata().map_err(|err| {
|
||||
let err = Error::Io(err).with_depth(depth).with_path(ent.path());
|
||||
Error::WithDepth { depth, err: Box::new(err) }
|
||||
let err = Error::Io(io::Error::from(err)).with_path(ent.path());
|
||||
Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(err),
|
||||
}
|
||||
})?;
|
||||
Ok(DirEntryRaw {
|
||||
path: ent.path(),
|
||||
|
|
@ -387,13 +395,8 @@ impl DirEntryRaw {
|
|||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn from_path(
|
||||
depth: usize,
|
||||
pb: PathBuf,
|
||||
link: bool,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
let md = fs::metadata(&pb)
|
||||
.map_err(|err| Error::Io(err).with_depth(depth).with_path(&pb))?;
|
||||
fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result<DirEntryRaw, Error> {
|
||||
let md = fs::metadata(&pb).map_err(|err| Error::Io(err).with_path(&pb))?;
|
||||
Ok(DirEntryRaw {
|
||||
path: pb,
|
||||
ty: md.file_type(),
|
||||
|
|
@ -404,15 +407,10 @@ impl DirEntryRaw {
|
|||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn from_path(
|
||||
depth: usize,
|
||||
pb: PathBuf,
|
||||
link: bool,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result<DirEntryRaw, Error> {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let md = fs::metadata(&pb)
|
||||
.map_err(|err| Error::Io(err).with_depth(depth).with_path(&pb))?;
|
||||
let md = fs::metadata(&pb).map_err(|err| Error::Io(err).with_path(&pb))?;
|
||||
Ok(DirEntryRaw {
|
||||
path: pb,
|
||||
ty: md.file_type(),
|
||||
|
|
@ -425,11 +423,7 @@ impl DirEntryRaw {
|
|||
// Placeholder implementation to allow compiling on non-standard platforms
|
||||
// (e.g. wasm32).
|
||||
#[cfg(not(any(windows, unix)))]
|
||||
fn from_path(
|
||||
depth: usize,
|
||||
pb: PathBuf,
|
||||
link: bool,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result<DirEntryRaw, Error> {
|
||||
Err(Error::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"unsupported platform",
|
||||
|
|
@ -508,8 +502,7 @@ pub struct WalkBuilder {
|
|||
///
|
||||
/// When `None`, the CWD is fetched from `std::env::current_dir()`. If
|
||||
/// that fails, then global gitignores are ignored (an error is logged).
|
||||
global_gitignores_relative_to:
|
||||
OnceLock<Result<PathBuf, Arc<std::io::Error>>>,
|
||||
global_gitignores_relative_to: OnceLock<Result<PathBuf, Arc<std::io::Error>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -551,16 +544,8 @@ impl WalkBuilder {
|
|||
/// is better to call `add` on this builder than to create multiple
|
||||
/// `Walk` values.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> WalkBuilder {
|
||||
WalkBuilder::from_iter([path])
|
||||
}
|
||||
|
||||
/// Create an empty builder to which paths can be added.
|
||||
///
|
||||
/// Note that if you call `build` on this instance before calling `add`
|
||||
/// on it, it will return exactly zero items during iteration.
|
||||
pub fn empty() -> WalkBuilder {
|
||||
WalkBuilder {
|
||||
paths: vec![],
|
||||
paths: vec![path.as_ref().to_path_buf()],
|
||||
ig_builder: IgnoreBuilder::new(),
|
||||
max_depth: None,
|
||||
min_depth: None,
|
||||
|
|
@ -575,21 +560,6 @@ impl WalkBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new builder for a recursive directory iterator from the
|
||||
/// sequence of paths.
|
||||
///
|
||||
/// Note that if the iterator is empty, this is the same as
|
||||
/// `WalkBuilder::empty`.
|
||||
pub fn from_iter<P: AsRef<Path>, I: IntoIterator<Item = P>>(
|
||||
paths: I,
|
||||
) -> WalkBuilder {
|
||||
let mut builder = WalkBuilder::empty();
|
||||
for path in paths.into_iter() {
|
||||
builder.add(path);
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
/// Build a new `Walk` iterator.
|
||||
pub fn build(&self) -> Walk {
|
||||
let follow_links = self.follow_links;
|
||||
|
|
@ -615,14 +585,10 @@ impl WalkBuilder {
|
|||
if let Some(ref sorter) = sorter {
|
||||
match sorter.clone() {
|
||||
Sorter::ByName(cmp) => {
|
||||
wd = wd.sort_by(move |a, b| {
|
||||
cmp(a.file_name(), b.file_name())
|
||||
});
|
||||
wd = wd.sort_by(move |a, b| cmp(a.file_name(), b.file_name()));
|
||||
}
|
||||
Sorter::ByPath(cmp) => {
|
||||
wd = wd.sort_by(move |a, b| {
|
||||
cmp(a.path(), b.path())
|
||||
});
|
||||
wd = wd.sort_by(move |a, b| cmp(a.path(), b.path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -631,76 +597,31 @@ impl WalkBuilder {
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter();
|
||||
let ig_root = self.build_ignore();
|
||||
let ig_root = self
|
||||
.get_or_set_current_dir()
|
||||
.map(|cwd| self.ig_builder.build_with_cwd(Some(cwd.to_path_buf())))
|
||||
.unwrap_or_else(|| self.ig_builder.build());
|
||||
Walk {
|
||||
its,
|
||||
it: None,
|
||||
ig_root: ig_root.clone(),
|
||||
ig: ig_root.clone(),
|
||||
max_depth: self.max_depth,
|
||||
max_filesize: self.max_filesize,
|
||||
skip: self.skip.clone(),
|
||||
filter: self.filter.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build matchers for checking paths against ignore files without
|
||||
/// recursively walking the configured roots.
|
||||
///
|
||||
/// The returned matchers use the path-based filtering configuration
|
||||
/// on this builder, including glob overrides, file type selections,
|
||||
/// parent ignore files, `.ignore`, `.gitignore`, global Git
|
||||
/// ignore files, explicitly added ignore files and custom ignore
|
||||
/// file names. For example, ripgrep configures `.rgignore` via
|
||||
/// [`WalkBuilder::add_custom_ignore_filename`]. Minimum and maximum depth
|
||||
/// limits, maximum file size and hidden-file filtering are also applied.
|
||||
/// Other options that only control traversal or require a directory entry,
|
||||
/// such as custom entry predicates, are not applied.
|
||||
///
|
||||
/// One matcher is returned for each configured path, in the same order as
|
||||
/// the paths were added to this builder. Each matcher accepts paths
|
||||
/// relative to its own [`IncrementalIgnore::root`]. The matcher for the
|
||||
/// special `-` path representing standard input always returns a non-match
|
||||
/// for all inputs.
|
||||
///
|
||||
/// Ignore matchers are loaded lazily and cached by directory.
|
||||
/// Thus, the first query may read ignore files from the root and
|
||||
/// its parents, while later queries reuse the compiled matchers.
|
||||
/// Errors encountered while loading ignore files are returned by
|
||||
/// [`IncrementalIgnore::matched_with_errors`]. Once an ignore file has
|
||||
/// been loaded, changes to it are not observed. Build new matchers to
|
||||
/// reload changed ignore files.
|
||||
///
|
||||
/// Matchers built together share the builder's base ignore configuration
|
||||
/// and compiled parent matchers.
|
||||
pub fn build_matchers(&self) -> Vec<IncrementalIgnore> {
|
||||
let ignore = self.build_ignore();
|
||||
let options = IncrementalIgnoreOptions {
|
||||
min_depth: self.min_depth,
|
||||
max_depth: self.max_depth,
|
||||
max_filesize: self.max_filesize,
|
||||
hidden: self.ig_builder.is_hidden(),
|
||||
follow_links: self.follow_links,
|
||||
};
|
||||
self.paths
|
||||
.iter()
|
||||
.map(move |path| {
|
||||
IncrementalIgnore::new(
|
||||
path.clone(),
|
||||
ignore.clone(),
|
||||
options.clone(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Build a new `WalkParallel` iterator.
|
||||
///
|
||||
/// Note that this *doesn't* return something that implements `Iterator`.
|
||||
/// Instead, the returned value must be run with a closure. e.g.,
|
||||
/// `builder.build_parallel().run(|| |path| { println!("{path:?}"); WalkState::Continue })`.
|
||||
pub fn build_parallel(&self) -> WalkParallel {
|
||||
let ig_root = self.build_ignore();
|
||||
let ig_root = self
|
||||
.get_or_set_current_dir()
|
||||
.map(|cwd| self.ig_builder.build_with_cwd(Some(cwd.to_path_buf())))
|
||||
.unwrap_or_else(|| self.ig_builder.build());
|
||||
WalkParallel {
|
||||
paths: self.paths.clone().into_iter(),
|
||||
ig_root,
|
||||
|
|
@ -730,10 +651,7 @@ impl WalkBuilder {
|
|||
/// The default, `None`, imposes no depth restriction.
|
||||
pub fn max_depth(&mut self, depth: Option<usize>) -> &mut WalkBuilder {
|
||||
self.max_depth = depth;
|
||||
if self.min_depth.is_some()
|
||||
&& self.max_depth.is_some()
|
||||
&& self.max_depth < self.min_depth
|
||||
{
|
||||
if self.min_depth.is_some() && self.max_depth.is_some() && self.max_depth < self.min_depth {
|
||||
self.max_depth = self.min_depth;
|
||||
}
|
||||
self
|
||||
|
|
@ -744,10 +662,7 @@ impl WalkBuilder {
|
|||
/// The default, `None`, imposes no minimum depth restriction.
|
||||
pub fn min_depth(&mut self, depth: Option<usize>) -> &mut WalkBuilder {
|
||||
self.min_depth = depth;
|
||||
if self.max_depth.is_some()
|
||||
&& self.min_depth.is_some()
|
||||
&& self.min_depth > self.max_depth
|
||||
{
|
||||
if self.max_depth.is_some() && self.min_depth.is_some() && self.min_depth > self.max_depth {
|
||||
self.min_depth = self.max_depth;
|
||||
}
|
||||
self
|
||||
|
|
@ -790,12 +705,7 @@ impl WalkBuilder {
|
|||
/// An error will also occur if this walker could not get the current
|
||||
/// working directory (and `WalkBuilder::current_dir` isn't set).
|
||||
pub fn add_ignore<P: AsRef<Path>>(&mut self, path: P) -> Option<Error> {
|
||||
// CHANGED: Root the ignore file at `""` instead of the current working
|
||||
// directory. Explicit ignores are scoped to the directory of the
|
||||
// ignore file (see `matched_ignore`), and a root of `""` makes the
|
||||
// rules apply to every walked path regardless of the walk root. This
|
||||
// also avoids depending on the current working directory entirely.
|
||||
//
|
||||
// CHANGED: Dropped this code
|
||||
// let path = path.as_ref();
|
||||
// let Some(cwd) = self.get_or_set_current_dir() else {
|
||||
// let err = std::io::Error::other(format!(
|
||||
|
|
@ -819,11 +729,7 @@ impl WalkBuilder {
|
|||
errs.into_error_option()
|
||||
}
|
||||
|
||||
/// CHANGED: Add a prebuilt Gitignore to the builder.
|
||||
///
|
||||
/// Like the ignore file added via `add_ignore`, these rules are matched
|
||||
/// against the full path of each walked entry, scoped to the `Gitignore`'s
|
||||
/// root path.
|
||||
/// CHANGED: Add a Gitignore to the builder.
|
||||
pub fn add_gitignore(&mut self, gi: Gitignore) {
|
||||
self.ig_builder.add_ignore(gi);
|
||||
}
|
||||
|
|
@ -1076,10 +982,7 @@ impl WalkBuilder {
|
|||
///
|
||||
/// Global gitignore files come from things like a user's git configuration
|
||||
/// or from gitignore files added via [`WalkBuilder::add_ignore`].
|
||||
pub fn current_dir(
|
||||
&mut self,
|
||||
cwd: impl Into<PathBuf>,
|
||||
) -> &mut WalkBuilder {
|
||||
pub fn current_dir(&mut self, cwd: impl Into<PathBuf>) -> &mut WalkBuilder {
|
||||
let cwd = cwd.into();
|
||||
self.ig_builder.current_dir(cwd.clone());
|
||||
if let Err(cwd) = self.global_gitignores_relative_to.set(Ok(cwd)) {
|
||||
|
|
@ -1099,10 +1002,7 @@ impl WalkBuilder {
|
|||
let result = std::env::current_dir().map_err(Arc::new);
|
||||
match result {
|
||||
Ok(ref path) => {
|
||||
log::trace!(
|
||||
"automatically discovered CWD: {}",
|
||||
path.display()
|
||||
);
|
||||
log::trace!("automatically discovered CWD: {}", path.display());
|
||||
}
|
||||
Err(ref err) => {
|
||||
log::debug!(
|
||||
|
|
@ -1116,13 +1016,6 @@ impl WalkBuilder {
|
|||
});
|
||||
result.as_ref().ok().map(|path| &**path)
|
||||
}
|
||||
|
||||
/// Build the root ignore matcher shared by all consumers of this builder.
|
||||
fn build_ignore(&self) -> Ignore {
|
||||
self.get_or_set_current_dir()
|
||||
.map(|cwd| self.ig_builder.build_with_cwd(Some(cwd.to_path_buf())))
|
||||
.unwrap_or_else(|| self.ig_builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk is a recursive directory iterator over file paths in one or more
|
||||
|
|
@ -1136,7 +1029,6 @@ pub struct Walk {
|
|||
it: Option<WalkEventIter>,
|
||||
ig_root: Ignore,
|
||||
ig: Ignore,
|
||||
max_depth: Option<usize>,
|
||||
max_filesize: Option<u64>,
|
||||
skip: Option<Arc<Handle>>,
|
||||
filter: Option<Filter>,
|
||||
|
|
@ -1152,17 +1044,6 @@ impl Walk {
|
|||
WalkBuilder::new(path).build()
|
||||
}
|
||||
|
||||
/// Create a new recursive directory iterator from the sequence of paths
|
||||
/// given.
|
||||
///
|
||||
/// Note that if the provided iterator is empty, then `Walk` is guaranteed
|
||||
/// to yield zero entries.
|
||||
pub fn from_iter<P: AsRef<Path>, I: IntoIterator<Item = P>>(
|
||||
paths: I,
|
||||
) -> Walk {
|
||||
WalkBuilder::from_iter(paths).build()
|
||||
}
|
||||
|
||||
fn skip_entry(&self, ent: &DirEntry) -> Result<bool, Error> {
|
||||
if ent.depth() == 0 {
|
||||
return Ok(false);
|
||||
|
|
@ -1247,17 +1128,12 @@ impl Iterator for Walk {
|
|||
self.it.as_mut().unwrap().it.skip_current_dir();
|
||||
// Still need to push this on the stack because
|
||||
// we'll get a WalkEvent::Exit event for this dir.
|
||||
// Its ignore files cannot apply to any visited entry.
|
||||
let (igtmp, _) =
|
||||
self.ig.add_child_with_entries(ent.path(), &[]);
|
||||
// We don't care if it errors though.
|
||||
let (igtmp, _) = self.ig.add_child(ent.path());
|
||||
self.ig = igtmp;
|
||||
continue;
|
||||
}
|
||||
let (igtmp, err) = if self.max_depth == Some(ent.depth()) {
|
||||
self.ig.add_child_with_entries(ent.path(), &[])
|
||||
} else {
|
||||
self.ig.add_child(ent.path())
|
||||
};
|
||||
let (igtmp, err) = self.ig.add_child(ent.path());
|
||||
self.ig = igtmp;
|
||||
ent.err = err;
|
||||
return Some(Ok(ent));
|
||||
|
|
@ -1299,7 +1175,11 @@ enum WalkEvent {
|
|||
|
||||
impl From<WalkDir> for WalkEventIter {
|
||||
fn from(it: WalkDir) -> WalkEventIter {
|
||||
WalkEventIter { depth: 0, it: it.into_iter(), next: None }
|
||||
WalkEventIter {
|
||||
depth: 0,
|
||||
it: it.into_iter(),
|
||||
next: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1372,9 +1252,7 @@ pub trait ParallelVisitorBuilder<'s> {
|
|||
fn build(&mut self) -> Box<dyn ParallelVisitor + 's>;
|
||||
}
|
||||
|
||||
impl<'a, 's, P: ParallelVisitorBuilder<'s>> ParallelVisitorBuilder<'s>
|
||||
for &'a mut P
|
||||
{
|
||||
impl<'a, 's, P: ParallelVisitorBuilder<'s>> ParallelVisitorBuilder<'s> for &'a mut P {
|
||||
fn build(&mut self) -> Box<dyn ParallelVisitor + 's> {
|
||||
(**self).build()
|
||||
}
|
||||
|
|
@ -1395,17 +1273,14 @@ struct FnBuilder<F> {
|
|||
builder: F,
|
||||
}
|
||||
|
||||
impl<'s, F: FnMut() -> FnVisitor<'s>> ParallelVisitorBuilder<'s>
|
||||
for FnBuilder<F>
|
||||
{
|
||||
impl<'s, F: FnMut() -> FnVisitor<'s>> ParallelVisitorBuilder<'s> for FnBuilder<F> {
|
||||
fn build(&mut self) -> Box<dyn ParallelVisitor + 's> {
|
||||
let visitor = (self.builder)();
|
||||
Box::new(FnVisitorImp { visitor })
|
||||
}
|
||||
}
|
||||
|
||||
type FnVisitor<'s> =
|
||||
Box<dyn FnMut(Result<DirEntry, Error>) -> WalkState + Send + 's>;
|
||||
type FnVisitor<'s> = Box<dyn FnMut(Result<DirEntry, Error>) -> WalkState + Send + 's>;
|
||||
|
||||
struct FnVisitorImp<'s> {
|
||||
visitor: FnVisitor<'s>,
|
||||
|
|
@ -1495,9 +1370,7 @@ impl WalkParallel {
|
|||
}
|
||||
};
|
||||
match DirEntryRaw::from_path(0, path, false) {
|
||||
Ok(dent) => {
|
||||
(DirEntry::new_raw(dent, None), root_device)
|
||||
}
|
||||
Ok(dent) => (DirEntry::new_raw(dent, None), root_device),
|
||||
Err(err) => {
|
||||
if visitor.visit(Err(err)).is_quit() {
|
||||
return;
|
||||
|
|
@ -1521,28 +1394,21 @@ impl WalkParallel {
|
|||
let quit_now = Arc::new(AtomicBool::new(false));
|
||||
let active_workers = Arc::new(AtomicUsize::new(threads));
|
||||
let stacks = Stack::new_for_each_thread(threads, stack);
|
||||
// Collect all of the workers first. In the case that
|
||||
// `builder.build()` panics, we want that to happen and
|
||||
// propagate before we actually start to run any of the
|
||||
// workers.
|
||||
let workers: Vec<_> = stacks
|
||||
.into_iter()
|
||||
.map(|stack| Worker {
|
||||
visitor: builder.build(),
|
||||
stack,
|
||||
quit_now: quit_now.clone(),
|
||||
active_workers: active_workers.clone(),
|
||||
max_depth: self.max_depth,
|
||||
min_depth: self.min_depth,
|
||||
max_filesize: self.max_filesize,
|
||||
follow_links: self.follow_links,
|
||||
skip: self.skip.clone(),
|
||||
filter: self.filter.clone(),
|
||||
})
|
||||
.collect();
|
||||
std::thread::scope(|s| {
|
||||
let handles: Vec<_> = workers
|
||||
let handles: Vec<_> = stacks
|
||||
.into_iter()
|
||||
.map(|stack| Worker {
|
||||
visitor: builder.build(),
|
||||
stack,
|
||||
quit_now: quit_now.clone(),
|
||||
active_workers: active_workers.clone(),
|
||||
max_depth: self.max_depth,
|
||||
min_depth: self.min_depth,
|
||||
max_filesize: self.max_filesize,
|
||||
follow_links: self.follow_links,
|
||||
skip: self.skip.clone(),
|
||||
filter: self.filter.clone(),
|
||||
})
|
||||
.map(|worker| s.spawn(|| worker.run()))
|
||||
.collect();
|
||||
for handle in handles {
|
||||
|
|
@ -1553,7 +1419,9 @@ impl WalkParallel {
|
|||
|
||||
fn threads(&self) -> usize {
|
||||
if self.threads == 0 {
|
||||
std::thread::available_parallelism().map_or(1, |n| n.get()).min(12)
|
||||
std::thread::available_parallelism()
|
||||
.map_or(1, |n| n.get())
|
||||
.min(12)
|
||||
} else {
|
||||
self.threads
|
||||
}
|
||||
|
|
@ -1584,12 +1452,6 @@ struct Work {
|
|||
root_device: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ReadDirResult {
|
||||
entries: Vec<fs::DirEntry>,
|
||||
errors: Vec<Error>,
|
||||
}
|
||||
|
||||
impl Work {
|
||||
/// Returns true if and only if this work item is a directory.
|
||||
fn is_dir(&self) -> bool {
|
||||
|
|
@ -1616,13 +1478,6 @@ impl Work {
|
|||
err
|
||||
}
|
||||
|
||||
/// Adds ignore rules for this directory without reading its contents.
|
||||
fn add_ignore(&mut self) {
|
||||
let (ig, err) = self.ignore.add_child(self.dent.path());
|
||||
self.ignore = ig;
|
||||
self.dent.err = err;
|
||||
}
|
||||
|
||||
/// Reads the directory contents of this work item and adds ignore
|
||||
/// rules for this directory.
|
||||
///
|
||||
|
|
@ -1630,7 +1485,7 @@ impl Work {
|
|||
/// an error is returned. If there was a problem reading the ignore
|
||||
/// rules for this directory, then the error is attached to this
|
||||
/// work item's directory entry.
|
||||
fn read_dir(&mut self) -> Result<ReadDirResult, Error> {
|
||||
fn read_dir(&mut self) -> Result<fs::ReadDir, Error> {
|
||||
let readdir = match fs::read_dir(self.dent.path()) {
|
||||
Ok(readdir) => readdir,
|
||||
Err(err) => {
|
||||
|
|
@ -1640,24 +1495,10 @@ impl Work {
|
|||
return Err(err);
|
||||
}
|
||||
};
|
||||
// Actually descend into the directory and read its contents
|
||||
let mut result = ReadDirResult::default();
|
||||
for entry in readdir {
|
||||
match entry {
|
||||
Ok(entry) => result.entries.push(entry),
|
||||
Err(err) => result.errors.push(
|
||||
Error::from(err)
|
||||
.with_path(self.dent.path())
|
||||
.with_depth(self.dent.depth() + 1),
|
||||
),
|
||||
}
|
||||
}
|
||||
let (ig, err) = self
|
||||
.ignore
|
||||
.add_child_with_entries(self.dent.path(), &result.entries);
|
||||
let (ig, err) = self.ignore.add_child(self.dent.path());
|
||||
self.ignore = ig;
|
||||
self.dent.err = err;
|
||||
Ok(result)
|
||||
Ok(readdir)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1681,11 +1522,11 @@ impl Stack {
|
|||
// breadth-first. We do depth-first because a breadth first traversal
|
||||
// on wide directories with a lot of gitignores is disastrous (for
|
||||
// example, searching a directory tree containing all of crates.io).
|
||||
let deques: Vec<Deque<Message>> =
|
||||
std::iter::repeat_with(Deque::new_lifo).take(threads).collect();
|
||||
let stealers = Arc::<[Stealer<Message>]>::from(
|
||||
deques.iter().map(Deque::stealer).collect::<Vec<_>>(),
|
||||
);
|
||||
let deques: Vec<Deque<Message>> = std::iter::repeat_with(Deque::new_lifo)
|
||||
.take(threads)
|
||||
.collect();
|
||||
let stealers =
|
||||
Arc::<[Stealer<Message>]>::from(deques.iter().map(Deque::stealer).collect::<Vec<_>>());
|
||||
let stacks: Vec<Stack> = deques
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
|
|
@ -1827,13 +1668,8 @@ impl<'s> Worker<'s> {
|
|||
// have sufficient read permissions to list the directory.
|
||||
// In that case we still want to provide the closure with a valid
|
||||
// entry before passing the error value.
|
||||
let readdir = work.read_dir();
|
||||
let depth = work.dent.depth();
|
||||
let readdir = if descend && self.max_depth.is_none_or(|m| depth < m) {
|
||||
Some(work.read_dir())
|
||||
} else {
|
||||
work.add_ignore();
|
||||
None
|
||||
};
|
||||
if should_visit {
|
||||
let state = self.visitor.visit(Ok(work.dent));
|
||||
if !state.is_continue() {
|
||||
|
|
@ -1844,10 +1680,6 @@ impl<'s> Worker<'s> {
|
|||
return WalkState::Skip;
|
||||
}
|
||||
|
||||
let readdir = match readdir {
|
||||
Some(readdir) => readdir,
|
||||
None => return WalkState::Skip,
|
||||
};
|
||||
let readdir = match readdir {
|
||||
Ok(readdir) => readdir,
|
||||
Err(err) => {
|
||||
|
|
@ -1855,19 +1687,11 @@ impl<'s> Worker<'s> {
|
|||
}
|
||||
};
|
||||
|
||||
for result in readdir.entries {
|
||||
let state = self.generate_work(
|
||||
&work.ignore,
|
||||
depth + 1,
|
||||
work.root_device,
|
||||
result,
|
||||
);
|
||||
if state.is_quit() {
|
||||
return state;
|
||||
}
|
||||
if self.max_depth.map_or(false, |max| depth >= max) {
|
||||
return WalkState::Skip;
|
||||
}
|
||||
for err in readdir.errors {
|
||||
let state = self.visitor.visit(Err(err));
|
||||
for result in readdir {
|
||||
let state = self.generate_work(&work.ignore, depth + 1, work.root_device, result);
|
||||
if state.is_quit() {
|
||||
return state;
|
||||
}
|
||||
|
|
@ -1893,8 +1717,14 @@ impl<'s> Worker<'s> {
|
|||
ig: &Ignore,
|
||||
depth: usize,
|
||||
root_device: Option<u64>,
|
||||
fs_dent: fs::DirEntry,
|
||||
result: Result<fs::DirEntry, io::Error>,
|
||||
) -> WalkState {
|
||||
let fs_dent = match result {
|
||||
Ok(fs_dent) => fs_dent,
|
||||
Err(err) => {
|
||||
return self.visitor.visit(Err(Error::from(err).with_depth(depth)));
|
||||
}
|
||||
};
|
||||
let mut dent = match DirEntryRaw::from_entry(depth, &fs_dent) {
|
||||
Ok(dent) => DirEntry::new_raw(dent, None),
|
||||
Err(err) => {
|
||||
|
|
@ -1930,24 +1760,26 @@ impl<'s> Worker<'s> {
|
|||
return WalkState::Continue;
|
||||
}
|
||||
}
|
||||
let should_skip_filesize =
|
||||
if self.max_filesize.is_some() && !dent.is_dir() {
|
||||
skip_filesize(
|
||||
self.max_filesize.unwrap(),
|
||||
dent.path(),
|
||||
&dent.metadata().ok(),
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let should_skip_filtered =
|
||||
if let Some(Filter(predicate)) = &self.filter {
|
||||
!predicate(&dent)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let should_skip_filesize = if self.max_filesize.is_some() && !dent.is_dir() {
|
||||
skip_filesize(
|
||||
self.max_filesize.unwrap(),
|
||||
dent.path(),
|
||||
&dent.metadata().ok(),
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let should_skip_filtered = if let Some(Filter(predicate)) = &self.filter {
|
||||
!predicate(&dent)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !should_skip_filesize && !should_skip_filtered {
|
||||
self.send(Work { dent, ignore: ig.clone(), root_device });
|
||||
self.send(Work {
|
||||
dent,
|
||||
ignore: ig.clone(),
|
||||
root_device,
|
||||
});
|
||||
}
|
||||
WalkState::Continue
|
||||
}
|
||||
|
|
@ -1988,9 +1820,6 @@ impl<'s> Worker<'s> {
|
|||
}
|
||||
// Wait for next `Work` or `Quit` message.
|
||||
loop {
|
||||
if self.is_quit_now() {
|
||||
return None;
|
||||
}
|
||||
if let Some(v) = self.recv() {
|
||||
self.activate_worker();
|
||||
value = Some(v);
|
||||
|
|
@ -2044,25 +1873,24 @@ impl<'s> Worker<'s> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'s> Drop for Worker<'s> {
|
||||
fn drop(&mut self) {
|
||||
if std::thread::panicking() {
|
||||
self.quit_now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_symlink_loop(
|
||||
ig_parent: &Ignore,
|
||||
child_path: &Path,
|
||||
child_depth: usize,
|
||||
) -> Result<(), Error> {
|
||||
let hchild = Handle::from_path(child_path).map_err(|err| {
|
||||
Error::from(err).with_path(child_path).with_depth(child_depth)
|
||||
Error::from(err)
|
||||
.with_path(child_path)
|
||||
.with_depth(child_depth)
|
||||
})?;
|
||||
for ig in ig_parent.parents().take_while(|ig| !ig.is_absolute_parent()) {
|
||||
for ig in ig_parent
|
||||
.parents()
|
||||
.take_while(|ig| !ig.is_absolute_parent())
|
||||
{
|
||||
let h = Handle::from_path(ig.path()).map_err(|err| {
|
||||
Error::from(err).with_path(child_path).with_depth(child_depth)
|
||||
Error::from(err)
|
||||
.with_path(child_path)
|
||||
.with_depth(child_depth)
|
||||
})?;
|
||||
if hchild == h {
|
||||
return Err(Error::Loop {
|
||||
|
|
@ -2077,11 +1905,7 @@ fn check_symlink_loop(
|
|||
|
||||
// Before calling this function, make sure that you ensure that is really
|
||||
// necessary as the arguments imply a file stat.
|
||||
fn skip_filesize(
|
||||
max_filesize: u64,
|
||||
path: &Path,
|
||||
ent: &Option<Metadata>,
|
||||
) -> bool {
|
||||
fn skip_filesize(max_filesize: u64, path: &Path, ent: &Option<Metadata>) -> bool {
|
||||
let filesize = match *ent {
|
||||
Some(ref md) => Some(md.len()),
|
||||
None => None,
|
||||
|
|
@ -2153,9 +1977,9 @@ fn path_equals(dent: &DirEntry, handle: &Handle) -> Result<bool, Error> {
|
|||
if dent.is_stdin() || never_equal(dent, handle) {
|
||||
return Ok(false);
|
||||
}
|
||||
Handle::from_path(dent.path()).map(|h| &h == handle).map_err(|err| {
|
||||
Error::Io(err).with_depth(dent.depth()).with_path(dent.path())
|
||||
})
|
||||
Handle::from_path(dent.path())
|
||||
.map(|h| &h == handle)
|
||||
.map_err(|err| Error::Io(err).with_path(dent.path()))
|
||||
}
|
||||
|
||||
/// Returns true if the given walkdir entry corresponds to a directory.
|
||||
|
|
@ -2173,14 +1997,16 @@ fn walkdir_is_dir(dent: &walkdir::DirEntry) -> bool {
|
|||
if !dent.file_type().is_symlink() || dent.depth() > 0 {
|
||||
return false;
|
||||
}
|
||||
dent.path().metadata().ok().map_or(false, |md| md.file_type().is_dir())
|
||||
dent.path()
|
||||
.metadata()
|
||||
.ok()
|
||||
.map_or(false, |md| md.file_type().is_dir())
|
||||
}
|
||||
|
||||
/// Returns true if and only if the given path is on the same device as the
|
||||
/// given root device.
|
||||
fn is_same_file_system(root_device: u64, path: &Path) -> Result<bool, Error> {
|
||||
let dent_device =
|
||||
device_num(path).map_err(|err| Error::Io(err).with_path(path))?;
|
||||
let dent_device = device_num(path).map_err(|err| Error::Io(err).with_path(path))?;
|
||||
Ok(root_device == dent_device)
|
||||
}
|
||||
|
||||
|
|
@ -2239,7 +2065,11 @@ mod tests {
|
|||
}
|
||||
|
||||
fn normal_path(unix: &str) -> String {
|
||||
if cfg!(windows) { unix.replace("\\", "/") } else { unix.to_string() }
|
||||
if cfg!(windows) {
|
||||
unix.replace("\\", "/")
|
||||
} else {
|
||||
unix.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_collect(prefix: &Path, builder: &WalkBuilder) -> Vec<String> {
|
||||
|
|
@ -2259,10 +2089,7 @@ mod tests {
|
|||
paths
|
||||
}
|
||||
|
||||
fn walk_collect_parallel(
|
||||
prefix: &Path,
|
||||
builder: &WalkBuilder,
|
||||
) -> Vec<String> {
|
||||
fn walk_collect_parallel(prefix: &Path, builder: &WalkBuilder) -> Vec<String> {
|
||||
let mut paths = vec![];
|
||||
for dent in walk_collect_entries_parallel(builder) {
|
||||
let path = dent.path().strip_prefix(prefix).unwrap();
|
||||
|
|
@ -2451,27 +2278,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_depth_does_not_load_unreachable_ignore_files() {
|
||||
let td = tmpdir();
|
||||
let leaf = td.path().join("leaf");
|
||||
mkdirp(&leaf);
|
||||
wfile(leaf.join(".ignore"), "{invalid\n");
|
||||
|
||||
let mut builder = WalkBuilder::new(td.path());
|
||||
builder.max_depth(Some(1));
|
||||
let entry = builder
|
||||
.build()
|
||||
.find_map(|result| {
|
||||
let entry = result.unwrap();
|
||||
(entry.path() == leaf).then_some(entry)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(entry.error().is_none());
|
||||
assert_paths(td.path(), &builder, &["leaf"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_depth() {
|
||||
let td = tmpdir();
|
||||
|
|
@ -2582,9 +2388,7 @@ mod tests {
|
|||
assert_eq!(1, dents.len());
|
||||
assert!(!dents[0].path_is_symlink());
|
||||
|
||||
let dents = walk_collect_entries_parallel(&WalkBuilder::new(
|
||||
td.path().join("foo"),
|
||||
));
|
||||
let dents = walk_collect_entries_parallel(&WalkBuilder::new(td.path().join("foo")));
|
||||
assert_eq!(1, dents.len());
|
||||
assert!(!dents[0].path_is_symlink());
|
||||
}
|
||||
|
|
@ -2670,88 +2474,8 @@ mod tests {
|
|||
|
||||
assert_paths(
|
||||
td.path(),
|
||||
&WalkBuilder::new(td.path())
|
||||
.filter_entry(|entry| entry.file_name() != OsStr::new("a")),
|
||||
&WalkBuilder::new(td.path()).filter_entry(|entry| entry.file_name() != OsStr::new("a")),
|
||||
&["x", "x/y", "x/y/foo"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let td = tmpdir();
|
||||
assert_paths(td.path(), &WalkBuilder::empty(), &[]);
|
||||
|
||||
let empty_paths: Vec<&OsStr> = Vec::new();
|
||||
assert_paths(td.path(), &WalkBuilder::from_iter(empty_paths), &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_iter() {
|
||||
let td = tmpdir();
|
||||
mkdirp(td.path().join("a/b/c"));
|
||||
mkdirp(td.path().join("d/e/f"));
|
||||
mkdirp(td.path().join("x/y"));
|
||||
wfile(td.path().join("a/b/foo"), "");
|
||||
wfile(td.path().join("d/e/f/foo"), "");
|
||||
wfile(td.path().join("x/y/foo"), "");
|
||||
|
||||
let paths = vec![
|
||||
td.path().join("a"),
|
||||
td.path().join("d"),
|
||||
td.path().join("x"),
|
||||
];
|
||||
|
||||
assert_paths(
|
||||
td.path(),
|
||||
&WalkBuilder::from_iter(paths),
|
||||
&[
|
||||
"x",
|
||||
"x/y",
|
||||
"x/y/foo",
|
||||
"d",
|
||||
"d/e",
|
||||
"d/e/f",
|
||||
"d/e/f/foo",
|
||||
"a",
|
||||
"a/b",
|
||||
"a/b/foo",
|
||||
"a/b/c",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// This should always panic and never hang.
|
||||
//
|
||||
// Ref: https://github.com/BurntSushi/ripgrep/issues/3009
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panic_in_parallel() {
|
||||
let td = tmpdir();
|
||||
wfile(td.path().join("foo.txt"), "");
|
||||
|
||||
WalkBuilder::new(td.path())
|
||||
.threads(40)
|
||||
.build_parallel()
|
||||
.run(|| Box::new(|_| panic!("oops!")));
|
||||
}
|
||||
|
||||
// This should always panic and never hang. The first call to the visitor
|
||||
// builder is used while processing the root paths. Previously, a panic on
|
||||
// the third call occurred after the first worker had already been spawned,
|
||||
// leaving it waiting indefinitely for workers that were never created.
|
||||
#[test]
|
||||
#[should_panic(expected = "builder panic")]
|
||||
fn panic_in_parallel_builder() {
|
||||
let td = tmpdir();
|
||||
wfile(td.path().join("foo.txt"), "");
|
||||
|
||||
let mut builds = 0;
|
||||
WalkBuilder::new(td.path()).threads(2).build_parallel().run(|| {
|
||||
builds += 1;
|
||||
if builds == 3 {
|
||||
panic!("builder panic");
|
||||
}
|
||||
Box::new(|_| WalkState::Continue)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ use std::path::Path;
|
|||
|
||||
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
|
||||
const IGNORE_FILE: &'static str =
|
||||
"tests/gitignore_matched_path_or_any_parents_tests.gitignore";
|
||||
const IGNORE_FILE: &'static str = "tests/gitignore_matched_path_or_any_parents_tests.gitignore";
|
||||
|
||||
fn get_gitignore() -> Gitignore {
|
||||
let mut builder = GitignoreBuilder::new("ROOT");
|
||||
|
|
@ -24,9 +23,7 @@ fn test_path_should_be_under_root() {
|
|||
#[test]
|
||||
fn test_files_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
let m = |path: &str| gitignore.matched_path_or_any_parents(Path::new(path), false);
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/file_root_00").is_ignore());
|
||||
|
|
@ -56,9 +53,7 @@ fn test_files_in_root() {
|
|||
#[test]
|
||||
fn test_files_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
let m = |path: &str| gitignore.matched_path_or_any_parents(Path::new(path), false);
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/parent_dir/file_deep_00").is_ignore());
|
||||
|
|
@ -88,9 +83,8 @@ fn test_files_in_deep() {
|
|||
#[test]
|
||||
fn test_dirs_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
let m =
|
||||
|path: &str, is_dir: bool| gitignore.matched_path_or_any_parents(Path::new(path), is_dir);
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/dir_root_00", true).is_ignore());
|
||||
|
|
@ -192,25 +186,20 @@ fn test_dirs_in_root() {
|
|||
#[test]
|
||||
fn test_dirs_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
let m =
|
||||
|path: &str, is_dir: bool| gitignore.matched_path_or_any_parents(Path::new(path), is_dir);
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_00/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/child_dir/file", false).is_ignore());
|
||||
|
||||
// 01
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_01/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/child_dir/file", false).is_ignore());
|
||||
|
||||
// 02
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02", true).is_none());
|
||||
|
|
@ -252,67 +241,51 @@ fn test_dirs_in_deep() {
|
|||
assert!(m("ROOT/parent_dir/dir_deep_20", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_20/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/child_dir/file", false).is_ignore());
|
||||
|
||||
// 21
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_21/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/child_dir/file", false).is_ignore());
|
||||
|
||||
// 22
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_22/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/child_dir/file", false).is_ignore());
|
||||
|
||||
// 23
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_23/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/child_dir/file", false).is_ignore());
|
||||
|
||||
// 30
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_30/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/child_dir/file", false).is_ignore());
|
||||
|
||||
// 31
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_31/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/child_dir/file", false).is_ignore());
|
||||
|
||||
// 32
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_32/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/child_dir/file", false).is_ignore());
|
||||
|
||||
// 33
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_33/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/child_dir/file", false).is_ignore());
|
||||
}
|
||||
|
|
|
|||
1
crates/node/.gitignore
vendored
1
crates/node/.gitignore
vendored
|
|
@ -202,6 +202,5 @@ index.js
|
|||
browser.js
|
||||
tailwindcss-oxide.wasi-browser.js
|
||||
tailwindcss-oxide.wasi.cjs
|
||||
tailwindcss-oxide.wasi.d.cts
|
||||
wasi-worker-browser.mjs
|
||||
wasi-worker.mjs
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ crate-type = ["cdylib"]
|
|||
|
||||
[dependencies]
|
||||
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
|
||||
napi = { version = "3.11.0", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "3.6.0"
|
||||
napi = { version = "3.3.0", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "3.2.5"
|
||||
tailwindcss-oxide = { path = "../oxide" }
|
||||
rayon = "1.12.0"
|
||||
rayon = "1.10.0"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.3.2"
|
||||
napi-build = "2.2.3"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-android-arm-eabi",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-android-arm64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-arm64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-x64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-freebsd-x64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-gnu",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-musl",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-gnu",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-musl",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
|
|
|
|||
1
crates/node/npm/wasm32-wasi/.npmrc
Normal file
1
crates/node/npm/wasm32-wasi/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
node-linker=hoisted
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# `@tailwindcss/oxide-wasm32-wasi`
|
||||
|
||||
This is the **wasm32-wasip1-threads** build of `@tailwindcss/oxide`
|
||||
This is the **wasm32-wasip1-threads** binary for `@tailwindcss/oxide`
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-wasm32-wasi",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"main": "tailwindcss-oxide.wasi.cjs",
|
||||
"files": [
|
||||
"tailwindcss-oxide.wasm32-wasi.wasm",
|
||||
|
|
@ -11,7 +14,7 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"provenance": true,
|
||||
|
|
@ -24,12 +27,12 @@
|
|||
},
|
||||
"browser": "tailwindcss-oxide.wasi-browser.js",
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.2.2",
|
||||
"@emnapi/core": "^1.11.3",
|
||||
"@emnapi/runtime": "^1.11.3",
|
||||
"@tybys/wasm-util": "^0.10.3",
|
||||
"@emnapi/wasi-threads": "^1.2.3",
|
||||
"tslib": "^2.8.1"
|
||||
"@napi-rs/wasm-runtime": "^1.0.7",
|
||||
"@emnapi/core": "^1.6.0",
|
||||
"@emnapi/runtime": "^1.6.0",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
|
|
|
|||
75
crates/node/npm/wasm32-wasi/pnpm-lock.yaml
generated
Normal file
75
crates/node/npm/wasm32-wasi/pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@emnapi/core':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0
|
||||
'@emnapi/runtime':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0
|
||||
'@emnapi/wasi-threads':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
'@napi-rs/wasm-runtime':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
'@tybys/wasm-util':
|
||||
specifier: ^0.10.1
|
||||
version: 0.10.1
|
||||
tslib:
|
||||
specifier: ^2.4.0
|
||||
version: 2.8.1
|
||||
|
||||
packages:
|
||||
|
||||
'@emnapi/core@1.5.0':
|
||||
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
|
||||
|
||||
'@emnapi/runtime@1.5.0':
|
||||
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.0.5':
|
||||
resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==}
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@emnapi/core@1.5.0':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@emnapi/runtime@1.5.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@napi-rs/wasm-runtime@1.0.5':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.5.0
|
||||
'@emnapi/runtime': 1.5.0
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-win32-arm64-msvc",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-win32-x64-msvc",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.16",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -33,14 +33,12 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@emnapi/core": "1.11.3",
|
||||
"@emnapi/runtime": "1.11.3",
|
||||
"@napi-rs/cli": "3.7.4",
|
||||
"@napi-rs/wasm-runtime": "^1.2.2",
|
||||
"emnapi": "1.11.3"
|
||||
"@napi-rs/cli": "^3.4.1",
|
||||
"@napi-rs/wasm-runtime": "^1.0.7",
|
||||
"emnapi": "1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
|
|
|
|||
|
|
@ -165,11 +165,6 @@ impl Scanner {
|
|||
self.scanner.get_files()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn scanned_files(&self) -> Vec<String> {
|
||||
self.scanner.get_scanned_files()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn globs(&mut self) -> Vec<GlobEntry> {
|
||||
self
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ ignore = { path = "../ignore" }
|
|||
regex = "1.11.1"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.48.0"
|
||||
tempfile = "3.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
unicode-width = "0.2.0"
|
||||
|
|
|
|||
|
|
@ -1,49 +1,44 @@
|
|||
use std::{ascii::escape_default, fmt::Display};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cursor<'a> {
|
||||
// The input we're scanning
|
||||
pub input: &'a [u8],
|
||||
|
||||
// The location of the cursor in the input
|
||||
pub pos: usize,
|
||||
|
||||
/// Is the cursor at the start of the input
|
||||
pub at_start: bool,
|
||||
|
||||
/// Is the cursor at the end of the input
|
||||
pub at_end: bool,
|
||||
|
||||
/// The previously consumed character
|
||||
/// If `at_start` is true, this will be NUL
|
||||
pub prev: u8,
|
||||
|
||||
/// The current character
|
||||
pub curr: u8,
|
||||
|
||||
/// The upcoming character (if any)
|
||||
/// If `at_end` is true, this will be NUL
|
||||
pub next: u8,
|
||||
}
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
#[inline(always)]
|
||||
pub fn new(input: &'a [u8]) -> Self {
|
||||
Self { input, pos: 0 }
|
||||
}
|
||||
|
||||
/// The current byte at `pos`, or 0x00 if past the end.
|
||||
#[inline(always)]
|
||||
pub fn curr(&self) -> u8 {
|
||||
if self.pos < self.input.len() {
|
||||
unsafe { *self.input.get_unchecked(self.pos) }
|
||||
} else {
|
||||
0x00
|
||||
}
|
||||
}
|
||||
|
||||
/// The next byte at `pos + 1`, or 0x00 if past the end.
|
||||
#[inline(always)]
|
||||
pub fn next(&self) -> u8 {
|
||||
let next_pos = self.pos + 1;
|
||||
if next_pos < self.input.len() {
|
||||
unsafe { *self.input.get_unchecked(next_pos) }
|
||||
} else {
|
||||
0x00
|
||||
}
|
||||
}
|
||||
|
||||
/// The previous byte at `pos - 1`, or 0x00 if at the start.
|
||||
#[inline(always)]
|
||||
pub fn prev(&self) -> u8 {
|
||||
if self.pos > 0 {
|
||||
unsafe { *self.input.get_unchecked(self.pos - 1) }
|
||||
} else {
|
||||
0x00
|
||||
}
|
||||
let mut cursor = Self {
|
||||
input,
|
||||
pos: 0,
|
||||
at_start: true,
|
||||
at_end: false,
|
||||
prev: 0x00,
|
||||
curr: 0x00,
|
||||
next: 0x00,
|
||||
};
|
||||
cursor.move_to(0);
|
||||
cursor
|
||||
}
|
||||
|
||||
pub fn advance_by(&mut self, amount: usize) {
|
||||
|
|
@ -53,15 +48,38 @@ impl<'a> Cursor<'a> {
|
|||
#[inline(always)]
|
||||
pub fn advance(&mut self) {
|
||||
self.pos += 1;
|
||||
|
||||
self.prev = self.curr;
|
||||
self.curr = self.next;
|
||||
self.next = *self
|
||||
.input
|
||||
.get(self.pos.saturating_add(1))
|
||||
.unwrap_or(&0x00u8);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn advance_twice(&mut self) {
|
||||
self.pos += 2;
|
||||
|
||||
self.prev = self.next;
|
||||
self.curr = *self.input.get(self.pos).unwrap_or(&0x00u8);
|
||||
self.next = *self
|
||||
.input
|
||||
.get(self.pos.saturating_add(1))
|
||||
.unwrap_or(&0x00u8);
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, pos: usize) {
|
||||
self.pos = pos.min(self.input.len());
|
||||
let len = self.input.len();
|
||||
let pos = pos.clamp(0, len);
|
||||
|
||||
self.pos = pos;
|
||||
self.at_start = pos == 0;
|
||||
self.at_end = pos + 1 >= len;
|
||||
|
||||
self.prev = *self.input.get(pos.wrapping_sub(1)).unwrap_or(&0x00u8);
|
||||
self.curr = *self.input.get(pos).unwrap_or(&0x00u8);
|
||||
self.next = *self.input.get(pos.saturating_add(1)).unwrap_or(&0x00u8);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,9 +90,9 @@ impl Display for Cursor<'_> {
|
|||
let pos = format!("{: >len_count$}", self.pos, len_count = len.len());
|
||||
write!(f, "{}/{} ", pos, len)?;
|
||||
|
||||
if self.pos == 0 {
|
||||
if self.at_start {
|
||||
write!(f, "S ")?;
|
||||
} else if self.pos + 1 >= self.input.len() {
|
||||
} else if self.at_end {
|
||||
write!(f, "E ")?;
|
||||
} else {
|
||||
write!(f, "M ")?;
|
||||
|
|
@ -91,9 +109,9 @@ impl Display for Cursor<'_> {
|
|||
write!(
|
||||
f,
|
||||
"[{} {} {}]",
|
||||
to_str(self.prev()),
|
||||
to_str(self.curr()),
|
||||
to_str(self.next())
|
||||
to_str(self.prev),
|
||||
to_str(self.curr),
|
||||
to_str(self.next)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -107,28 +125,36 @@ mod test {
|
|||
fn test_cursor() {
|
||||
let mut cursor = Cursor::new(b"hello world");
|
||||
assert_eq!(cursor.pos, 0);
|
||||
assert_eq!(cursor.prev(), 0x00);
|
||||
assert_eq!(cursor.curr(), b'h');
|
||||
assert_eq!(cursor.next(), b'e');
|
||||
assert!(cursor.at_start);
|
||||
assert!(!cursor.at_end);
|
||||
assert_eq!(cursor.prev, 0x00);
|
||||
assert_eq!(cursor.curr, b'h');
|
||||
assert_eq!(cursor.next, b'e');
|
||||
|
||||
cursor.advance_by(1);
|
||||
assert_eq!(cursor.pos, 1);
|
||||
assert_eq!(cursor.prev(), b'h');
|
||||
assert_eq!(cursor.curr(), b'e');
|
||||
assert_eq!(cursor.next(), b'l');
|
||||
assert!(!cursor.at_start);
|
||||
assert!(!cursor.at_end);
|
||||
assert_eq!(cursor.prev, b'h');
|
||||
assert_eq!(cursor.curr, b'e');
|
||||
assert_eq!(cursor.next, b'l');
|
||||
|
||||
// Advancing too far should stop at the end
|
||||
cursor.advance_by(10);
|
||||
assert_eq!(cursor.pos, 11);
|
||||
assert_eq!(cursor.prev(), b'd');
|
||||
assert_eq!(cursor.curr(), 0x00);
|
||||
assert_eq!(cursor.next(), 0x00);
|
||||
assert!(!cursor.at_start);
|
||||
assert!(cursor.at_end);
|
||||
assert_eq!(cursor.prev, b'd');
|
||||
assert_eq!(cursor.curr, 0x00);
|
||||
assert_eq!(cursor.next, 0x00);
|
||||
|
||||
// Can't advance past the end
|
||||
cursor.advance_by(1);
|
||||
assert_eq!(cursor.pos, 11);
|
||||
assert_eq!(cursor.prev(), b'd');
|
||||
assert_eq!(cursor.curr(), 0x00);
|
||||
assert_eq!(cursor.next(), 0x00);
|
||||
assert!(!cursor.at_start);
|
||||
assert!(cursor.at_end);
|
||||
assert_eq!(cursor.prev, b'd');
|
||||
assert_eq!(cursor.curr, 0x00);
|
||||
assert_eq!(cursor.next, 0x00);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ impl Machine for ArbitraryPropertyMachine<IdleState> {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Start of an arbitrary property
|
||||
Class::OpenBracket => {
|
||||
self.start_pos = cursor.pos;
|
||||
|
|
@ -97,8 +97,8 @@ impl Machine for ArbitraryPropertyMachine<ParsingPropertyState> {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Dash => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of a CSS variable
|
||||
//
|
||||
// E.g.: `[--my-color:red]`
|
||||
|
|
@ -137,7 +137,7 @@ impl ArbitraryPropertyMachine<ParsingPropertyState> {
|
|||
fn parse_property_variable(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.css_variable_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// End of the CSS variable, must be followed by a `:`
|
||||
//
|
||||
// E.g.: `[--my-color:red]`
|
||||
|
|
@ -165,8 +165,8 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
|||
let len = cursor.input.len();
|
||||
let start_of_value_pos = cursor.pos;
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Escape => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
//
|
||||
// E.g.: `[color:var(--my-\ color)]`
|
||||
|
|
@ -181,7 +181,7 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
|||
},
|
||||
|
||||
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
|
||||
if !self.bracket_stack.push(cursor.curr()) {
|
||||
if !self.bracket_stack.push(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -190,7 +190,7 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
|||
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
|
||||
if !self.bracket_stack.is_empty() =>
|
||||
{
|
||||
if !self.bracket_stack.pop(cursor.curr()) {
|
||||
if !self.bracket_stack.pop(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -227,16 +227,10 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
|||
Class::Slash if start_of_value_pos == cursor.pos => return self.restart(),
|
||||
|
||||
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
|
||||
Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
|
||||
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
|
||||
return self.restart()
|
||||
}
|
||||
|
||||
// An `!` at the top-level is invalid. We don't allow things to end with
|
||||
// `!important` either as we have dedicated syntax for this.
|
||||
Class::Exclamation if self.bracket_stack.is_empty() => {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
|
|
@ -299,9 +293,6 @@ enum Class {
|
|||
#[bytes(b'/')]
|
||||
Slash,
|
||||
|
||||
#[bytes(b'!')]
|
||||
Exclamation,
|
||||
|
||||
#[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')]
|
||||
Whitespace,
|
||||
|
||||
|
|
@ -378,9 +369,6 @@ mod tests {
|
|||
"[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]",
|
||||
vec!["[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]"],
|
||||
),
|
||||
// A property containing `!` at the top-level is invalid
|
||||
("[color:red!]", vec![]),
|
||||
("[color:red!important]", vec![]),
|
||||
] {
|
||||
for wrapper in [
|
||||
// No wrapper
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl Machine for ArbitraryValueMachine {
|
|||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
// An arbitrary value must start with an open bracket
|
||||
if Class::OpenBracket != cursor.curr().into() {
|
||||
if Class::OpenBracket != cursor.curr.into() {
|
||||
return MachineState::Idle;
|
||||
}
|
||||
|
||||
|
|
@ -42,8 +42,8 @@ impl Machine for ArbitraryValueMachine {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Escape => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
//
|
||||
// E.g.: `[color:var(--my-\ color)]`
|
||||
|
|
@ -61,7 +61,7 @@ impl Machine for ArbitraryValueMachine {
|
|||
},
|
||||
|
||||
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
|
||||
if !self.bracket_stack.push(cursor.curr()) {
|
||||
if !self.bracket_stack.push(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -70,7 +70,7 @@ impl Machine for ArbitraryValueMachine {
|
|||
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
|
||||
if !self.bracket_stack.is_empty() =>
|
||||
{
|
||||
if !self.bracket_stack.pop(cursor.curr()) {
|
||||
if !self.bracket_stack.pop(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -96,7 +96,7 @@ impl Machine for ArbitraryValueMachine {
|
|||
Class::Whitespace => return self.restart(),
|
||||
|
||||
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
|
||||
Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
|
||||
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
|
||||
return self.restart()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,13 +80,13 @@ impl Machine for ArbitraryVariableMachine<IdleState> {
|
|||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Arbitrary variables start with `(` followed by a CSS variable
|
||||
//
|
||||
// E.g.: `(--my-variable)`
|
||||
// ^^
|
||||
//
|
||||
Class::OpenParen => match cursor.next().into() {
|
||||
Class::OpenParen => match cursor.next.into() {
|
||||
Class::Dash => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
|
|
@ -117,7 +117,7 @@ impl Machine for ArbitraryVariableMachine<ParsingState> {
|
|||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.css_variable_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// A CSS variable followed by a `,` means that there is a fallback
|
||||
//
|
||||
// E.g.: `(--my-color,red)`
|
||||
|
|
@ -134,7 +134,7 @@ impl Machine for ArbitraryVariableMachine<ParsingState> {
|
|||
_ => {
|
||||
cursor.advance();
|
||||
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// End of an arbitrary variable, must be followed by `)`
|
||||
Class::CloseParen => self.done(self.start_pos, cursor),
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ impl Machine for ArbitraryVariableMachine<ParsingDataTypeState> {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Valid data type characters
|
||||
//
|
||||
// E.g.: `(length:--my-length)`
|
||||
|
|
@ -169,7 +169,7 @@ impl Machine for ArbitraryVariableMachine<ParsingDataTypeState> {
|
|||
//
|
||||
// E.g.: `(length:--my-length)`
|
||||
// ^
|
||||
Class::Colon => match cursor.next().into() {
|
||||
Class::Colon => match cursor.next.into() {
|
||||
Class::Dash => {
|
||||
cursor.advance();
|
||||
return self.transition::<ParsingState>().next(cursor);
|
||||
|
|
@ -196,8 +196,8 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
|
|||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
let len = cursor.input.len();
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Escape => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
//
|
||||
// E.g.: `(--my-\ color)`
|
||||
|
|
@ -212,7 +212,7 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
|
|||
},
|
||||
|
||||
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
|
||||
if !self.bracket_stack.push(cursor.curr()) {
|
||||
if !self.bracket_stack.push(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -221,7 +221,7 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
|
|||
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
|
||||
if !self.bracket_stack.is_empty() =>
|
||||
{
|
||||
if !self.bracket_stack.pop(cursor.curr()) {
|
||||
if !self.bracket_stack.pop(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -253,7 +253,7 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
|
|||
Class::Whitespace => return self.restart(),
|
||||
|
||||
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
|
||||
Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
|
||||
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
|
||||
return self.restart()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ impl BracketStack {
|
|||
b'(' => b')',
|
||||
b'[' => b']',
|
||||
b'{' => b'}',
|
||||
b'<' => b'>',
|
||||
_ => std::hint::unreachable_unchecked(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,14 +32,14 @@ impl Machine for CandidateMachine {
|
|||
while cursor.pos < len {
|
||||
// Skip ahead for known characters that will never be part of a candidate. No need to
|
||||
// run any sub-machines.
|
||||
if cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr.is_ascii_whitespace() {
|
||||
self.reset();
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Candidates don't start with these characters, so we can skip ahead.
|
||||
if matches!(cursor.curr(), b':' | b'"' | b'\'' | b'`') {
|
||||
if matches!(cursor.curr, b':' | b'"' | b'\'' | b'`') {
|
||||
self.reset();
|
||||
cursor.advance();
|
||||
continue;
|
||||
|
|
@ -56,7 +56,7 @@ impl Machine for CandidateMachine {
|
|||
// E.g.: `Some Class`
|
||||
// ^ ^ Invalid, we can jump ahead to the next boundary
|
||||
//
|
||||
if matches!(cursor.curr(), b'<' | b'A'..=b'Z') {
|
||||
if matches!(cursor.curr, b'<' | b'A'..=b'Z') {
|
||||
if let Some(offset) = cursor.input[cursor.pos..]
|
||||
.iter()
|
||||
.position(|&c| is_valid_before_boundary(&c))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ impl Machine for CssVariableMachine {
|
|||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
// CSS Variables must start with `--`
|
||||
if Class::Dash != cursor.curr().into() || Class::Dash != cursor.next().into() {
|
||||
if Class::Dash != cursor.curr.into() || Class::Dash != cursor.next.into() {
|
||||
return MachineState::Idle;
|
||||
}
|
||||
|
||||
|
|
@ -30,11 +30,11 @@ impl Machine for CssVariableMachine {
|
|||
cursor.advance_twice();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
|
||||
//
|
||||
Class::AllowedCharacter | Class::Dash => {
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
// Valid character followed by a valid character or an escape character
|
||||
//
|
||||
// E.g.: `--my-variable`
|
||||
|
|
@ -58,7 +58,7 @@ impl Machine for CssVariableMachine {
|
|||
}
|
||||
}
|
||||
|
||||
Class::Escape => match cursor.next().into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
//
|
||||
// In CSS it is allowed, but in the context of a class it's not because then we
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ impl<'a> Extractor<'a> {
|
|||
{
|
||||
let cursor = &mut self.cursor.clone();
|
||||
while cursor.pos < len {
|
||||
if cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr.is_ascii_whitespace() {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ impl<'a> Extractor<'a> {
|
|||
let cursor = &mut self.cursor.clone();
|
||||
|
||||
while cursor.pos < len {
|
||||
if cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr.is_ascii_whitespace() {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -147,7 +147,7 @@ impl<'a> Extractor<'a> {
|
|||
|
||||
let cursor = &mut self.cursor.clone();
|
||||
while cursor.pos < len {
|
||||
if cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr.is_ascii_whitespace() {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -238,7 +238,7 @@ mod tests {
|
|||
use std::hint::black_box;
|
||||
|
||||
fn pre_process_input(input: &str, extension: &str) -> String {
|
||||
let input = crate::scanner::pre_process_input(input.as_bytes().to_vec(), extension);
|
||||
let input = crate::scanner::pre_process_input(input.as_bytes(), extension);
|
||||
String::from_utf8(input).unwrap()
|
||||
}
|
||||
|
||||
|
|
@ -908,18 +908,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20233
|
||||
#[test]
|
||||
fn test_template_toolkit_syntax() {
|
||||
assert_extract_candidates_contains(
|
||||
&pre_process_input(
|
||||
r#"<div class="[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]"></div>"#,
|
||||
"tx",
|
||||
),
|
||||
vec!["bg-white/40", "bg-white/10"],
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17050
|
||||
#[test]
|
||||
fn test_haml_syntax() {
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ impl Machine for ModifierMachine {
|
|||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
// A modifier must start with a `/`, everything else is not a valid start of a modifier
|
||||
if Class::Slash != cursor.curr().into() {
|
||||
if Class::Slash != cursor.curr.into() {
|
||||
return MachineState::Idle;
|
||||
}
|
||||
|
||||
let start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Start of an arbitrary value:
|
||||
//
|
||||
// ```
|
||||
|
|
@ -70,9 +70,9 @@ impl Machine for ModifierMachine {
|
|||
Class::ValidStart => {
|
||||
let len = cursor.input.len();
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::ValidStart | Class::ValidInside => {
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
// Only valid characters are allowed, if followed by another valid character
|
||||
Class::ValidStart | Class::ValidInside => cursor.advance(),
|
||||
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ impl Machine for NamedUtilityMachine<IdleState> {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
Class::AlphaLower => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::AlphaLower => match cursor.next.into() {
|
||||
// Valid single character utility in between quotes
|
||||
//
|
||||
// E.g.: `<div class="a"></div>`
|
||||
|
|
@ -90,7 +90,7 @@ impl Machine for NamedUtilityMachine<IdleState> {
|
|||
//
|
||||
// E.g.: `-mx-2.5`
|
||||
// ^^
|
||||
Class::Dash => match cursor.next().into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
Class::AlphaLower => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
|
|
@ -118,7 +118,7 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Followed by a boundary character, we are at the end of the utility.
|
||||
//
|
||||
// E.g.: `'flex'`
|
||||
|
|
@ -132,14 +132,14 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
|
||||
// ^
|
||||
Class::AlphaLower | Class::AlphaUpper => {
|
||||
if is_valid_after_boundary(&cursor.next()) || {
|
||||
if is_valid_after_boundary(&cursor.next) || {
|
||||
// Or any of these characters
|
||||
//
|
||||
// - `:`, because of JS object keys
|
||||
// - `/`, because of modifiers
|
||||
// - `!`, because of important
|
||||
matches!(
|
||||
cursor.next().into(),
|
||||
cursor.next.into(),
|
||||
Class::Colon | Class::Slash | Class::Exclamation
|
||||
)
|
||||
} {
|
||||
|
|
@ -150,7 +150,7 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
cursor.advance()
|
||||
}
|
||||
|
||||
Class::Dash => match cursor.next().into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `bg-[#0088cc]`
|
||||
|
|
@ -196,7 +196,7 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
_ => return self.restart(),
|
||||
},
|
||||
|
||||
Class::Underscore => match cursor.next().into() {
|
||||
Class::Underscore => match cursor.next.into() {
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the utility but not at the end of the utility.
|
||||
//
|
||||
|
|
@ -225,14 +225,14 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
// ^
|
||||
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
|
||||
// ^
|
||||
_ if is_valid_after_boundary(&cursor.next()) || {
|
||||
_ if is_valid_after_boundary(&cursor.next) || {
|
||||
// Or any of these characters
|
||||
//
|
||||
// - `:`, because of JS object keys
|
||||
// - `/`, because of modifiers
|
||||
// - `!`, because of important
|
||||
matches!(
|
||||
cursor.next().into(),
|
||||
cursor.next.into(),
|
||||
Class::Colon | Class::Slash | Class::Exclamation
|
||||
)
|
||||
} =>
|
||||
|
|
@ -249,11 +249,11 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
// E.g.: `px-2.5`
|
||||
// ^^^
|
||||
Class::Dot => {
|
||||
if !matches!(cursor.prev().into(), Class::Number) {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(cursor.next().into(), Class::Number) {
|
||||
if !matches!(cursor.next.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
|
|
@ -278,19 +278,18 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
//
|
||||
Class::Number => {
|
||||
if !matches!(
|
||||
cursor.prev().into(),
|
||||
cursor.prev.into(),
|
||||
Class::Dash
|
||||
| Class::Underscore
|
||||
| Class::Dot
|
||||
| Class::Number
|
||||
| Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(
|
||||
cursor.next().into(),
|
||||
cursor.next.into(),
|
||||
Class::Dot
|
||||
| Class::Number
|
||||
| Class::AlphaLower
|
||||
|
|
@ -314,7 +313,7 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
// ^^
|
||||
// ```
|
||||
Class::Percent => {
|
||||
if !matches!(cursor.prev().into(), Class::Number) {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
|
|
@ -426,8 +425,6 @@ mod tests {
|
|||
// With number followed by dash or underscore
|
||||
("text-title1-strong", vec!["text-title1-strong"]),
|
||||
("text-title1_strong", vec!["text-title1_strong"]),
|
||||
// With capital letter followed by number
|
||||
("text-titleV1-strong", vec!["text-titleV1-strong"]),
|
||||
// With trailing % sign
|
||||
("from-15%", vec!["from-15%"]),
|
||||
// Arbitrary value with bracket notation
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ impl Machine for NamedVariantMachine<IdleState> {
|
|||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
Class::AlphaLower | Class::Star => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::AlphaLower | Class::Star => match cursor.next.into() {
|
||||
// Valid single character variant, must be followed by a `:`
|
||||
//
|
||||
// E.g.: `<div class="x:flex"></div>`
|
||||
|
|
@ -134,8 +134,8 @@ impl Machine for NamedVariantMachine<ParsingState> {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Dash => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `data-[state=pending]:`.
|
||||
|
|
@ -192,7 +192,7 @@ impl Machine for NamedVariantMachine<ParsingState> {
|
|||
};
|
||||
}
|
||||
|
||||
Class::Underscore => match cursor.next().into() {
|
||||
Class::Underscore => match cursor.next.into() {
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the variant but not at the end of the variant.
|
||||
//
|
||||
|
|
@ -240,11 +240,11 @@ impl Machine for NamedVariantMachine<ParsingState> {
|
|||
// E.g.: `2.5xl:flex`
|
||||
// ^^^
|
||||
Class::Dot => {
|
||||
if !matches!(cursor.prev().into(), Class::Number) {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(cursor.next().into(), Class::Number) {
|
||||
if !matches!(cursor.next.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ impl Machine for NamedVariantMachine<ParsingState> {
|
|||
impl NamedVariantMachine<ParsingState> {
|
||||
#[inline(always)]
|
||||
fn parse_arbitrary_end(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
Class::Slash => {
|
||||
cursor.advance();
|
||||
self.transition::<ParsingModifierState>().next(cursor)
|
||||
|
|
@ -285,7 +285,7 @@ impl Machine for NamedVariantMachine<ParsingModifierState> {
|
|||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.modifier_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// Modifier must be followed by a `:`
|
||||
//
|
||||
// E.g.: `group-hover/name:`
|
||||
|
|
@ -308,7 +308,7 @@ impl Machine for NamedVariantMachine<ParsingEndState> {
|
|||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// The end of a variant must be the `:`
|
||||
//
|
||||
// E.g.: `hover:`
|
||||
|
|
|
|||
|
|
@ -32,14 +32,14 @@ impl PreProcessor for Clojure {
|
|||
let mut cursor = cursor::Cursor::new(&content);
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Consume strings as-is
|
||||
b'"' => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
|
|
@ -57,8 +57,8 @@ impl PreProcessor for Clojure {
|
|||
|
||||
// Discard line comments until the end of the line.
|
||||
// Comments start with `;;`
|
||||
b';' if matches!(cursor.next(), b';') => {
|
||||
while cursor.pos < len && cursor.curr() != b'\n' {
|
||||
b';' if matches!(cursor.next, b';') => {
|
||||
while cursor.pos < len && cursor.curr != b'\n' {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ impl PreProcessor for Clojure {
|
|||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// A `.` surrounded by digits is a decimal number, so we don't want to replace it.
|
||||
//
|
||||
// E.g.:
|
||||
|
|
@ -78,8 +78,8 @@ impl PreProcessor for Clojure {
|
|||
// gap-1.5
|
||||
// ^
|
||||
// ```
|
||||
b'.' if cursor.prev().is_ascii_digit()
|
||||
&& cursor.next().is_ascii_digit() =>
|
||||
b'.' if cursor.prev.is_ascii_digit()
|
||||
&& cursor.next.is_ascii_digit() =>
|
||||
{
|
||||
// Keep the `.` as-is
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ impl PreProcessor for Clojure {
|
|||
result[cursor.pos] = b' ';
|
||||
}
|
||||
// End of keyword.
|
||||
_ if !is_keyword_character(cursor.curr()) => {
|
||||
_ if !is_keyword_character(cursor.curr) => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
|
@ -110,11 +110,11 @@ impl PreProcessor for Clojure {
|
|||
|
||||
// Handle quote with a list, e.g.: `'(…)`
|
||||
// and with a vector, e.g.: `'[…]`
|
||||
b'\'' if matches!(cursor.next(), b'[' | b'(') => {
|
||||
b'\'' if matches!(cursor.next, b'[' | b'(') => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' ';
|
||||
let end = match cursor.curr() {
|
||||
let end = match cursor.curr {
|
||||
b'[' => b']',
|
||||
b'(' => b')',
|
||||
_ => unreachable!(),
|
||||
|
|
@ -122,7 +122,7 @@ impl PreProcessor for Clojure {
|
|||
|
||||
// Consume until the closing `]`
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
x if x == end => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
|
|
@ -134,7 +134,7 @@ impl PreProcessor for Clojure {
|
|||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
|
|
@ -157,14 +157,14 @@ impl PreProcessor for Clojure {
|
|||
}
|
||||
|
||||
// Handle quote with a keyword, e.g.: `'bg-white`
|
||||
b'\'' if !cursor.next().is_ascii_whitespace() => {
|
||||
b'\'' if !cursor.next.is_ascii_whitespace() => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// End of keyword.
|
||||
_ if !is_keyword_character(cursor.curr()) => {
|
||||
_ if !is_keyword_character(cursor.curr) => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ impl PreProcessor for Elixir {
|
|||
|
||||
while cursor.pos < content.len() {
|
||||
// Look for a sigil marker
|
||||
if cursor.curr() != b'~' {
|
||||
if cursor.curr != b'~' {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Scan charlists, strings, and wordlists
|
||||
if !matches!(cursor.next(), b'c' | b'C' | b's' | b'S' | b'w' | b'W') {
|
||||
if !matches!(cursor.next, b'c' | b'C' | b's' | b'S' | b'w' | b'W') {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ impl PreProcessor for Elixir {
|
|||
cursor.advance_twice();
|
||||
|
||||
// Match the opening for a sigil
|
||||
if !matches!(cursor.curr(), b'(' | b'[' | b'{') {
|
||||
if !matches!(cursor.curr, b'(' | b'[' | b'{') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -35,19 +35,19 @@ impl PreProcessor for Elixir {
|
|||
result[cursor.pos] = b' ';
|
||||
|
||||
// Scan until we find a balanced closing one and replace it too
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
|
||||
while cursor.pos < content.len() {
|
||||
cursor.advance();
|
||||
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
b'(' | b'[' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
|
||||
if bracket_stack.is_empty() {
|
||||
// Replace the closing bracket with a space
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ impl PreProcessor for Haml {
|
|||
let mut last_newline_position = 0;
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escape the next character
|
||||
b'\\' => {
|
||||
cursor.advance_twice();
|
||||
|
|
@ -119,7 +119,7 @@ impl PreProcessor for Haml {
|
|||
b'-' if cursor.input[last_newline_position..cursor.pos]
|
||||
.iter()
|
||||
.all(u8::is_ascii_whitespace)
|
||||
&& matches!(cursor.next(), b'#') =>
|
||||
&& matches!(cursor.next, b'#') =>
|
||||
{
|
||||
// Just consume the comment
|
||||
let updated_last_newline_position =
|
||||
|
|
@ -159,7 +159,7 @@ impl PreProcessor for Haml {
|
|||
// Override the last known newline position
|
||||
last_newline_position = end;
|
||||
|
||||
let replaced = pre_process_input(ruby_code.to_vec(), "rb");
|
||||
let replaced = pre_process_input(ruby_code, "rb");
|
||||
result.replace_range(start..end, replaced);
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ impl PreProcessor for Haml {
|
|||
// digit.
|
||||
// E.g.: `bg-red-500.2xl:flex`
|
||||
// ^^^
|
||||
if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
|
||||
if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
|
||||
let mut next_cursor = cursor.clone();
|
||||
next_cursor.advance();
|
||||
|
||||
|
|
@ -203,81 +203,6 @@ impl PreProcessor for Haml {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle Ruby syntax with `%w[]` arrays embedded in Haml attribute hashes. E.g.:
|
||||
//
|
||||
// ```haml
|
||||
// %div{class: %w[bg-blue-500 w-10 h-10]}
|
||||
// ```
|
||||
//
|
||||
// A `%` that follows a value is not a percent literal. E.g.: the `50%w` in
|
||||
// `hit rate 50%w.`
|
||||
b'%' if matches!(cursor.next(), b'w' | b'W')
|
||||
&& !cursor.prev().is_ascii_alphanumeric()
|
||||
&& !matches!(cursor.prev(), b'_' | b')' | b']' | b'}') =>
|
||||
{
|
||||
// Boundary characters
|
||||
let (open, close) = match cursor.input.get(cursor.pos + 2) {
|
||||
Some(b'[') => (b'[', b']'),
|
||||
Some(b'(') => (b'(', b')'),
|
||||
Some(b'{') => (b'{', b'}'),
|
||||
Some(b'<') => (b'<', b'>'),
|
||||
|
||||
// Any other ASCII punctuation can be used as a custom delimiter
|
||||
Some(&c) if c.is_ascii_punctuation() => (c, c),
|
||||
|
||||
// Everything else is not a valid delimiter
|
||||
_ => {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
result[cursor.pos] = b' '; // Replace `%`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace `w`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace the opening delimiter
|
||||
cursor.advance();
|
||||
|
||||
// Paired delimiters can be nested as long as they are balanced. E.g.:
|
||||
// `%w[foo[bar]baz]` produces a flat array.
|
||||
let mut depth = 1_usize;
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter
|
||||
// itself
|
||||
b'\\' if close != b'\\' => {
|
||||
// Use backslash to embed spaces in the strings.
|
||||
if cursor.next() == b' ' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Start of a nested delimiter pair
|
||||
c if c == open && open != close => depth += 1,
|
||||
|
||||
// Closing delimiter
|
||||
c if c == close => {
|
||||
depth -= 1;
|
||||
|
||||
// End of the literal, replace the closing delimiter with a space
|
||||
if depth == 0 {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else is valid content
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// Replace following characters with spaces if they are not inside of brackets
|
||||
b'#' | b'=' if bracket_stack.is_empty() => {
|
||||
result[cursor.pos] = b' ';
|
||||
|
|
@ -288,11 +213,11 @@ impl PreProcessor for Haml {
|
|||
if bracket_stack.is_empty() {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
|
||||
// Replace closing bracket with a space
|
||||
if bracket_stack.is_empty() {
|
||||
|
|
@ -332,7 +257,7 @@ impl Haml {
|
|||
// :url => { :action => "add", :id => product.id },
|
||||
// :update => { :success => "cart", :failure => "error" }
|
||||
// ```
|
||||
let evaluation_type = cursor.curr();
|
||||
let evaluation_type = cursor.curr;
|
||||
|
||||
let block_indentation_level = cursor
|
||||
.pos
|
||||
|
|
@ -342,17 +267,17 @@ impl Haml {
|
|||
let mut last_newline_position = last_known_newline_position;
|
||||
|
||||
// Consume until the end of the line first
|
||||
while cursor.pos < len && cursor.curr() != b'\n' {
|
||||
while cursor.pos < len && cursor.curr != b'\n' {
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Block is already done, aka just a line
|
||||
if evaluation_type == b'=' && cursor.prev() != b',' {
|
||||
if evaluation_type == b'=' && cursor.prev != b',' {
|
||||
return cursor.pos;
|
||||
}
|
||||
|
||||
'outer: while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escape the next character
|
||||
b'\\' => {
|
||||
cursor.advance_twice();
|
||||
|
|
@ -364,7 +289,7 @@ impl Haml {
|
|||
last_newline_position = cursor.pos;
|
||||
|
||||
// We are done with this block
|
||||
if evaluation_type == b'=' && cursor.prev() != b',' {
|
||||
if evaluation_type == b'=' && cursor.prev != b',' {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -375,11 +300,11 @@ impl Haml {
|
|||
// Skip whitespace and compute the indentation level
|
||||
x if x.is_ascii_whitespace() => {
|
||||
// Find first non-whitespace character
|
||||
while cursor.pos < len && cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr() == b'\n' {
|
||||
while cursor.pos < len && cursor.curr.is_ascii_whitespace() {
|
||||
if cursor.curr == b'\n' {
|
||||
last_newline_position = cursor.pos;
|
||||
|
||||
if evaluation_type == b'=' && cursor.prev() != b',' {
|
||||
if evaluation_type == b'=' && cursor.prev != b',' {
|
||||
// We are done with this block
|
||||
break 'outer;
|
||||
}
|
||||
|
|
@ -499,81 +424,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20386
|
||||
#[test]
|
||||
fn test_embedded_ruby_percent_w_delimiters() {
|
||||
for (input, expected) in [
|
||||
// %w[…] in an attribute hash
|
||||
(
|
||||
"%div{class: %w[flex px-2.5]}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
// %w<…>
|
||||
(
|
||||
"%div{class: %w<flex px-2.5>}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
(
|
||||
"%div{class: %w<flex <nested> px-2.5>}",
|
||||
"%div class: flex <nested> px-2.5 ",
|
||||
),
|
||||
// Custom delimiters
|
||||
(
|
||||
"%div{class: %w|flex px-2.5|}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
(
|
||||
"%div{class: %W!flex px-2.5!}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
(
|
||||
"%div{class: %w#text-sm leading-6#}",
|
||||
"%div class: text-sm leading-6 ",
|
||||
),
|
||||
(
|
||||
"%div{class: %w=italic tracking-wide=}",
|
||||
"%div class: italic tracking-wide ",
|
||||
),
|
||||
// Nested paired delimiters stay balanced inside the literal
|
||||
(
|
||||
"%div{class: %w[content-['[hello]'] p-4]}",
|
||||
"%div class: content-['[hello]'] p-4 ",
|
||||
),
|
||||
// Escaped spaces embed a space in a single array element
|
||||
(
|
||||
r#"%div{class: %w[foo\ bar baz-1]}"#,
|
||||
r#"%div class: foo bar baz-1 "#,
|
||||
),
|
||||
// A `%` that follows a value is not a percent literal
|
||||
("%p hit rate 50%w.", "%p hit rate 50%w "),
|
||||
] {
|
||||
Haml::test(input, expected);
|
||||
}
|
||||
|
||||
let input = r#"
|
||||
%div{class: %w[bg-blue-500 w-10 h-10]}
|
||||
%div{class: %w<flex px-2.5>}
|
||||
%div{class: %w|underline font-bold|}
|
||||
- classes = %w<mt-4 grid>
|
||||
"#;
|
||||
|
||||
Haml::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"bg-blue-500",
|
||||
"w-10",
|
||||
"h-10",
|
||||
"flex",
|
||||
"px-2.5",
|
||||
"underline",
|
||||
"font-bold",
|
||||
"mt-4",
|
||||
"grid",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352
|
||||
#[test]
|
||||
fn test_haml_full_file_17051() {
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ impl PreProcessor for Json {
|
|||
let mut cursor = cursor::Cursor::new(content);
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Consume strings as-is
|
||||
b'"' => {
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
|
|
|
|||
|
|
@ -9,27 +9,20 @@ impl PreProcessor for Markdown {
|
|||
let len = content.len();
|
||||
let mut result = content.to_vec();
|
||||
let mut cursor = cursor::Cursor::new(content);
|
||||
let mut bracket_stack = vec![];
|
||||
|
||||
let mut in_directive = false;
|
||||
|
||||
while cursor.pos < len {
|
||||
match (in_directive, cursor.curr()) {
|
||||
match (in_directive, cursor.curr) {
|
||||
(false, b'{') => {
|
||||
result[cursor.pos] = b' ';
|
||||
in_directive = true;
|
||||
}
|
||||
(true, b'(' | b'[' | b'{' | b'<') => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
(true, b')' | b']' | b'}' | b'>') if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop();
|
||||
}
|
||||
(true, b'}') => {
|
||||
result[cursor.pos] = b' ';
|
||||
in_directive = false;
|
||||
}
|
||||
(true, b'.') if bracket_stack.is_empty() => {
|
||||
(true, b'.') => {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -67,17 +60,4 @@ mod tests {
|
|||
Markdown::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_classes_keep_the_dots() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
r#"{<div class="px-2.5"></div>}"#,
|
||||
r#" <div class="px-2.5"></div> "#,
|
||||
),
|
||||
(r#"{content-['example.js']}"#, r#" content-['example.js'] "#),
|
||||
] {
|
||||
Markdown::test(input, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ pub mod ruby;
|
|||
pub mod rust;
|
||||
pub mod slim;
|
||||
pub mod svelte;
|
||||
pub mod template_toolkit;
|
||||
pub mod twig;
|
||||
pub mod vue;
|
||||
|
||||
pub use clojure::*;
|
||||
|
|
@ -26,6 +24,4 @@ pub use ruby::*;
|
|||
pub use rust::*;
|
||||
pub use slim::*;
|
||||
pub use svelte::*;
|
||||
pub use template_toolkit::*;
|
||||
pub use twig::*;
|
||||
pub use vue::*;
|
||||
|
|
|
|||
|
|
@ -25,33 +25,7 @@ pub trait PreProcessor: Sized + Default {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_extract_exact(input: &str, expected: Vec<&str>) {
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
|
||||
let input = input.as_bytes();
|
||||
|
||||
let processor = Self::default();
|
||||
let transformed = processor.process(input);
|
||||
|
||||
let extracted = Extractor::new(&transformed).extract();
|
||||
|
||||
// Extract all candidates and css variables.
|
||||
let candidates = extracted
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
Extracted::Candidate(bytes) => std::str::from_utf8(bytes).ok(),
|
||||
Extracted::CssVariable(bytes) => std::str::from_utf8(bytes).ok(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if candidates != expected {
|
||||
dbg!(&candidates, &expected);
|
||||
panic!("Extracted candidates do not match expected candidates");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_extract_contains(input: &str, expected: Vec<&str>) {
|
||||
fn test_extract_contains(input: &str, items: Vec<&str>) {
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
|
||||
let input = input.as_bytes();
|
||||
|
|
@ -72,7 +46,7 @@ pub trait PreProcessor: Sized + Default {
|
|||
|
||||
// Ensure all items are present in the candidates.
|
||||
let mut missing = vec![];
|
||||
for item in &expected {
|
||||
for item in &items {
|
||||
if !candidates.contains(item) {
|
||||
missing.push(item);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ impl PreProcessor for Pug {
|
|||
let mut bracket_stack = BracketStack::default();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
|
||||
//
|
||||
// ```diff
|
||||
|
|
@ -43,7 +43,7 @@ impl PreProcessor for Pug {
|
|||
// digit.
|
||||
// E.g.: `bg-red-500.2xl:flex`
|
||||
// ^^^
|
||||
if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
|
||||
if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
|
||||
let mut next_cursor = cursor.clone();
|
||||
next_cursor.advance();
|
||||
|
||||
|
|
@ -68,17 +68,17 @@ impl PreProcessor for Pug {
|
|||
//
|
||||
// However, we also need to make sure that we keep the parens that are part of the
|
||||
// utility class. E.g.: `bg-(--my-color)`.
|
||||
b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => {
|
||||
b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => {
|
||||
result[cursor.pos] = b' ';
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b'(' | b'[' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
}
|
||||
|
||||
// Consume everything else
|
||||
|
|
|
|||
|
|
@ -34,135 +34,51 @@ impl PreProcessor for Ruby {
|
|||
|
||||
// Extract embedded template languages
|
||||
// https://viewcomponent.org/guide/templates.html#interpolations
|
||||
// Only process if content is valid UTF-8, otherwise skip HEREDOC extraction
|
||||
// but still perform the byte-level Ruby processing below
|
||||
if let Ok(content_as_str) = std::str::from_utf8(content) {
|
||||
let starts = TEMPLATE_START_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let ends = TEMPLATE_END_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let content_as_str = std::str::from_utf8(content).unwrap();
|
||||
|
||||
for start in starts.iter() {
|
||||
// The language for this block
|
||||
let lang = start.get(1).unwrap().as_str();
|
||||
let starts = TEMPLATE_START_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let ends = TEMPLATE_END_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// The HEREDOC delimiter
|
||||
let delimiter_start = start.get(2).unwrap().as_str();
|
||||
for start in starts.iter() {
|
||||
// The language for this block
|
||||
let lang = start.get(1).unwrap().as_str();
|
||||
|
||||
// Where the "body" starts for the HEREDOC block
|
||||
let body_start = start.get(0).unwrap().end();
|
||||
// The HEREDOC delimiter
|
||||
let delimiter_start = start.get(2).unwrap().as_str();
|
||||
|
||||
// Look through all of the ends to find a matching language
|
||||
for end in ends.iter() {
|
||||
// 1. This must appear after the start
|
||||
let body_end = end.get(0).unwrap().start();
|
||||
if body_end < body_start {
|
||||
continue;
|
||||
}
|
||||
// Where the "body" starts for the HEREDOC block
|
||||
let body_start = start.get(0).unwrap().end();
|
||||
|
||||
// The languages must match otherwise we haven't found the end
|
||||
let delimiter_end = end.get(1).unwrap().as_str();
|
||||
if delimiter_end != delimiter_start {
|
||||
continue;
|
||||
}
|
||||
|
||||
let body = &content_as_str[body_start..body_end];
|
||||
let replaced =
|
||||
pre_process_input(body.as_bytes().to_vec(), &lang.to_ascii_lowercase());
|
||||
|
||||
result.replace_range(body_start..body_end, replaced);
|
||||
break;
|
||||
// Look through all of the ends to find a matching language
|
||||
for end in ends.iter() {
|
||||
// 1. This must appear after the start
|
||||
let body_end = end.get(0).unwrap().start();
|
||||
if body_end < body_start {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The languages must match otherwise we haven't found the end
|
||||
let delimiter_end = end.get(1).unwrap().as_str();
|
||||
if delimiter_end != delimiter_start {
|
||||
continue;
|
||||
}
|
||||
|
||||
let body = &content_as_str[body_start..body_end];
|
||||
let replaced = pre_process_input(body.as_bytes(), &lang.to_ascii_lowercase());
|
||||
|
||||
result.replace_range(body_start..body_end, replaced);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ruby extraction
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
b'"' => {
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
// End of the string
|
||||
b'"' => break,
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
b'\'' => {
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
// End of the string
|
||||
b'\'' => break,
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace comments in Ruby files
|
||||
//
|
||||
// Except for strict locals, these are defined in a `<%# locals: … %>`. Checking if
|
||||
// the comment is preceded by a `%` should be enough without having to perform more
|
||||
// parsing logic. Worst case we _do_ scan a few comments.
|
||||
//
|
||||
// We also want to skip interpolation syntax, which look like `#{…}`.
|
||||
b'#' if !matches!(cursor.prev(), b'%') && !matches!(cursor.next(), b'{') => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// End of the comment
|
||||
b'\n' => break,
|
||||
|
||||
// Everything else is part of the comment and replaced
|
||||
_ => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Looking for `%w`, `%W`, or `%p`
|
||||
if cursor.curr() != b'%' || !matches!(cursor.next(), b'w' | b'W' | b'p') {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// A `%` that follows a value is a modulo operation, not a percent literal. E.g.: the
|
||||
// `50%w` in `hit rate 50%w.`
|
||||
if cursor.prev().is_ascii_alphanumeric()
|
||||
|| matches!(cursor.prev(), b'_' | b')' | b']' | b'}')
|
||||
{
|
||||
// Looking for `%w` or `%W`
|
||||
if cursor.curr != b'%' && !matches!(cursor.next, b'w' | b'W') {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -170,17 +86,10 @@ impl PreProcessor for Ruby {
|
|||
cursor.advance_twice();
|
||||
|
||||
// Boundary character
|
||||
let boundary = match cursor.curr() {
|
||||
let boundary = match cursor.curr {
|
||||
b'[' => b']',
|
||||
b'(' => b')',
|
||||
b'{' => b'}',
|
||||
b'<' => b'>',
|
||||
b' ' => b'\n',
|
||||
|
||||
// Any other ASCII punctuation can be used as a custom delimiter
|
||||
c if c.is_ascii_punctuation() => c,
|
||||
|
||||
// Everything else is not a valid delimiter
|
||||
_ => {
|
||||
cursor.advance();
|
||||
continue;
|
||||
|
|
@ -196,11 +105,11 @@ impl PreProcessor for Ruby {
|
|||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter itself
|
||||
b'\\' if boundary != b'\\' => {
|
||||
match cursor.curr {
|
||||
// Skip escaped characters
|
||||
b'\\' => {
|
||||
// Use backslash to embed spaces in the strings.
|
||||
if cursor.next() == b' ' {
|
||||
if cursor.next == b' ' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
|
|
@ -209,36 +118,20 @@ impl PreProcessor for Ruby {
|
|||
|
||||
// Start of a nested bracket
|
||||
b'[' | b'(' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
|
||||
// Start of a nested `<…>`, which Ruby allows inside a `%w<…>` literal
|
||||
b'<' if boundary == b'>' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
// End of a nested bracket
|
||||
b']' | b')' | b'}' if !bracket_stack.is_empty() => {
|
||||
if !bracket_stack.pop(cursor.curr()) {
|
||||
// Unbalanced
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// End of a nested `<…>`
|
||||
b'>' if boundary == b'>' && !bracket_stack.is_empty() => {
|
||||
if !bracket_stack.pop(cursor.curr()) {
|
||||
if !bracket_stack.pop(cursor.curr) {
|
||||
// Unbalanced
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// End of the pattern, replace the boundary character with a space
|
||||
_ if cursor.curr() == boundary => {
|
||||
if boundary != b'\n' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
_ if cursor.curr == boundary => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -280,69 +173,12 @@ mod tests {
|
|||
"%w(flex data-[state=pending]:bg-(--my-color) flex-col)",
|
||||
"%w flex data-[state=pending]:bg-(--my-color) flex-col ",
|
||||
),
|
||||
// %w<…>
|
||||
("%w<flex px-2.5>", "%w flex px-2.5 "),
|
||||
(
|
||||
"%w<flex data-[state=pending]:bg-(--my-color) flex-col>",
|
||||
"%w flex data-[state=pending]:bg-(--my-color) flex-col ",
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
("%w<flex <nested> px-2.5>", "%w flex <nested> px-2.5 "),
|
||||
// %w|…|, %w:…:, %w!…!
|
||||
("%w|flex px-2.5|", "%w flex px-2.5 "),
|
||||
("%w:flex px-2.5:", "%w flex px-2.5 "),
|
||||
("%w!flex px-2.5!", "%w flex px-2.5 "),
|
||||
(r#"%w\flex px-2.5\"#, r#"%w flex px-2.5 "#),
|
||||
// A `%` that follows a value is a modulo operation, not a percent literal
|
||||
(
|
||||
"hit rate 50%w.\n%w[flex px-2.5]",
|
||||
"hit rate 50%w.\n%w flex px-2.5 ",
|
||||
),
|
||||
|
||||
// %w …\n
|
||||
("%w flex px-2.5\n", "%w flex px-2.5\n"),
|
||||
|
||||
// Use backslash to embed spaces in the strings.
|
||||
(r#"%w[foo\ bar baz\ bat]"#, r#"%w foo bar baz bat "#),
|
||||
(r#"%W[foo\ bar baz\ bat]"#, r#"%W foo bar baz bat "#),
|
||||
|
||||
// The nested delimiters evaluated to a flat array of strings
|
||||
// (not nested array).
|
||||
(r#"%w[foo[bar baz]qux]"#, r#"%w foo[bar baz]qux "#),
|
||||
|
||||
(
|
||||
"# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]",
|
||||
" \n \n \n%w flex px-2.5 "
|
||||
),
|
||||
|
||||
(r#""foo # bar""#, r#""foo # bar""#),
|
||||
(r#"'foo # bar'"#, r#"'foo # bar'"#),
|
||||
(
|
||||
r#"def call = tag.span "Foo", class: %w[rounded-full h-0.75 w-0.75]"#,
|
||||
r#"def call = tag.span "Foo", class: %w rounded-full h-0.75 w-0.75 "#
|
||||
),
|
||||
|
||||
(r#"%w[foo ' bar]"#, r#"%w foo ' bar "#),
|
||||
(r#"%w[foo " bar]"#, r#"%w foo " bar "#),
|
||||
(r#"%W[foo ' bar]"#, r#"%W foo ' bar "#),
|
||||
(r#"%W[foo " bar]"#, r#"%W foo " bar "#),
|
||||
|
||||
(r#"%p foo ' bar "#, r#"%p foo ' bar "#),
|
||||
(r#"%p foo " bar "#, r#"%p foo " bar "#),
|
||||
|
||||
(
|
||||
"%p has a ' quote\n# this should be removed\n%p has a ' quote",
|
||||
"%p has a ' quote\n \n%p has a ' quote"
|
||||
),
|
||||
(
|
||||
"%p has a \" quote\n# this should be removed\n%p has a \" quote",
|
||||
"%p has a \" quote\n \n%p has a \" quote"
|
||||
),
|
||||
|
||||
(
|
||||
"%w#this text is kept# # this text is not",
|
||||
"%w this text is kept ",
|
||||
),
|
||||
] {
|
||||
Ruby::test(input, expected);
|
||||
}
|
||||
|
|
@ -375,30 +211,6 @@ mod tests {
|
|||
"%w(flex data-[state=pending]:bg-(--my-color) flex-col)",
|
||||
vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"],
|
||||
),
|
||||
// %w<…>
|
||||
("%w<flex px-2.5>", vec!["flex", "px-2.5"]),
|
||||
("%w<px-2.5 flex>", vec!["flex", "px-2.5"]),
|
||||
("%w<2xl:flex>", vec!["2xl:flex"]),
|
||||
(
|
||||
"%w<flex data-[state=pending]:bg-(--my-color) flex-col>",
|
||||
vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"],
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
("%w<flex <nested> px-2.5>", vec!["flex", "px-2.5"]),
|
||||
// %w|…|, %w:…:, %w!…!
|
||||
("%w|flex px-2.5|", vec!["flex", "px-2.5"]),
|
||||
("%w:flex px-2.5:", vec!["flex", "px-2.5"]),
|
||||
("%w!flex px-2.5!", vec!["flex", "px-2.5"]),
|
||||
|
||||
(
|
||||
"# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]",
|
||||
vec!["flex", "px-2.5"],
|
||||
),
|
||||
|
||||
(r#""foo # bar""#, vec!["foo", "bar"]),
|
||||
(r#"'foo # bar'"#, vec!["foo", "bar"]),
|
||||
|
||||
(r#"%w[foo ' bar]"#, vec!["foo", "bar"]),
|
||||
] {
|
||||
Ruby::test_extract_contains(input, expected);
|
||||
}
|
||||
|
|
@ -450,79 +262,4 @@ mod tests {
|
|||
"#;
|
||||
Ruby::test_extract_contains(input, vec!["z-1", "z-2", "z-3"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19728
|
||||
#[test]
|
||||
fn test_interpolated_expressions() {
|
||||
let input = r#"
|
||||
def width_class(width = nil)
|
||||
<<~STYLE_CLASS
|
||||
#{width || 'w-100'}
|
||||
STYLE_CLASS
|
||||
end
|
||||
"#;
|
||||
|
||||
Ruby::test_extract_contains(input, vec!["w-100"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19239
|
||||
#[test]
|
||||
fn test_skip_comments() {
|
||||
let input = r#"
|
||||
# From activerecord-8.1.1/lib/active_record/errors.rb:147
|
||||
# Rails uses RDoc cross-reference syntax in inline documentation:
|
||||
# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]
|
||||
"#;
|
||||
|
||||
// Nothing should be extracted from comments, so expect an empty array.
|
||||
Ruby::test_extract_exact(input, vec![]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19481
|
||||
#[test]
|
||||
fn test_strict_locals() {
|
||||
// Strict locals are defined in a `<%# locals: … %>`, but the `#` looks like a comment
|
||||
// which we should not ignore in this case.
|
||||
let input = r#"
|
||||
<%# locals: (css: "text-amber-600") %>
|
||||
<% more_css = "text-sky-500" %>
|
||||
|
||||
<p class="text-green-500">
|
||||
In a partial
|
||||
</p>
|
||||
|
||||
<p class="<%= css %>">
|
||||
In a partial using explicit local variables
|
||||
</p>
|
||||
|
||||
<p class="<%= more_css %>">
|
||||
In a partial using explicit local variables
|
||||
</p>
|
||||
"#;
|
||||
|
||||
Ruby::test_extract_contains(
|
||||
input,
|
||||
vec!["text-amber-600", "text-sky-500", "text-green-500"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf8_does_not_panic() {
|
||||
// Invalid UTF-8 sequence: 0x80 is a continuation byte without a leading byte
|
||||
let invalid_utf8: &[u8] = &[0x80, 0x81, 0x82];
|
||||
|
||||
let processor = Ruby::default();
|
||||
|
||||
// Should not panic, just return the input unchanged
|
||||
let result = processor.process(invalid_utf8);
|
||||
assert_eq!(result, invalid_utf8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_utf8_with_multibyte_chars() {
|
||||
// Test that valid UTF-8 with multi-byte characters (like em-dashes) works
|
||||
let input = "# Comment with em—dash\n%w[flex px-2.5]";
|
||||
|
||||
Ruby::test_extract_contains(input, vec!["flex", "px-2.5"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ impl Rust {
|
|||
let mut bracket_stack = bracket_stack::BracketStack::default();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => {
|
||||
cursor.advance_twice();
|
||||
|
|
@ -46,7 +46,7 @@ impl Rust {
|
|||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ impl Rust {
|
|||
// digit.
|
||||
// E.g.: `bg-red-500.2xl:flex`
|
||||
// ^^^
|
||||
if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
|
||||
if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
|
||||
let mut next_cursor = cursor.clone();
|
||||
next_cursor.advance();
|
||||
|
||||
|
|
@ -103,17 +103,11 @@ impl Rust {
|
|||
}
|
||||
|
||||
b'[' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
|
||||
// Handle `p.flex[condition]`. If there is a `-` before it, it will likely be an
|
||||
// arbitrary value e.g. `text-[red]`
|
||||
if !matches!(cursor.prev(), b'-') {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b']' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
}
|
||||
|
||||
// Consume everything else
|
||||
|
|
@ -219,20 +213,4 @@ mod tests {
|
|||
let input = r#"html! { \x.px-4.text-black { } }"#;
|
||||
Rust::test(input, r#"html! { \x px-4 text-black { } }"#);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20233
|
||||
#[test]
|
||||
fn test_maud_template_extraction_with_conditional_classes() {
|
||||
let input = r#"
|
||||
use maud::{html, Markup};
|
||||
|
||||
pub fn main() -> Markup {
|
||||
html! {
|
||||
p.text-black[cuteness > 50] { "Squee!" }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
Rust::test_extract_contains(input, vec!["text-black"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ impl PreProcessor for Slim {
|
|||
let mut bracket_stack = BracketStack::default();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
|
||||
//
|
||||
// ```diff
|
||||
|
|
@ -43,7 +43,7 @@ impl PreProcessor for Slim {
|
|||
// digit.
|
||||
// E.g.: `bg-red-500.2xl:flex`
|
||||
// ^^^
|
||||
if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
|
||||
if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
|
||||
let mut next_cursor = cursor.clone();
|
||||
next_cursor.advance();
|
||||
|
||||
|
|
@ -65,74 +65,17 @@ impl PreProcessor for Slim {
|
|||
// class=%w[bg-blue-500 w-10 h-10]
|
||||
// ]
|
||||
// ```
|
||||
//
|
||||
// A `%` that follows a value is not a percent literal. E.g.: the `50%w` in
|
||||
// `hit rate 50%w.`
|
||||
b'%' if matches!(cursor.next(), b'w' | b'W')
|
||||
&& !cursor.prev().is_ascii_alphanumeric()
|
||||
&& !matches!(cursor.prev(), b'_' | b')' | b']' | b'}') =>
|
||||
b'%' if matches!(cursor.next, b'w' | b'W')
|
||||
&& matches!(cursor.input.get(cursor.pos + 2), Some(b'[' | b'(' | b'{')) =>
|
||||
{
|
||||
// Boundary characters
|
||||
let (open, close) = match cursor.input.get(cursor.pos + 2) {
|
||||
Some(b'[') => (b'[', b']'),
|
||||
Some(b'(') => (b'(', b')'),
|
||||
Some(b'{') => (b'{', b'}'),
|
||||
Some(b'<') => (b'<', b'>'),
|
||||
|
||||
// Any other ASCII punctuation can be used as a custom delimiter
|
||||
Some(&c) if c.is_ascii_punctuation() => (c, c),
|
||||
|
||||
// Everything else is not a valid delimiter
|
||||
_ => {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
result[cursor.pos] = b' '; // Replace `%`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace `w`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace the opening delimiter
|
||||
cursor.advance();
|
||||
|
||||
// Paired delimiters can be nested as long as they are balanced. E.g.:
|
||||
// `%w[foo[bar]baz]` produces a flat array.
|
||||
let mut depth = 1_usize;
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter
|
||||
// itself
|
||||
b'\\' if close != b'\\' => {
|
||||
// Use backslash to embed spaces in the strings.
|
||||
if cursor.next() == b' ' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Start of a nested delimiter pair
|
||||
c if c == open && open != close => depth += 1,
|
||||
|
||||
// Closing delimiter
|
||||
c if c == close => {
|
||||
depth -= 1;
|
||||
|
||||
// End of the literal, replace the closing delimiter with a space
|
||||
if depth == 0 {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else is valid content
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
result[cursor.pos] = b' '; // Replace `[` or `(` or `{`
|
||||
bracket_stack.push(cursor.curr);
|
||||
cursor.advance(); // Move past the bracket
|
||||
continue;
|
||||
}
|
||||
|
||||
// Any `[` preceded by an alphanumeric value will not be part of a candidate.
|
||||
|
|
@ -153,10 +96,10 @@ impl PreProcessor for Slim {
|
|||
// Instead of listing all boundary characters, let's list the characters we know
|
||||
// will be invalid instead.
|
||||
b'[' if bracket_stack.is_empty()
|
||||
&& matches!(cursor.prev(), b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
|
||||
&& matches!(cursor.prev, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
|
||||
{
|
||||
result[cursor.pos] = b' ';
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
// In Slim the class name shorthand can be followed by a parenthesis. E.g.:
|
||||
|
|
@ -171,17 +114,17 @@ impl PreProcessor for Slim {
|
|||
//
|
||||
// However, we also need to make sure that we keep the parens that are part of the
|
||||
// utility class. E.g.: `bg-(--my-color)`.
|
||||
b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => {
|
||||
b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => {
|
||||
result[cursor.pos] = b' ';
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b'(' | b'[' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
}
|
||||
|
||||
// Consume everything else
|
||||
|
|
@ -373,77 +316,16 @@ mod tests {
|
|||
]
|
||||
"#;
|
||||
|
||||
let expected = "
|
||||
div \n class= bg-blue-500 w-10 h-10 \n ]
|
||||
div \n class= w-10 bg-green-500 h-10 \n ]
|
||||
";
|
||||
let expected = r#"
|
||||
div
|
||||
class= bg-blue-500 w-10 h-10]
|
||||
]
|
||||
div
|
||||
class= w-10 bg-green-500 h-10]
|
||||
]
|
||||
"#;
|
||||
|
||||
Slim::test(input, expected);
|
||||
Slim::test_extract_contains(input, vec!["bg-blue-500", "bg-green-500", "w-10", "h-10"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20386
|
||||
#[test]
|
||||
fn test_embedded_ruby_percent_w_delimiters() {
|
||||
for (input, expected) in [
|
||||
// %w<…>, Slim only counts `[({` nesting in attribute values, so the code must be
|
||||
// wrapped in parentheses to contain spaces
|
||||
(
|
||||
"div class=(%w<bg-blue-500 w-10 h-10>)",
|
||||
"div class= bg-blue-500 w-10 h-10 )",
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
(
|
||||
"div class=(%w<flex <nested> px-2.5>)",
|
||||
"div class= flex <nested> px-2.5 )",
|
||||
),
|
||||
// Custom delimiters
|
||||
("div class=(%w|flex px-2.5|)", "div class= flex px-2.5 )"),
|
||||
("div class=(%W!flex px-2.5!)", "div class= flex px-2.5 )"),
|
||||
(
|
||||
"div class=(%w#text-sm leading-6#)",
|
||||
"div class= text-sm leading-6 )",
|
||||
),
|
||||
(
|
||||
"div class=(%w=italic tracking-wide=)",
|
||||
"div class= italic tracking-wide )",
|
||||
),
|
||||
// Nested paired delimiters stay balanced inside the literal
|
||||
(
|
||||
"div class=(%w[content-['[hello]'] p-4])",
|
||||
"div class= content-['[hello]'] p-4 )",
|
||||
),
|
||||
// Escaped spaces embed a space in a single array element
|
||||
(
|
||||
r#"div class=(%w[foo\ bar baz-1])"#,
|
||||
r#"div class= foo bar baz-1 )"#,
|
||||
),
|
||||
// Ruby control line, which is plain Ruby code
|
||||
("- classes = %w<mt-4 flex>", "- classes = mt-4 flex "),
|
||||
// A `%` that follows a value is not a percent literal
|
||||
("| hit rate 50%w.", "| hit rate 50%w "),
|
||||
] {
|
||||
Slim::test(input, expected);
|
||||
}
|
||||
|
||||
let input = r#"
|
||||
div[
|
||||
class=(%w<bg-blue-500 w-10 h-10>)
|
||||
]
|
||||
- classes = %w|w-10 bg-green-500 h-10|
|
||||
= tag.div class: %W!px-2.5 flex!
|
||||
"#;
|
||||
|
||||
Slim::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"w-10",
|
||||
"h-10",
|
||||
"px-2.5",
|
||||
"flex",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TemplateToolkit;
|
||||
|
||||
impl PreProcessor for TemplateToolkit {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let len = content.len();
|
||||
let mut result = content.to_vec();
|
||||
let mut cursor = cursor::Cursor::new(content);
|
||||
|
||||
while cursor.pos < len {
|
||||
match (cursor.curr(), cursor.next()) {
|
||||
(b'[', b'%') => result[cursor.pos] = b' ',
|
||||
(b'%', b']') => result[cursor.pos + 1] = b' ',
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TemplateToolkit;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_template_toolkit_pre_processor() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
"[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]",
|
||||
" % IF $is_open % bg-white/40 % ELSE % bg-white/10 % END % ",
|
||||
),
|
||||
("[% WRAPPER %]flex[% END %]", " % WRAPPER % flex % END % "),
|
||||
] {
|
||||
TemplateToolkit::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extraction_between_template_tags_works() {
|
||||
TemplateToolkit::test_extract_contains(
|
||||
r#"<div class="[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]"></div>"#,
|
||||
vec!["bg-white/40", "bg-white/10"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
.relative
|
||||
^^^^^^^^
|
||||
- # Blurred background star
|
||||
^^^^^^^^^^ ^^^^
|
||||
.absolute.left-0.z-0{ class: "-top-[400px] -right-[400px]" }
|
||||
^^^^^^^^ ^^^^^^ ^^^ ^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^
|
||||
.flex.justify-end.blur-3xl
|
||||
|
|
@ -74,8 +75,8 @@
|
|||
^^^^^^^^^^ ^^^^^^
|
||||
libraries, our components are designed to be fast, flexible, and easy to
|
||||
^^^ ^^^^^^^^^^ ^^^ ^^^^^^^^ ^^ ^^ ^^^ ^^^^ ^^
|
||||
use <i>directly</i> in Ruby on Rails!
|
||||
^^^ ^^^^^^^^ ^^ ^^
|
||||
use <i>directy</i> in Ruby on Rails!
|
||||
^^^ ^^^^^^^ ^^ ^^
|
||||
|
||||
= doc_example(css: "mt-8 lg:mt-0 lg:basis-3/5 h-44") do
|
||||
^^^ ^^^^ ^^^^^^^ ^^^^^^^^^^^^ ^^^^ ^^
|
||||
|
|
@ -101,7 +102,7 @@
|
|||
|
||||
%p.mt-2
|
||||
^^^^
|
||||
Utilize
|
||||
Utlize
|
||||
= daisy_link("HAML", "https://haml.info/", target: "_blank")
|
||||
^^^^^^
|
||||
so your views are simple, concise, and easy to understand.
|
||||
|
|
@ -119,8 +120,8 @@
|
|||
%p.mt-2
|
||||
^^^^
|
||||
:markdown
|
||||
**PLUS!** You can utilize filters like _Markdown_, CoffeeScript,
|
||||
^^^ ^^^^^^^ ^^^^^^^ ^^^^
|
||||
**PLUS!** You can utlize filters like _Markdown_, CoffeeScript,
|
||||
^^^ ^^^^^^ ^^^^^^^ ^^^^
|
||||
Textile, and many more!
|
||||
^^^ ^^^^ ^^^^^
|
||||
|
||||
|
|
@ -195,6 +196,7 @@
|
|||
^^^^^^^^ ^^^^ ^^^ ^^^^ ^^
|
||||
:escaped
|
||||
- # app/components/character_component.html.haml
|
||||
^^^^
|
||||
= part(:component) do
|
||||
^^
|
||||
= part(:head)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
= daisy_link "DaisyUI", "https://daisyui.com/", target: "_blank"
|
||||
= daisy_link "TailwindCSS", "https://tailwindcss.com/", target: "_blank"
|
||||
libraries, our components are designed to be fast, flexible, and easy to
|
||||
use <i>directly</i> in Ruby on Rails!
|
||||
use <i>directy</i> in Ruby on Rails!
|
||||
|
||||
= doc_example(css: "mt-8 lg:mt-0 lg:basis-3/5 h-44") do
|
||||
.flex.flex-col.sm:flex-row.items-center.gap-4
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
Simple, Concise Views
|
||||
|
||||
%p.mt-2
|
||||
Utilize
|
||||
Utlize
|
||||
= daisy_link("HAML", "https://haml.info/", target: "_blank")
|
||||
so your views are simple, concise, and easy to understand.
|
||||
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
|
||||
%p.mt-2
|
||||
:markdown
|
||||
**PLUS!** You can utilize filters like _Markdown_, CoffeeScript,
|
||||
**PLUS!** You can utlize filters like _Markdown_, CoffeeScript,
|
||||
Textile, and many more!
|
||||
|
||||
.mt-8.xl:mt-0
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Twig;
|
||||
|
||||
impl PreProcessor for Twig {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let len = content.len();
|
||||
let mut result = content.to_vec();
|
||||
let mut cursor = cursor::Cursor::new(content);
|
||||
let mut bracket_stack = vec![];
|
||||
|
||||
const ADD_CLASS: &[u8] = b"addClass";
|
||||
const REMOVE_CLASS: &[u8] = b"removeClass";
|
||||
|
||||
while cursor.pos < len {
|
||||
match (!bracket_stack.is_empty(), cursor.curr()) {
|
||||
// addClass(
|
||||
// ^
|
||||
(false, b'(')
|
||||
if cursor.pos >= ADD_CLASS.len()
|
||||
&& matches!(
|
||||
&content[cursor.pos - ADD_CLASS.len()..cursor.pos],
|
||||
ADD_CLASS
|
||||
) =>
|
||||
{
|
||||
bracket_stack.push(cursor.curr());
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
// removeClass(
|
||||
// ^
|
||||
(false, b'(')
|
||||
if cursor.pos >= REMOVE_CLASS.len()
|
||||
&& matches!(
|
||||
&content[cursor.pos - REMOVE_CLASS.len()..cursor.pos],
|
||||
REMOVE_CLASS
|
||||
) =>
|
||||
{
|
||||
bracket_stack.push(cursor.curr());
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
(true, b'(') => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
|
||||
(true, b')') => {
|
||||
bracket_stack.pop();
|
||||
|
||||
if bracket_stack.is_empty() {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Twig;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_twig_pre_processor() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
// Ensure we don't crash when we encounter an `(`
|
||||
"(p-(--value))",
|
||||
"(p-(--value))",
|
||||
),
|
||||
(
|
||||
// addClass with single argument
|
||||
"addClass(p-(--value))",
|
||||
"addClass p-(--value) ",
|
||||
),
|
||||
(
|
||||
// addClass with single argument
|
||||
"addClass(m-4 p-8 w-full)",
|
||||
"addClass m-4 p-8 w-full ",
|
||||
),
|
||||
(
|
||||
// removeClass with single argument
|
||||
"removeClass(p-(--value))",
|
||||
"removeClass p-(--value) ",
|
||||
),
|
||||
(
|
||||
// removeClass with single argument
|
||||
"removeClass(m-4 p-8 w-full)",
|
||||
"removeClass m-4 p-8 w-full ",
|
||||
),
|
||||
(
|
||||
// Combined with single arguments
|
||||
"addClass(m-(--value))|removeClass(p-(--value))",
|
||||
"addClass m-(--value) |removeClass p-(--value) ",
|
||||
),
|
||||
] {
|
||||
Twig::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19458
|
||||
#[test]
|
||||
fn test_extraction_in_add_class_and_remove_class_works() {
|
||||
// Inside normal HTML
|
||||
let input = r#"
|
||||
<div data-loading="addClass(opacity-50)">
|
||||
<!-- -->
|
||||
</div>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<div data-loading="addClass opacity-50 ">
|
||||
<!-- -->
|
||||
</div>
|
||||
"#;
|
||||
|
||||
Twig::test(input, expected);
|
||||
Twig::test_extract_contains(input, vec!["opacity-50"]);
|
||||
|
||||
// Inside a component
|
||||
let input = r#"
|
||||
<twig:d:Card
|
||||
{{ ...attributes.defaults({
|
||||
as: "a",
|
||||
class: "cursor-pointer bg-base-200 card-sm md:card-md lg:card-lg",
|
||||
role: "button",
|
||||
tabindex: "0",
|
||||
"data-action": "live#$render",
|
||||
"data-loading": "addAttribute(disabled)",
|
||||
"data-poll": "delay(60000)|$render",
|
||||
"data-loading": "addClass(border border-red-500 opacity-75)",
|
||||
})
|
||||
}}
|
||||
>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<twig:d:Card
|
||||
{{ ...attributes.defaults({
|
||||
as: "a",
|
||||
class: "cursor-pointer bg-base-200 card-sm md:card-md lg:card-lg",
|
||||
role: "button",
|
||||
tabindex: "0",
|
||||
"data-action": "live#$render",
|
||||
"data-loading": "addAttribute(disabled)",
|
||||
"data-poll": "delay(60000)|$render",
|
||||
"data-loading": "addClass border border-red-500 opacity-75 ",
|
||||
})
|
||||
}}
|
||||
>
|
||||
"#;
|
||||
|
||||
Twig::test(input, expected);
|
||||
Twig::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
// class
|
||||
"cursor-pointer",
|
||||
"bg-base-200",
|
||||
"card-sm",
|
||||
"md:card-md",
|
||||
"lg:card-lg",
|
||||
// data-loading
|
||||
"border",
|
||||
"border-red-500",
|
||||
"opacity-75",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,15 +15,13 @@ impl PreProcessor for Vue {
|
|||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let mut result = content.to_vec();
|
||||
|
||||
// Only process template tags if content is valid UTF-8
|
||||
if let Ok(content_as_str) = std::str::from_utf8(content) {
|
||||
for (_, [lang, body]) in TEMPLATE_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.map(|c| c.extract())
|
||||
{
|
||||
let replaced = pre_process_input(body.as_bytes().to_vec(), lang);
|
||||
result = result.replace(body, replaced);
|
||||
}
|
||||
let content_as_str = std::str::from_utf8(content).unwrap();
|
||||
for (_, [lang, body]) in TEMPLATE_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.map(|c| c.extract())
|
||||
{
|
||||
let replaced = pre_process_input(body.as_bytes(), lang);
|
||||
result = result.replace(body, replaced);
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -45,16 +43,4 @@ mod tests {
|
|||
|
||||
Vue::test_extract_contains(input, vec!["bg-neutral-900", "text-red-500"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf8_does_not_panic() {
|
||||
// Invalid UTF-8 sequence: 0x80 is a continuation byte without a leading byte
|
||||
let invalid_utf8: &[u8] = &[0x80, 0x81, 0x82];
|
||||
|
||||
let processor = Vue::default();
|
||||
|
||||
// Should not panic, just return the input unchanged
|
||||
let result = processor.process(invalid_utf8);
|
||||
assert_eq!(result, invalid_utf8);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,20 +30,20 @@ impl Machine for StringMachine {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
if Class::Quote != cursor.curr().into() {
|
||||
if Class::Quote != cursor.curr.into() {
|
||||
return MachineState::Idle;
|
||||
}
|
||||
|
||||
// Start of a string
|
||||
let len = cursor.input.len();
|
||||
let start_pos = cursor.pos;
|
||||
let end_char = cursor.curr();
|
||||
let end_char = cursor.curr;
|
||||
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Escape => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
Class::Whitespace => return MachineState::Idle,
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ impl Machine for StringMachine {
|
|||
},
|
||||
|
||||
// End of the string
|
||||
Class::Quote if cursor.curr() == end_char => return self.done(start_pos, cursor),
|
||||
Class::Quote if cursor.curr == end_char => return self.done(start_pos, cursor),
|
||||
|
||||
// Any kind of whitespace is not allowed
|
||||
Class::Whitespace => return MachineState::Idle,
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ impl Machine for UtilityMachine {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// LEGACY: Important marker
|
||||
Class::Exclamation => {
|
||||
self.legacy_important = true;
|
||||
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
// Start of an arbitrary property
|
||||
//
|
||||
// E.g.: `![color:red]`
|
||||
|
|
@ -78,7 +78,7 @@ impl UtilityMachine {
|
|||
fn parse_arbitrary_property(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.arbitrary_property_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// End of arbitrary property, but there is a potential modifier.
|
||||
//
|
||||
// E.g.: `[color:#0088cc]/`
|
||||
|
|
@ -109,7 +109,7 @@ impl UtilityMachine {
|
|||
fn parse_named_utility(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.named_utility_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// End of a named utility, but there is a potential modifier.
|
||||
//
|
||||
// E.g.: `bg-red-500/`
|
||||
|
|
@ -140,7 +140,7 @@ impl UtilityMachine {
|
|||
fn parse_modifier(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.modifier_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// A modifier followed by a modifier is invalid
|
||||
Class::Slash => self.restart(),
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ impl Machine for VariantMachine {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Start of an arbitrary variant
|
||||
//
|
||||
// E.g.: `[&:hover]:`
|
||||
|
|
@ -48,7 +48,7 @@ impl VariantMachine {
|
|||
start_pos: usize,
|
||||
cursor: &mut cursor::Cursor<'_>,
|
||||
) -> MachineState {
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
// End of an arbitrary value, must be followed by a `:`
|
||||
//
|
||||
// E.g.: `[&:hover]:`
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub fn fast_skip(cursor: &Cursor) -> Option<usize> {
|
|||
return None;
|
||||
}
|
||||
|
||||
if !cursor.curr().is_ascii_whitespace() {
|
||||
if !cursor.curr.is_ascii_whitespace() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,11 @@ fn sort_by_dir_and_name(a: &DirEntry, z: &DirEntry) -> Ordering {
|
|||
|
||||
pub fn resolve_globs(
|
||||
base: PathBuf,
|
||||
dirs: &FxHashSet<PathBuf>,
|
||||
dirs: &[PathBuf],
|
||||
extensions: &FxHashSet<String>,
|
||||
) -> Vec<GlobEntry> {
|
||||
let allowed_paths: FxHashSet<PathBuf> = FxHashSet::from_iter(dirs.iter().cloned());
|
||||
|
||||
// A list of known extensions + a list of extensions we found in the project.
|
||||
let mut found_extensions: FxHashSet<String> =
|
||||
FxHashSet::from_iter(KNOWN_EXTENSIONS.iter().map(|x| x.to_string()));
|
||||
|
|
@ -70,7 +72,7 @@ pub fn resolve_globs(
|
|||
continue;
|
||||
}
|
||||
|
||||
if !dirs.contains(path) {
|
||||
if !allowed_paths.contains(path) {
|
||||
let mut path = path;
|
||||
while let Some(parent) = path.parent() {
|
||||
if parent == base {
|
||||
|
|
@ -111,8 +113,7 @@ pub fn resolve_globs(
|
|||
continue;
|
||||
}
|
||||
|
||||
if !dirs.contains(path) {
|
||||
it.skip_current_dir();
|
||||
if !allowed_paths.contains(path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
.git
|
||||
.hg
|
||||
.jj
|
||||
.next
|
||||
.parcel-cache
|
||||
.pnpm-store
|
||||
.svelte-kit
|
||||
.svn
|
||||
.turbo
|
||||
.venv
|
||||
.vercel
|
||||
.yarn
|
||||
__pycache__
|
||||
node_modules
|
||||
.yarn
|
||||
.venv
|
||||
venv
|
||||
.next
|
||||
.turbo
|
||||
.parcel-cache
|
||||
__pycache__
|
||||
.svelte-kit
|
||||
.pnpm-store
|
||||
.vercel
|
||||
|
|
|
|||
|
|
@ -2,5 +2,3 @@ package-lock.json
|
|||
pnpm-lock.yaml
|
||||
bun.lockb
|
||||
.gitignore
|
||||
.env
|
||||
.env.*
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::sync::{self, Arc, Mutex};
|
||||
use tracing_subscriber::fmt::writer::BoxMakeWriter;
|
||||
|
||||
pub static SHOULD_TRACE: sync::LazyLock<bool> = sync::LazyLock::new(
|
||||
|| matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
|
||||
);
|
||||
|
||||
fn dim(input: &str) -> String {
|
||||
format!("\u{001b}[2m{input}\u{001b}[22m")
|
||||
}
|
||||
|
||||
fn blue(input: &str) -> String {
|
||||
format!("\u{001b}[34m{input}\u{001b}[39m")
|
||||
}
|
||||
|
||||
fn highlight(input: &str) -> String {
|
||||
format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`")))
|
||||
}
|
||||
|
||||
struct MutexWriter(Arc<Mutex<std::fs::File>>);
|
||||
|
||||
impl Write for MutexWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.0.lock().unwrap().write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.0.lock().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_tracing() {
|
||||
if !*SHOULD_TRACE {
|
||||
return;
|
||||
}
|
||||
|
||||
let root = Path::new(".tailwindcss");
|
||||
let logs_dir = root.join("logs");
|
||||
if let Err(err) = std::fs::create_dir_all(&logs_dir) {
|
||||
eprintln!(
|
||||
"{} Failed to create {}, skipping debug logs ({err})",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&logs_dir.display().to_string())
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure everything inside `.tailwindcss/` is ignored by git. The file is only created if it
|
||||
// doesn't exist yet, an existing `.gitignore` is left untouched.
|
||||
if let Ok(mut file) = std::fs::File::create_new(root.join(".gitignore")) {
|
||||
_ = file.write_all(b"*\n");
|
||||
}
|
||||
|
||||
let file_path = logs_dir.join(format!(
|
||||
"scanner-{}-{}.log",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0),
|
||||
std::process::id()
|
||||
));
|
||||
let file = match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&file_path)
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{} Failed to create {}, skipping debug logs ({err})",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&file_path.display().to_string())
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let absolute_file_path = dunce::canonicalize(&file_path).unwrap_or_else(|_| file_path.clone());
|
||||
eprintln!(
|
||||
"{} Writing debug info to: {}\n",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&absolute_file_path.display().to_string())
|
||||
);
|
||||
|
||||
let file = Arc::new(Mutex::new(file));
|
||||
|
||||
let writer: BoxMakeWriter = BoxMakeWriter::new({
|
||||
let file = file.clone();
|
||||
move || Box::new(MutexWriter(file.clone())) as Box<dyn Write + Send>
|
||||
});
|
||||
|
||||
_ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
|
||||
.with_writer(writer)
|
||||
.with_ansi(false)
|
||||
.compact()
|
||||
.try_init();
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
pub mod auto_source_detection;
|
||||
pub mod detect_sources;
|
||||
pub mod init_tracing;
|
||||
pub mod sources;
|
||||
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
|
|
@ -15,12 +14,15 @@ use bstr::ByteSlice;
|
|||
use fast_glob::glob_match;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use ignore::{gitignore::GitignoreBuilder, WalkBuilder};
|
||||
use init_tracing::{init_tracing, SHOULD_TRACE};
|
||||
use rayon::prelude::*;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{self, Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
use tracing::event;
|
||||
use tracing_subscriber::fmt::writer::BoxMakeWriter;
|
||||
|
||||
// @source "some/folder"; // This is auto source detection
|
||||
// @source "some/folder/**/*"; // This is auto source detection
|
||||
|
|
@ -32,6 +34,70 @@ use tracing::event;
|
|||
//
|
||||
// @source "do-include-me.bin"; // `.bin` is typically ignored, but now it's explicit so should be included
|
||||
// @source "git-ignored.html"; // A git ignored file that is listed explicitly, should be scanned
|
||||
static SHOULD_TRACE: sync::LazyLock<bool> = sync::LazyLock::new(
|
||||
|| matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
|
||||
);
|
||||
|
||||
fn dim(input: &str) -> String {
|
||||
format!("\u{001b}[2m{input}\u{001b}[22m")
|
||||
}
|
||||
|
||||
fn blue(input: &str) -> String {
|
||||
format!("\u{001b}[34m{input}\u{001b}[39m")
|
||||
}
|
||||
|
||||
fn highlight(input: &str) -> String {
|
||||
format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`")))
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
if !*SHOULD_TRACE {
|
||||
return;
|
||||
}
|
||||
|
||||
let file_path = format!("tailwindcss-{}.log", std::process::id());
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&file_path)
|
||||
.unwrap_or_else(|_| panic!("Failed to open {file_path}"));
|
||||
|
||||
let file_path = Path::new(&file_path);
|
||||
let absolute_file_path = dunce::canonicalize(file_path)
|
||||
.unwrap_or_else(|_| panic!("Failed to canonicalize {file_path:?}"));
|
||||
eprintln!(
|
||||
"{} Writing debug info to: {}\n",
|
||||
dim("[DEBUG]"),
|
||||
highlight(absolute_file_path.as_path().to_str().unwrap())
|
||||
);
|
||||
|
||||
let file = Arc::new(Mutex::new(file));
|
||||
|
||||
let writer: BoxMakeWriter = BoxMakeWriter::new({
|
||||
let file = file.clone();
|
||||
move || Box::new(MutexWriter(file.clone())) as Box<dyn Write + Send>
|
||||
});
|
||||
|
||||
_ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
|
||||
.with_writer(writer)
|
||||
.with_ansi(false)
|
||||
.compact()
|
||||
.try_init();
|
||||
}
|
||||
|
||||
struct MutexWriter(Arc<Mutex<std::fs::File>>);
|
||||
|
||||
impl Write for MutexWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.0.lock().unwrap().write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.0.lock().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChangedContent {
|
||||
|
|
@ -63,33 +129,26 @@ pub struct Scanner {
|
|||
/// The walker to detect all files that we have to scan
|
||||
walker: Option<WalkBuilder>,
|
||||
|
||||
/// All changed content that we have to parse
|
||||
changed_content: Vec<ChangedContent>,
|
||||
|
||||
/// All found extensions
|
||||
extensions: FxHashSet<String>,
|
||||
|
||||
/// All CSS files we want to scan for CSS variable usage
|
||||
css_files: Vec<PathBuf>,
|
||||
|
||||
/// All files that we have to scan
|
||||
files: FxHashSet<PathBuf>,
|
||||
files: Vec<PathBuf>,
|
||||
|
||||
/// All directories, sub-directories, etc… we saw during source detection
|
||||
dirs: FxHashSet<PathBuf>,
|
||||
dirs: Vec<PathBuf>,
|
||||
|
||||
/// All generated globs, used for setting up watchers
|
||||
globs: Option<Vec<GlobEntry>>,
|
||||
|
||||
/// Track unique set of candidates
|
||||
candidates: FxHashSet<String>,
|
||||
|
||||
/// Track mtimes for files so incremental scans can skip unchanged files.
|
||||
mtimes: FxHashMap<PathBuf, SystemTime>,
|
||||
|
||||
/// Files that were scanned during the last `scan()` call.
|
||||
scanned_files: Vec<String>,
|
||||
|
||||
/// Whether we've completed at least one full scan. When false, we skip
|
||||
/// mtime tracking entirely so the initial build stays fast.
|
||||
has_scanned_once: bool,
|
||||
|
||||
/// Whether sources have been scanned since the last `scan()` call
|
||||
sources_scanned: bool,
|
||||
}
|
||||
|
||||
impl Scanner {
|
||||
|
|
@ -104,6 +163,7 @@ impl Scanner {
|
|||
}
|
||||
|
||||
let sources = Sources::new(public_source_entries_to_private_source_entries(sources));
|
||||
|
||||
if *SHOULD_TRACE {
|
||||
event!(tracing::Level::INFO, "Optimized sources:");
|
||||
for source in sources.iter() {
|
||||
|
|
@ -111,27 +171,29 @@ impl Scanner {
|
|||
}
|
||||
}
|
||||
|
||||
let walker = create_walker(&sources);
|
||||
|
||||
Self {
|
||||
sources,
|
||||
walker,
|
||||
sources: sources.clone(),
|
||||
walker: create_walker(sources),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan(&mut self) -> Vec<String> {
|
||||
self.sources_scanned = false;
|
||||
self.scan_sources();
|
||||
|
||||
let (scanned_blobs, css_files, files) = self.discover_sources();
|
||||
// TODO: performance improvement, bail early if we don't have any changed content
|
||||
// if self.changed_content.is_empty() {
|
||||
// return vec![];
|
||||
// }
|
||||
|
||||
self.extract_candidates(scanned_blobs, css_files);
|
||||
self.scanned_files = files;
|
||||
let _new_candidates = self.extract_candidates();
|
||||
|
||||
// Return all candidates sorted
|
||||
let mut result = self.candidates.iter().cloned().collect::<Vec<_>>();
|
||||
result.par_sort_unstable();
|
||||
result
|
||||
// Make sure we have a sorted list of candidates
|
||||
let mut candidates = self.candidates.iter().cloned().collect::<Vec<_>>();
|
||||
candidates.par_sort_unstable();
|
||||
|
||||
// Return all candidates instead of only the new ones
|
||||
candidates
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
|
|
@ -146,7 +208,7 @@ impl Scanner {
|
|||
|
||||
// Raw content can be parsed directly, no need to verify if the file exists and is allowed
|
||||
// to be scanned.
|
||||
let mut content_to_scan: Vec<ChangedContent> = changed_contents;
|
||||
self.changed_content.extend(changed_contents);
|
||||
|
||||
// Fully resolve all files
|
||||
let changed_files = changed_files
|
||||
|
|
@ -170,7 +232,7 @@ impl Scanner {
|
|||
});
|
||||
|
||||
// All known files are allowed to be scanned
|
||||
content_to_scan.extend(known_files);
|
||||
self.changed_content.extend(known_files);
|
||||
|
||||
// Figure out if the new unknown files are allowed to be scanned
|
||||
if !new_unknown_files.is_empty() {
|
||||
|
|
@ -181,11 +243,6 @@ impl Scanner {
|
|||
continue;
|
||||
}
|
||||
|
||||
// The walked path can contain symlinks, while the changed files have already
|
||||
// been canonicalized. Lazily canonicalize the walked path so we can compare
|
||||
// the real paths as well.
|
||||
let mut canonical_path: Option<PathBuf> = None;
|
||||
|
||||
let mut drop_file_indexes = vec![];
|
||||
for (idx, changed_file) in new_unknown_files.iter().enumerate().rev() {
|
||||
let ChangedContent::File(file, _) = changed_file else {
|
||||
|
|
@ -194,17 +251,9 @@ impl Scanner {
|
|||
|
||||
// When the file is found on disk it means that all the rules pass. We can
|
||||
// extract the current file and remove it from the list of passed in files.
|
||||
let matches = file == path
|
||||
|| (file.file_name() == path.file_name() && {
|
||||
if canonical_path.is_none() {
|
||||
canonical_path = dunce::canonicalize(path).ok();
|
||||
}
|
||||
canonical_path.as_deref() == Some(file.as_path())
|
||||
});
|
||||
|
||||
if matches {
|
||||
self.files.insert(path.to_path_buf()); // Track for future use
|
||||
content_to_scan.push(changed_file.clone()); // Track for parsing
|
||||
if file == path {
|
||||
self.files.push(path.to_path_buf()); // Track for future use
|
||||
self.changed_content.push(changed_file.clone()); // Track for parsing
|
||||
drop_file_indexes.push(idx);
|
||||
}
|
||||
}
|
||||
|
|
@ -225,17 +274,18 @@ impl Scanner {
|
|||
}
|
||||
}
|
||||
|
||||
// Read all content into blobs for extraction
|
||||
let blobs = read_all_files(content_to_scan);
|
||||
self.extract_candidates(blobs, vec![])
|
||||
self.extract_candidates()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn extract_candidates(&mut self, blobs: Vec<Vec<u8>>, css_files: Vec<PathBuf>) -> Vec<String> {
|
||||
// Extract all candidates from the pre-read blobs
|
||||
let mut new_candidates = parse_all_blobs(blobs);
|
||||
fn extract_candidates(&mut self) -> Vec<String> {
|
||||
let changed_content = self.changed_content.drain(..).collect::<Vec<_>>();
|
||||
|
||||
// Extract all candidates from the changed content
|
||||
let mut new_candidates = parse_all_blobs(read_all_files(changed_content));
|
||||
|
||||
// Extract all CSS variables from the CSS files
|
||||
let css_files = self.css_files.drain(..).collect::<Vec<_>>();
|
||||
if !css_files.is_empty() {
|
||||
let css_variables = extract_css_variables(read_all_files(
|
||||
css_files
|
||||
|
|
@ -247,23 +297,63 @@ impl Scanner {
|
|||
new_candidates.extend(css_variables);
|
||||
}
|
||||
|
||||
// Only keep candidates we haven't seen before
|
||||
for existing in self.candidates.iter() {
|
||||
new_candidates.remove(existing);
|
||||
}
|
||||
// Only compute the new candidates and ignore the ones we already have. This is for
|
||||
// subsequent calls to prevent serializing the entire set of candidates every time.
|
||||
let mut new_candidates = new_candidates
|
||||
.into_par_iter()
|
||||
.filter(|candidate| !self.candidates.contains(candidate))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
new_candidates.par_sort_unstable();
|
||||
|
||||
// Track new candidates for subsequent calls
|
||||
self.candidates.extend(new_candidates.iter().cloned());
|
||||
self.candidates.par_extend(new_candidates.clone());
|
||||
|
||||
let mut result: Vec<String> = new_candidates.into_iter().collect();
|
||||
result.par_sort_unstable();
|
||||
new_candidates
|
||||
}
|
||||
|
||||
result
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn scan_sources(&mut self) {
|
||||
let Some(walker) = &mut self.walker else {
|
||||
return;
|
||||
};
|
||||
|
||||
for entry in walker.build().filter_map(Result::ok) {
|
||||
let path = entry.into_path();
|
||||
let Ok(metadata) = path.metadata() else {
|
||||
continue;
|
||||
};
|
||||
if metadata.is_dir() {
|
||||
self.dirs.push(path);
|
||||
} else if metadata.is_file() {
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default(); // In case the file has no extension
|
||||
|
||||
match extension {
|
||||
// Special handing for CSS files, we don't want to extract candidates from
|
||||
// these files, but we do want to extract used CSS variables.
|
||||
"css" => {
|
||||
self.css_files.push(path.clone());
|
||||
}
|
||||
_ => {
|
||||
self.changed_content.push(ChangedContent::File(
|
||||
path.to_path_buf(),
|
||||
extension.to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.extensions.insert(extension.to_owned());
|
||||
self.files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_files(&mut self) -> Vec<String> {
|
||||
let _ = self.discover_sources();
|
||||
self.scan_sources();
|
||||
|
||||
self.files
|
||||
.par_iter()
|
||||
|
|
@ -271,18 +361,13 @@ impl Scanner {
|
|||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_scanned_files(&self) -> Vec<String> {
|
||||
self.scanned_files.clone()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_globs(&mut self) -> Vec<GlobEntry> {
|
||||
if let Some(globs) = &self.globs {
|
||||
return globs.clone();
|
||||
}
|
||||
|
||||
let _ = self.discover_sources();
|
||||
self.scan_sources();
|
||||
|
||||
let mut globs = vec![];
|
||||
for source in self.sources.iter() {
|
||||
|
|
@ -356,167 +441,21 @@ impl Scanner {
|
|||
let i = s.as_ptr() as usize - offset;
|
||||
let original = &original_content[i..i + s.len()];
|
||||
if original.contains_str("-[]") {
|
||||
return String::from_utf8(original.to_vec())
|
||||
.ok()
|
||||
.map(|candidate| (candidate, i));
|
||||
return Some(unsafe {
|
||||
(String::from_utf8_unchecked(original.to_vec()), i)
|
||||
});
|
||||
}
|
||||
|
||||
String::from_utf8(s.to_vec())
|
||||
.ok()
|
||||
.map(|candidate| (candidate, i))
|
||||
// SAFETY: When we parsed the candidates, we already guaranteed that the byte
|
||||
// slices are valid, therefore we don't have to re-check here when we want to
|
||||
// convert it back to a string.
|
||||
Some(unsafe { (String::from_utf8_unchecked(s.to_vec()), i) })
|
||||
}
|
||||
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn discover_sources(&mut self) -> (Vec<Vec<u8>>, Vec<PathBuf>, Vec<String>) {
|
||||
if self.sources_scanned {
|
||||
return (vec![], vec![], vec![]);
|
||||
}
|
||||
self.sources_scanned = true;
|
||||
|
||||
let Some(walker) = &mut self.walker else {
|
||||
return (vec![], vec![], vec![]);
|
||||
};
|
||||
|
||||
// Use synchronous walk for the initial build (lower overhead) and parallel
|
||||
// walk for subsequent calls (watch mode) where the overhead is amortised.
|
||||
let all_entries = if self.has_scanned_once {
|
||||
walk_parallel(walker)
|
||||
} else {
|
||||
walk_synchronous(walker)
|
||||
};
|
||||
|
||||
let mut css_files: Vec<PathBuf> = vec![];
|
||||
let mut content_paths: Vec<(PathBuf, String)> = vec![];
|
||||
let mut changed_files = vec![];
|
||||
|
||||
// Fresh state
|
||||
self.files.clear();
|
||||
self.dirs.clear();
|
||||
self.extensions.clear();
|
||||
self.globs = None;
|
||||
|
||||
// Cache canonicalized folders in case a file itself is not symlinked, but any of the parent
|
||||
// folders are symlinked.
|
||||
let mut cached_canonical_dirs: FxHashMap<PathBuf, PathBuf> = FxHashMap::default();
|
||||
|
||||
for entry in all_entries {
|
||||
match entry {
|
||||
WalkEntry::Dir(path) => {
|
||||
self.dirs.insert(path);
|
||||
}
|
||||
WalkEntry::File {
|
||||
path,
|
||||
mtime,
|
||||
is_symlink,
|
||||
} => {
|
||||
// Deduplicate: parallel walk can visit the same file from multiple threads
|
||||
if !self.files.insert(path.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track canonicalized paths in addition to potentially symlinked file paths
|
||||
let canonical = if is_symlink {
|
||||
dunce::canonicalize(&path).ok()
|
||||
} else {
|
||||
path.parent().and_then(|parent| {
|
||||
// Perf: cache the canonicalized parent path such that sibling files don't
|
||||
// have to canonicalize over and over again.
|
||||
let canonical_parent = cached_canonical_dirs
|
||||
.entry(parent.to_path_buf())
|
||||
.or_insert_with(|| {
|
||||
dunce::canonicalize(parent)
|
||||
.unwrap_or_else(|_| parent.to_path_buf())
|
||||
});
|
||||
|
||||
if canonical_parent.as_path() != parent {
|
||||
path.file_name()
|
||||
.map(|file_name| canonical_parent.join(file_name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(canonical) = canonical {
|
||||
if canonical != path {
|
||||
self.files.insert(canonical);
|
||||
}
|
||||
}
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
|
||||
self.extensions.insert(extension.to_owned());
|
||||
|
||||
// On incremental scans, check mtime to skip unchanged files.
|
||||
// On the first scan, track mtimes while still scanning every file.
|
||||
let changed = if self.has_scanned_once {
|
||||
match mtime {
|
||||
Some(mtime) => {
|
||||
let prev = self.mtimes.insert(path.clone(), mtime);
|
||||
prev.is_none_or(|prev| prev != mtime)
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
} else {
|
||||
if let Some(mtime) = mtime {
|
||||
self.mtimes.insert(path.clone(), mtime);
|
||||
}
|
||||
|
||||
true
|
||||
};
|
||||
|
||||
if !changed {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(file) = path.clone().into_os_string().into_string() {
|
||||
changed_files.push(file);
|
||||
}
|
||||
|
||||
match extension.as_str() {
|
||||
// Special handing for CSS files, we don't want to extract candidates from
|
||||
// these files, but we do want to extract used CSS variables.
|
||||
"css" => css_files.push(path),
|
||||
_ => content_paths.push((path, extension)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure `mtimes` don't include stale files
|
||||
self.mtimes.retain(|path, _| self.files.contains(path));
|
||||
|
||||
// Read + preprocess all discovered files in parallel
|
||||
let scanned_blobs: Vec<Vec<u8>> = content_paths
|
||||
.into_par_iter()
|
||||
.filter_map(|(path, ext)| {
|
||||
let content = std::fs::read(&path).ok()?;
|
||||
event!(tracing::Level::INFO, "Reading {:?}", path);
|
||||
let processed = pre_process_input(content, &ext);
|
||||
if processed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(processed)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !self.has_scanned_once {
|
||||
self.has_scanned_once = true;
|
||||
}
|
||||
|
||||
changed_files.par_sort_unstable();
|
||||
|
||||
(scanned_blobs, css_files, changed_files)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_changed_content(c: ChangedContent) -> Option<Vec<u8>> {
|
||||
|
|
@ -535,28 +474,26 @@ fn read_changed_content(c: ChangedContent) -> Option<Vec<u8>> {
|
|||
ChangedContent::Content(contents, extension) => (contents.into_bytes(), extension),
|
||||
};
|
||||
|
||||
Some(pre_process_input(content, &extension))
|
||||
Some(pre_process_input(&content, &extension))
|
||||
}
|
||||
|
||||
pub fn pre_process_input(content: Vec<u8>, extension: &str) -> Vec<u8> {
|
||||
pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
|
||||
use crate::extractor::pre_processors::*;
|
||||
|
||||
match extension {
|
||||
"clj" | "cljs" | "cljc" => Clojure.process(&content),
|
||||
"heex" | "eex" | "ex" | "exs" => Elixir.process(&content),
|
||||
"cshtml" | "razor" => Razor.process(&content),
|
||||
"haml" => Haml.process(&content),
|
||||
"json" | "jsonl" | "ndjson" => Json.process(&content),
|
||||
"md" | "mdx" => Markdown.process(&content),
|
||||
"pug" => Pug.process(&content),
|
||||
"rb" | "erb" => Ruby.process(&content),
|
||||
"slim" | "slang" => Slim.process(&content),
|
||||
"svelte" => Svelte.process(&content),
|
||||
"rs" => Rust.process(&content),
|
||||
"tt" | "tt2" | "tx" => TemplateToolkit.process(&content),
|
||||
"twig" => Twig.process(&content),
|
||||
"vue" => Vue.process(&content),
|
||||
_ => content,
|
||||
"clj" | "cljs" | "cljc" => Clojure.process(content),
|
||||
"heex" | "eex" | "ex" | "exs" => Elixir.process(content),
|
||||
"cshtml" | "razor" => Razor.process(content),
|
||||
"haml" => Haml.process(content),
|
||||
"json" => Json.process(content),
|
||||
"md" | "mdx" => Markdown.process(content),
|
||||
"pug" => Pug.process(content),
|
||||
"rb" | "erb" => Ruby.process(content),
|
||||
"slim" | "slang" => Slim.process(content),
|
||||
"svelte" => Svelte.process(content),
|
||||
"rs" => Rust.process(content),
|
||||
"vue" => Vue.process(content),
|
||||
_ => content.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -575,23 +512,23 @@ fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn extract_css_variables(blobs: Vec<Vec<u8>>) -> FxHashSet<String> {
|
||||
fn extract_css_variables(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
||||
extract(blobs, |mut extractor| {
|
||||
extractor.extract_variables_from_css()
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> FxHashSet<String> {
|
||||
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
||||
extract(blobs, |mut extractor| extractor.extract())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn extract<H>(blobs: Vec<Vec<u8>>, handle: H) -> FxHashSet<String>
|
||||
fn extract<H>(blobs: Vec<Vec<u8>>, handle: H) -> Vec<String>
|
||||
where
|
||||
H: Fn(Extractor) -> Vec<Extracted> + std::marker::Sync,
|
||||
{
|
||||
blobs
|
||||
let mut result: Vec<_> = blobs
|
||||
.par_iter()
|
||||
.flat_map(|blob| blob.par_split(|x| *x == b'\n'))
|
||||
.filter_map(|blob| {
|
||||
|
|
@ -616,113 +553,26 @@ where
|
|||
a
|
||||
})
|
||||
.into_iter()
|
||||
.filter_map(|s| String::from_utf8(s.to_vec()).ok())
|
||||
.collect()
|
||||
.map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) })
|
||||
.collect();
|
||||
|
||||
// SAFETY: Unstable sort is faster and in this scenario it's also safe because we are
|
||||
// guaranteed to have unique candidates.
|
||||
result.par_sort_unstable();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum WalkEntry {
|
||||
Dir(PathBuf),
|
||||
File {
|
||||
path: PathBuf,
|
||||
mtime: Option<SystemTime>,
|
||||
|
||||
/// Whether the path itself is a symlink
|
||||
is_symlink: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ignore::DirEntry> for WalkEntry {
|
||||
fn from(entry: ignore::DirEntry) -> Self {
|
||||
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
|
||||
let is_symlink = entry.path_is_symlink();
|
||||
let path = entry.into_path();
|
||||
|
||||
if is_dir {
|
||||
WalkEntry::Dir(path)
|
||||
} else {
|
||||
let mtime = path.metadata().ok().and_then(|m| m.modified().ok());
|
||||
WalkEntry::File {
|
||||
path,
|
||||
mtime,
|
||||
is_symlink,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk the file system synchronously. Used for the initial build where the overhead of spawning
|
||||
/// parallel walker threads is not worth it.
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn walk_synchronous(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
||||
walker
|
||||
.build()
|
||||
.filter_map(Result::ok)
|
||||
.map(WalkEntry::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Walk the file system in parallel. Used in watch mode where the parallel walker overhead is
|
||||
/// amortised across many rebuilds and subsequent calls are much faster.
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn walk_parallel(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
||||
struct FlushOnDrop {
|
||||
local: Vec<WalkEntry>,
|
||||
shared: Arc<Mutex<Vec<WalkEntry>>>,
|
||||
}
|
||||
|
||||
impl Drop for FlushOnDrop {
|
||||
fn drop(&mut self) {
|
||||
if !self.local.is_empty() {
|
||||
self.shared.lock().unwrap().append(&mut self.local);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let collected: Arc<Mutex<Vec<WalkEntry>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
walker.build_parallel().run(|| {
|
||||
let mut buf = FlushOnDrop {
|
||||
local: Vec::with_capacity(256),
|
||||
shared: collected.clone(),
|
||||
};
|
||||
|
||||
Box::new(move |entry| {
|
||||
let Ok(entry) = entry else {
|
||||
return ignore::WalkState::Continue;
|
||||
};
|
||||
|
||||
buf.local.push(WalkEntry::from(entry));
|
||||
|
||||
if buf.local.len() >= 256 {
|
||||
buf.shared.lock().unwrap().append(&mut buf.local);
|
||||
}
|
||||
|
||||
ignore::WalkState::Continue
|
||||
})
|
||||
});
|
||||
|
||||
// All threads have finished and flushed their buffers via FlushOnDrop::drop
|
||||
Arc::try_unwrap(collected).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
/// Sets up a WalkBuilder with all source roots, gitignore rules, and source pattern matching.
|
||||
/// Create a walker for the given sources to detect all the files that we have to scan.
|
||||
///
|
||||
/// This is the common setup shared between the full walker (with mtime tracking for re-scans)
|
||||
/// and the parallel walker (without mtime tracking for the initial scan).
|
||||
fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
||||
/// The `mtimes` map is used to keep track of the last modified time of each file. This is used to
|
||||
/// determine if a file or folder has changed since the last scan and we can skip folders that
|
||||
/// haven't changed.
|
||||
fn create_walker(sources: Sources) -> Option<WalkBuilder> {
|
||||
let mtimes: Arc<Mutex<FxHashMap<PathBuf, SystemTime>>> = Default::default();
|
||||
let mut other_roots: FxHashSet<&PathBuf> = FxHashSet::default();
|
||||
let mut first_root: Option<&PathBuf> = None;
|
||||
|
||||
let mut ignores: Vec<(&PathBuf, Vec<String>)> = Default::default();
|
||||
let mut emit = |base, pattern| match ignores.last_mut() {
|
||||
Some((prev_base, patterns)) if *prev_base == base => {
|
||||
patterns.push(pattern);
|
||||
}
|
||||
_ => {
|
||||
ignores.push((base, vec![pattern]));
|
||||
}
|
||||
};
|
||||
let mut ignores: BTreeMap<&PathBuf, BTreeSet<String>> = Default::default();
|
||||
|
||||
for source in sources.iter() {
|
||||
match source {
|
||||
|
|
@ -734,7 +584,7 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
}
|
||||
}
|
||||
SourceEntry::Pattern { base, pattern } => {
|
||||
let pattern = pattern.to_owned();
|
||||
let mut pattern = pattern.to_string();
|
||||
|
||||
if first_root.is_none() {
|
||||
first_root = Some(base);
|
||||
|
|
@ -743,19 +593,35 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
}
|
||||
|
||||
if !pattern.contains("**") {
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
if !pattern.starts_with("/") {
|
||||
pattern = format!("/{pattern}");
|
||||
}
|
||||
|
||||
// Specific patterns should take precedence even over git-ignored files:
|
||||
emit(base, format!("!{}", pattern));
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(format!("!{}", pattern));
|
||||
} else {
|
||||
// Assumption: the pattern we receive will already be brace expanded. So
|
||||
// `*.{html,jsx}` will result in two separate patterns: `*.html` and `*.jsx`.
|
||||
if let Some(extension) = Path::new(&pattern).extension() {
|
||||
// Extend auto source detection to include the extension
|
||||
emit(base, format!("!*.{}", extension.to_string_lossy()));
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(format!("!*.{}", extension.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
}
|
||||
SourceEntry::Ignored { base, pattern } => {
|
||||
emit(base, pattern.to_owned());
|
||||
let mut pattern = pattern.to_string();
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
if !pattern.starts_with("/") {
|
||||
pattern = format!("/{pattern}");
|
||||
}
|
||||
ignores.entry(base).or_default().insert(pattern);
|
||||
}
|
||||
SourceEntry::External { base } => {
|
||||
if first_root.is_none() {
|
||||
|
|
@ -765,10 +631,16 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
}
|
||||
|
||||
// External sources should take precedence even over git-ignored files:
|
||||
emit(base, "!/**/*".to_owned());
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(format!("!{}", "/**/*"));
|
||||
|
||||
// External sources should still disallow binary extensions:
|
||||
emit(base, BINARY_EXTENSIONS_GLOB.clone());
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(BINARY_EXTENSIONS_GLOB.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -845,61 +717,75 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
builder.add_gitignore(ignore);
|
||||
}
|
||||
|
||||
// Pre-compute source matching data to avoid allocations in the hot filter_entry path
|
||||
let auto_bases: Vec<PathBuf> = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => Some(base.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
builder.filter_entry({
|
||||
move |entry| {
|
||||
let path = entry.path();
|
||||
|
||||
let pattern_sources: Vec<(PathBuf, String)> = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Pattern { base, pattern } => Some((base.into(), pattern.into())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Source pattern matching filter (lock-free, safe for parallel walking)
|
||||
builder.filter_entry(move |entry| {
|
||||
let path = entry.path();
|
||||
|
||||
// Ensure the entries are matching any of the provided source patterns (this is
|
||||
// necessary for manual-patterns that can filter the file extension)
|
||||
if path.is_file() {
|
||||
let mut matches = false;
|
||||
|
||||
for base in &auto_bases {
|
||||
if path.starts_with(base) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !matches {
|
||||
for (base, pattern) in &pattern_sources {
|
||||
let remainder = path.strip_prefix(base);
|
||||
if remainder.is_ok_and(|remainder| {
|
||||
let mut path_str = remainder.to_string_lossy().to_string();
|
||||
if !path_str.starts_with("/") {
|
||||
path_str = format!("/{path_str}");
|
||||
// Ensure the entries are matching any of the provided source patterns (this is
|
||||
// necessary for manual-patterns that can filter the file extension)
|
||||
if path.is_file() {
|
||||
let mut matches = false;
|
||||
for source in sources.iter() {
|
||||
match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => {
|
||||
if path.starts_with(base) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
glob_match(pattern, path_str.as_bytes())
|
||||
}) {
|
||||
matches = true;
|
||||
break;
|
||||
SourceEntry::Pattern { base, pattern } => {
|
||||
let mut pattern = pattern.to_string();
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
if !pattern.starts_with("/") {
|
||||
pattern = format!("/{pattern}");
|
||||
}
|
||||
|
||||
// Check if path starts with base, if so, remove the prefix and check the remainder against the pattern
|
||||
let remainder = path.strip_prefix(base);
|
||||
if remainder.is_ok_and(|remainder| {
|
||||
let mut path_str = remainder.to_string_lossy().to_string();
|
||||
if !path_str.starts_with("/") {
|
||||
path_str = format!("/{path_str}");
|
||||
}
|
||||
glob_match(pattern, path_str.as_bytes())
|
||||
}) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !matches {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !matches {
|
||||
return false;
|
||||
let mut mtimes = mtimes.lock().unwrap();
|
||||
let current_time = match entry.metadata() {
|
||||
Ok(metadata) if metadata.is_file() => {
|
||||
if let Ok(time) = metadata.modified() {
|
||||
Some(time)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let previous_time =
|
||||
current_time.and_then(|time| mtimes.insert(entry.clone().into_path(), time));
|
||||
|
||||
match (current_time, previous_time) {
|
||||
(Some(current), Some(prev)) if prev == current => false,
|
||||
_ => {
|
||||
event!(tracing::Level::INFO, "Discovering {:?}", path);
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
Some(builder)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
use crate::glob::split_pattern;
|
||||
use crate::GlobEntry;
|
||||
use bexpand::Expression;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use ignore::gitignore::Gitignore;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{event, Level};
|
||||
|
||||
use super::auto_source_detection::IGNORED_CONTENT_DIRS;
|
||||
|
|
@ -50,14 +49,13 @@ pub enum SourceEntry {
|
|||
/// ```
|
||||
Ignored { base: PathBuf, pattern: String },
|
||||
|
||||
/// External sources are directories that are ignored (by us or .gitignore rules), but should be
|
||||
/// included bypassing the default ignore rules.
|
||||
/// External sources are sources outside of your git root which should not
|
||||
/// follow gitignore rules.
|
||||
///
|
||||
/// Represented by:
|
||||
///
|
||||
/// ```css
|
||||
/// @source "../node_modules/my-lib";`
|
||||
/// @source "../node_modules/my-lib/**/*";`
|
||||
/// ```
|
||||
External { base: PathBuf },
|
||||
}
|
||||
|
|
@ -77,164 +75,6 @@ impl Sources {
|
|||
}
|
||||
}
|
||||
|
||||
/// When dealing with a pattern, then it could be that we end up with:
|
||||
///
|
||||
/// ```json
|
||||
/// { base: '/some/folder', pattern: 'foo.ts' }
|
||||
/// ```
|
||||
///
|
||||
/// If we just emit `!foo.ts` for the `/some/folder` path, then _everything_ else in that folder
|
||||
/// would still be walked (but the result will be ignored).
|
||||
///
|
||||
/// Instead, we have to ensure that we ignore everything in that folder _except_ for the `foo.ts`
|
||||
/// pattern.
|
||||
///
|
||||
/// This should be equivalent to:
|
||||
/// ```gitignore
|
||||
/// *
|
||||
/// !foo.ts
|
||||
/// ```
|
||||
///
|
||||
/// However, we have to be careful that we don't start ignoring files/folders that already exist.
|
||||
/// ```css
|
||||
/// @source "./some/folder/foo.ts";
|
||||
/// @source "./some/folder/bar.ts";
|
||||
/// ```
|
||||
/// Would result in:
|
||||
/// ```json
|
||||
/// { base: '/some/folder', pattern: 'foo.ts' }
|
||||
/// { base: '/some/folder', pattern: 'bar.ts' }
|
||||
/// ```
|
||||
///
|
||||
/// If we were to blindly emit `*` for each pattern, then the `.gitignore` equivalent would look like
|
||||
/// this:
|
||||
/// ```gitignore
|
||||
/// *
|
||||
/// !foo.ts
|
||||
/// *
|
||||
/// !bar.ts
|
||||
/// ```
|
||||
///
|
||||
/// This would result in ignoring the `foo.ts` file as well. Therefore we only want to insert
|
||||
/// this `*` pattern when nothing else exists yet.
|
||||
///
|
||||
/// There is another problem that we need to solve. Let's say you have a pattern that contains a `*`
|
||||
/// in the pattern:
|
||||
/// ```css
|
||||
/// @source './src/ba*/*.html';
|
||||
/// ```
|
||||
///
|
||||
/// This would result in
|
||||
/// ```json
|
||||
/// { base: '/src', pattern: '/ba*/*.html' }
|
||||
/// ```
|
||||
///
|
||||
/// If we now inject the `*` pattern for the `/src` folder, then we wouldn't scan any `ba*` folders
|
||||
/// (e.g. `bar` or `baz`). For this, we have to make sure that we add inverse patterns for these
|
||||
/// folders. This would essentially result in:
|
||||
/// ```gitignore
|
||||
/// * ← ignore everything
|
||||
/// !/ba*/ ← except for the `ba*/` pattern, so we scan these folders
|
||||
/// !/ba*/*.html ← then ensure we scan the `*.html` files in it as well
|
||||
/// ```
|
||||
///
|
||||
fn expand_restricted_patterns(sources: Vec<SourceEntry>) -> Vec<SourceEntry> {
|
||||
let unrestricted_roots = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => Some(base.clone()),
|
||||
SourceEntry::Pattern { base, pattern } if pattern.contains("**") => Some(base.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Bases of restricted patterns. Each of these becomes its own walk root with its own
|
||||
// `*` + `!<pattern>` rules, so an ancestor base must not ignore them recursively.
|
||||
let pattern_roots = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Pattern { base, .. } => Some(base.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut restricted_roots: FxHashSet<PathBuf> = FxHashSet::default();
|
||||
let mut expanded = vec![];
|
||||
|
||||
for source in sources {
|
||||
let SourceEntry::Pattern { base, pattern } = &source else {
|
||||
expanded.push(source);
|
||||
continue;
|
||||
};
|
||||
|
||||
// `base` is already included by another `@source` that we know should be walked. This
|
||||
// includes the case where `base` is _nested_ inside such a root, because everything under
|
||||
// an unrestricted root is walked already. Restricting it would incorrectly hide siblings
|
||||
// that the broader source is supposed to pick up.
|
||||
if unrestricted_roots.iter().any(|root| base.starts_with(root)) {
|
||||
expanded.push(source);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore everything in the directory. We will later add the specific patterns we are
|
||||
// interested in.
|
||||
if restricted_roots.insert(base.clone()) {
|
||||
// When another source root is nested inside this base — an unrestricted root, or the
|
||||
// base of another restricted pattern (which is walked from its own root with its own
|
||||
// rules) — only ignore direct children so the nested root can still be walked.
|
||||
let has_nested_root = unrestricted_roots.iter().any(|root| root.starts_with(base))
|
||||
|| pattern_roots
|
||||
.iter()
|
||||
.any(|root| root != base && root.starts_with(base));
|
||||
|
||||
let pattern = if has_nested_root { "/*" } else { "*" };
|
||||
|
||||
expanded.push(SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: pattern.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure to _include_ parent paths, otherwise the `*` from above would block walking the
|
||||
// folders that need to be walked.
|
||||
//
|
||||
// ```css
|
||||
// @source './src/ba*/*.html';
|
||||
// ```
|
||||
//
|
||||
// ```gitignore
|
||||
// * ← added by the above rule
|
||||
// !/ba*/ ← this is what we're focusing on in this block
|
||||
// !/ba*/*.html ← this is added later
|
||||
// ```
|
||||
{
|
||||
let mut dir = PathBuf::new();
|
||||
let mut components = Path::new(pattern).components().peekable();
|
||||
|
||||
while let Some(component) = components.next() {
|
||||
if components.peek().is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
match component {
|
||||
Component::Prefix(_) | Component::RootDir | Component::CurDir => continue,
|
||||
Component::ParentDir | Component::Normal(_) => dir.push(component),
|
||||
}
|
||||
|
||||
expanded.push(SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: format!("!/{}/", path_to_posix_string(&dir).trim_start_matches('/')),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Track the original source
|
||||
expanded.push(source);
|
||||
}
|
||||
|
||||
expanded
|
||||
}
|
||||
|
||||
impl PublicSourceEntry {
|
||||
/// Optimize the PublicSourceEntry by trying to move all the static parts of the pattern to the
|
||||
/// base of the PublicSourceEntry.
|
||||
|
|
@ -262,456 +102,72 @@ impl PublicSourceEntry {
|
|||
/// resolved path.
|
||||
pub fn optimize(&mut self) {
|
||||
// Resolve base path immediately
|
||||
let Ok(mut base) = dunce::canonicalize(&self.base) else {
|
||||
let Ok(base) = dunce::canonicalize(&self.base) else {
|
||||
event!(Level::ERROR, "Failed to resolve base: {:?}", self.base);
|
||||
return;
|
||||
};
|
||||
self.base = base.to_string_lossy().to_string();
|
||||
|
||||
let mut new_pattern = PathBuf::new();
|
||||
enum ComponentStage {
|
||||
Base,
|
||||
Pattern,
|
||||
// No dynamic part, figure out if we are dealing with a file or a directory.
|
||||
if !self.pattern.contains('*') {
|
||||
let combined_path = if self.pattern.starts_with("/") {
|
||||
PathBuf::from(&self.pattern)
|
||||
} else {
|
||||
PathBuf::from(&self.base).join(&self.pattern)
|
||||
};
|
||||
|
||||
match dunce::canonicalize(combined_path) {
|
||||
Ok(resolved_path) if resolved_path.is_dir() => {
|
||||
self.base = resolved_path.to_string_lossy().to_string();
|
||||
self.pattern = "**/*".to_owned();
|
||||
}
|
||||
Ok(resolved_path) if resolved_path.is_file() => {
|
||||
self.base = resolved_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
// Ensure leading slash, otherwise it will match against all files in all folders/
|
||||
self.pattern =
|
||||
format!("/{}", resolved_path.file_name().unwrap().to_string_lossy());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut stage = ComponentStage::Base;
|
||||
|
||||
let mut components = Path::new(&self.pattern).components().peekable();
|
||||
while let Some(component) = components.next() {
|
||||
match stage {
|
||||
ComponentStage::Base => {
|
||||
match component {
|
||||
// Ignore the current dir, e.g. `.`
|
||||
Component::CurDir => {}
|
||||
// Contains dynamic part
|
||||
let (static_part, dynamic_part) = split_pattern(&self.pattern);
|
||||
|
||||
// Go up a directory, e.g. `..`
|
||||
Component::ParentDir => {
|
||||
base.pop();
|
||||
}
|
||||
|
||||
// Once we hit a component that contains a wildcard character, then we
|
||||
// can't change the base anymore and we must move to the pattern part.
|
||||
Component::Normal(part) if part.to_string_lossy().contains("*") => {
|
||||
new_pattern.push(component);
|
||||
stage = ComponentStage::Pattern;
|
||||
}
|
||||
|
||||
// File or folder, but not the last component
|
||||
Component::Normal(part) if components.peek().is_some() => {
|
||||
base.push(part);
|
||||
}
|
||||
|
||||
// Last file or folder. If it's a folder, we move it to the base,
|
||||
// otherwise we move it to the pattern.
|
||||
Component::Normal(part) => {
|
||||
let full_path = base.join(part);
|
||||
if full_path.is_dir() {
|
||||
base.push(part);
|
||||
} else {
|
||||
new_pattern.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
// When we're dealing with an absolute path, then we have to bypass the
|
||||
// `base` entirely.
|
||||
Component::Prefix(_) => {
|
||||
base.clear();
|
||||
base.push(component);
|
||||
}
|
||||
Component::RootDir => {
|
||||
#[cfg(not(windows))]
|
||||
base.clear();
|
||||
base.push(component);
|
||||
}
|
||||
let base: PathBuf = self.base.clone().into();
|
||||
let base = match static_part {
|
||||
Some(static_part) => {
|
||||
// TODO: If the base does not exist on disk, try removing the last slash and try
|
||||
// again.
|
||||
match dunce::canonicalize(base.join(static_part)) {
|
||||
Ok(base) => base,
|
||||
Err(err) => {
|
||||
event!(tracing::Level::ERROR, "Failed to resolve glob: {:?}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ComponentStage::Pattern => {
|
||||
new_pattern.push(component);
|
||||
}
|
||||
None => base,
|
||||
};
|
||||
|
||||
let pattern = match dynamic_part {
|
||||
Some(dynamic_part) => dynamic_part,
|
||||
None => {
|
||||
if base.is_dir() {
|
||||
"**/*".to_owned()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.base = base.to_string_lossy().to_string();
|
||||
self.pattern = path_to_posix_string(&new_pattern);
|
||||
|
||||
// Ensure we have `**/*` when the base is a folder and we don't have a pattern at all
|
||||
if self.pattern == "" {
|
||||
self.pattern = "/**/*".to_owned();
|
||||
}
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
else if !self.pattern.starts_with("/") {
|
||||
self.pattern = format!("/{}", self.pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_posix_string(path: &Path) -> String {
|
||||
let mut parts = Vec::new();
|
||||
let mut is_rooted = false;
|
||||
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Prefix(prefix) => {
|
||||
parts.push(prefix.as_os_str().to_string_lossy().to_string());
|
||||
}
|
||||
Component::RootDir => {
|
||||
is_rooted = true;
|
||||
if parts.is_empty() {
|
||||
parts.push(String::new());
|
||||
}
|
||||
}
|
||||
Component::CurDir => {
|
||||
parts.push(".".to_string());
|
||||
}
|
||||
Component::ParentDir => {
|
||||
parts.push("..".to_string());
|
||||
}
|
||||
Component::Normal(part) => {
|
||||
parts.push(part.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = parts.join("/");
|
||||
if result.is_empty() && is_rooted {
|
||||
"/".to_string()
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn path_to_posix_string_serializes_relative_paths() {
|
||||
let path = PathBuf::from("src").join("**").join("*.html");
|
||||
|
||||
assert_eq!(path_to_posix_string(&path), "src/**/*.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_to_posix_string_serializes_rooted_paths() {
|
||||
let path = PathBuf::from(std::path::MAIN_SEPARATOR.to_string())
|
||||
.join("src")
|
||||
.join("**")
|
||||
.join("*.html");
|
||||
|
||||
assert_eq!(path_to_posix_string(&path), "/src/**/*.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_to_posix_string_serializes_empty_paths() {
|
||||
assert_eq!(path_to_posix_string(&PathBuf::new()), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_hoists_static_directories_and_keeps_files_in_the_pattern() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src").join("examples")).unwrap();
|
||||
|
||||
let mut source = PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/examples/index.html".to_string(),
|
||||
negated: false,
|
||||
};
|
||||
|
||||
source.optimize();
|
||||
|
||||
assert_eq!(
|
||||
source.base,
|
||||
dunce::canonicalize(dir.path().join("src").join("examples"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(source.pattern, "/index.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_hoists_folder_patterns() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src").join("examples")).unwrap();
|
||||
|
||||
let mut source = PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/examples".to_string(),
|
||||
negated: false,
|
||||
};
|
||||
|
||||
source.optimize();
|
||||
|
||||
assert_eq!(
|
||||
source.base,
|
||||
dunce::canonicalize(dir.path().join("src").join("examples"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(source.pattern, "/**/*");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_keeps_wildcards_in_the_pattern() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
|
||||
let mut source = PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/**/*.html".to_string(),
|
||||
negated: false,
|
||||
};
|
||||
|
||||
source.optimize();
|
||||
|
||||
assert_eq!(
|
||||
source.base,
|
||||
dunce::canonicalize(dir.path().join("src"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(source.pattern, "/**/*.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concrete_patterns_are_expanded_to_restrict_their_base() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
let base = dunce::canonicalize(dir.path().join("src")).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/foo.html".to_string(),
|
||||
negated: false,
|
||||
}]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: "*".to_string(),
|
||||
},
|
||||
SourceEntry::Pattern {
|
||||
base,
|
||||
pattern: "/foo.html".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_patterns_include_parent_directory_allow_rules() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
let base = dunce::canonicalize(dir.path().join("src")).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/ef*/*.html".to_string(),
|
||||
negated: false,
|
||||
}]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: "*".to_string(),
|
||||
},
|
||||
SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: "!/ef*/".to_string(),
|
||||
},
|
||||
SourceEntry::Pattern {
|
||||
base,
|
||||
pattern: "/ef*/*.html".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrestricted_sources_do_not_expand_patterns_for_the_same_base() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
let base = dunce::canonicalize(dir.path().join("src")).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![
|
||||
PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/foo.html".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Auto { base: base.clone() },
|
||||
SourceEntry::Pattern {
|
||||
base,
|
||||
pattern: "/foo.html".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_parent_bases_do_not_open_unrelated_siblings() {
|
||||
let dir = tempdir().unwrap();
|
||||
let project = dir.path().join("Users").join("robin").join("docus-test");
|
||||
fs::create_dir_all(&project).unwrap();
|
||||
|
||||
let users = dunce::canonicalize(dir.path().join("Users")).unwrap();
|
||||
let project = dunce::canonicalize(project).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![
|
||||
PublicSourceEntry {
|
||||
base: project.to_string_lossy().to_string(),
|
||||
pattern: "**/*".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
PublicSourceEntry {
|
||||
base: project.to_string_lossy().to_string(),
|
||||
pattern: "../../app.config.ts".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Auto {
|
||||
base: project.clone(),
|
||||
},
|
||||
SourceEntry::Ignored {
|
||||
base: users.clone(),
|
||||
pattern: "/*".to_string(),
|
||||
},
|
||||
SourceEntry::Pattern {
|
||||
base: users,
|
||||
pattern: "/app.config.ts".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_patterns_preserve_source_order() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
let base = dunce::canonicalize(dir.path().join("src")).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![
|
||||
PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/foo.html".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/foo.html".to_string(),
|
||||
negated: true,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: "*".to_string(),
|
||||
},
|
||||
SourceEntry::Pattern {
|
||||
base: base.clone(),
|
||||
pattern: "/foo.html".to_string(),
|
||||
},
|
||||
SourceEntry::Ignored {
|
||||
base,
|
||||
pattern: "/foo.html".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Run the public-to-private conversion for an auto-detected source pointing at `base` and
|
||||
/// return the resulting entry.
|
||||
fn auto_source_entry(base: &Path) -> SourceEntry {
|
||||
public_source_entries_to_private_source_entries(vec![PublicSourceEntry {
|
||||
base: base.to_string_lossy().to_string(),
|
||||
pattern: "**/*".to_string(),
|
||||
negated: false,
|
||||
}])
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_detected_folders_become_auto_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
let base = dir.path().join("src");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::Auto { base });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_ignored_by_default_become_external_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
let base = dir.path().join("node_modules").join("my-lib");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::External { base });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_ignored_by_gitignore_become_external_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
// Pretend this is a git repository so the `.gitignore` search is bounded to it.
|
||||
fs::create_dir_all(dir.path().join(".git")).unwrap();
|
||||
fs::write(dir.path().join(".gitignore"), "dist/\n").unwrap();
|
||||
|
||||
let base = dir.path().join("dist");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::External { base });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_ignored_by_a_parent_gitignore_become_external_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join(".git")).unwrap();
|
||||
// A `.gitignore` higher up in the tree should still apply to nested directories.
|
||||
fs::write(dir.path().join(".gitignore"), "generated/\n").unwrap();
|
||||
|
||||
let base = dir.path().join("packages").join("app").join("generated");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::External { base });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_not_ignored_by_gitignore_stay_auto_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join(".git")).unwrap();
|
||||
fs::write(dir.path().join(".gitignore"), "dist/\n").unwrap();
|
||||
|
||||
let base = dir.path().join("src");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::Auto { base });
|
||||
self.pattern = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -762,111 +218,18 @@ pub fn public_source_entries_to_private_source_entries(
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Compiled `.gitignore` matchers are cached per directory so we read and parse each
|
||||
// `.gitignore` file at most once, even though entries commonly share ancestor directories (e.g.
|
||||
// the repository root). A cached `None` means the directory has no `.gitignore` file.
|
||||
let mut gitignores: FxHashMap<PathBuf, Option<Gitignore>> = FxHashMap::default();
|
||||
|
||||
// Boundary for the `.gitignore` walk when a source is not inside a git repository (see below).
|
||||
let cwd = std::env::current_dir()
|
||||
.map(|cwd| dunce::canonicalize(&cwd).unwrap_or(cwd))
|
||||
.ok();
|
||||
|
||||
// Convert from public SourceEntry to private SourceEntry
|
||||
let sources = expanded_globs
|
||||
expanded_globs
|
||||
.into_iter()
|
||||
.map(|public_source| {
|
||||
let mut source: SourceEntry = public_source.into();
|
||||
|
||||
// Promote auto-sources to external sources if they were gitignored
|
||||
if let SourceEntry::Auto { ref base } = source {
|
||||
let inside_git_repo = base.ancestors().any(|dir| dir.join(".git").exists());
|
||||
|
||||
// Walk up from the folder, applying each `.gitignore` relative to the directory
|
||||
// that contains it (matching git), and stop at the git repository root so
|
||||
// `.gitignore` files outside of the repo are not considered.
|
||||
for dir in base.ancestors() {
|
||||
let gitignore = gitignores.entry(dir.to_path_buf()).or_insert_with(|| {
|
||||
let path = dir.join(".gitignore");
|
||||
|
||||
// `Gitignore::new` roots the matcher at the directory containing the file,
|
||||
// so patterns match relative to it.
|
||||
path.is_file().then(|| Gitignore::new(&path).0)
|
||||
});
|
||||
|
||||
// Only `.gitignore` files in ancestors of `base` can ignore `base` itself.
|
||||
// Patterns in `base`'s own `.gitignore` only match paths _inside_ `base`, never
|
||||
// `base` itself (the file walker still applies them to `base`'s contents).
|
||||
//
|
||||
// Skipping `base`'s own `.gitignore` also prevents a false positive for
|
||||
// whitelist style `.gitignore` files, because relativizing `base` against
|
||||
// itself yields the empty path, which incorrectly matches `/*`.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```gitignore
|
||||
// /*
|
||||
// !/.gitignore
|
||||
// !/app
|
||||
// !/public
|
||||
// ```
|
||||
//
|
||||
// Everything inside `base` except `.gitignore`, `app` and `public` is ignored,
|
||||
// but `base` itself is not.
|
||||
if dir != base {
|
||||
if let Some(gitignore) = gitignore {
|
||||
if gitignore
|
||||
.matched_path_or_any_parents(&base, true)
|
||||
.is_ignore()
|
||||
{
|
||||
source = SourceEntry::External { base: base.into() };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop at the git repository root.
|
||||
if dir.join(".git").exists() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Without a git repository there is no repository root to stop at. Stop once
|
||||
// the directory contains the current working directory instead, so `.gitignore`
|
||||
// files outside of the project (e.g. in the user's home directory) can never
|
||||
// promote a source to an external source. Note that the file walker still
|
||||
// applies those `.gitignore` files when deciding which files to scan.
|
||||
if !inside_git_repo && cwd.as_ref().is_some_and(|cwd| cwd.starts_with(dir)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source
|
||||
})
|
||||
.collect::<Vec<SourceEntry>>();
|
||||
|
||||
expand_restricted_patterns(sources)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Convert a public source entry to a source entry
|
||||
impl From<PublicSourceEntry> for SourceEntry {
|
||||
fn from(value: PublicSourceEntry) -> Self {
|
||||
if value.negated {
|
||||
return SourceEntry::Ignored {
|
||||
base: value.base.into(),
|
||||
pattern: value.pattern,
|
||||
};
|
||||
}
|
||||
|
||||
let auto =
|
||||
value.pattern == "/**/*" || PathBuf::from(&value.base).join(&value.pattern).is_dir();
|
||||
|
||||
if !auto {
|
||||
return SourceEntry::Pattern {
|
||||
base: value.base.into(),
|
||||
pattern: value.pattern,
|
||||
};
|
||||
}
|
||||
let auto = value.pattern.ends_with("**/*")
|
||||
|| PathBuf::from(&value.base).join(&value.pattern).is_dir();
|
||||
|
||||
let inside_ignored_content_dir = IGNORED_CONTENT_DIRS.iter().any(|dir| {
|
||||
value.base.contains(&format!(
|
||||
|
|
@ -876,16 +239,24 @@ impl From<PublicSourceEntry> for SourceEntry {
|
|||
std::path::MAIN_SEPARATOR
|
||||
)) || value
|
||||
.base
|
||||
.ends_with(&format!("{}{}", std::path::MAIN_SEPARATOR, dir))
|
||||
.ends_with(&format!("{}{}", std::path::MAIN_SEPARATOR, dir,))
|
||||
});
|
||||
|
||||
match inside_ignored_content_dir {
|
||||
false => SourceEntry::Auto {
|
||||
match (value.negated, auto, inside_ignored_content_dir) {
|
||||
(false, true, false) => SourceEntry::Auto {
|
||||
base: value.base.into(),
|
||||
},
|
||||
true => SourceEntry::External {
|
||||
(false, true, true) => SourceEntry::External {
|
||||
base: value.base.into(),
|
||||
},
|
||||
(false, false, _) => SourceEntry::Pattern {
|
||||
base: value.base.into(),
|
||||
pattern: value.pattern,
|
||||
},
|
||||
(true, _, _) => SourceEntry::Ignored {
|
||||
base: value.base.into(),
|
||||
pattern: value.pattern,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -126,72 +126,6 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19706
|
||||
test(
|
||||
'Config files (TS, valid types)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tsconfig.json': json`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"composite": true,
|
||||
"outDir": "./.build",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["tailwind.config.ts"]
|
||||
}
|
||||
`,
|
||||
'index.html': html`
|
||||
<div class="text-primary"></div>
|
||||
`,
|
||||
'tailwind.config.ts': ts`
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
function defineConfig(config: Config): Config {
|
||||
return config
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'blue',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@config '../tailwind.config.ts';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
// We expect that these commands don't crash:
|
||||
await exec('pnpm tsc -b')
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`text-primary`])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'Config files (CJS, watch mode)',
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
import { candidate, css, html, json, test, ts } from '../utils'
|
||||
import { candidate, css, html, json, test } from '../utils'
|
||||
|
||||
test(
|
||||
'builds the `@tailwindcss/typography` plugin utilities',
|
||||
|
|
@ -42,86 +42,12 @@ test(
|
|||
|
||||
await fs.expectFileToContain('dist/out.css', [
|
||||
candidate`prose`,
|
||||
':where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *))',
|
||||
':where(tbody td, tfoot td):not(:where([class~="not-prose"], [class~="not-prose"] *))',
|
||||
':where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *))',
|
||||
':where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *))',
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'builds the `@tailwindcss/typography` plugin utilities with `@variant` usages',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.14",
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'index.html': html`
|
||||
<div className="prose prose-custom">
|
||||
<h1>Headline</h1>
|
||||
<p>
|
||||
Until now, trying to style an article, document, or blog post with Tailwind has been a
|
||||
tedious task that required a keen eye for typography and a lot of complex custom CSS.
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss/utilities';
|
||||
@theme {
|
||||
--breakpoint-sm: 640px;
|
||||
}
|
||||
@plugin '@tailwindcss/typography';
|
||||
@config '../tailwind.config.js';
|
||||
@custom-variant custom (&.custom);
|
||||
`,
|
||||
'tailwind.config.js': ts`
|
||||
module.exports = {
|
||||
theme: {
|
||||
typography: ({ theme }) => ({
|
||||
custom: {
|
||||
css: {
|
||||
hr: {
|
||||
'--x': '1',
|
||||
'@variant sm:custom': {
|
||||
'--x': '2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
// We don't want to see `@variant` in the output
|
||||
let contents = await fs.read('dist/out.css')
|
||||
expect(contents).not.toContain('@variant')
|
||||
|
||||
expect(await fs.dumpFiles('dist/out.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/out.css ---
|
||||
.prose-custom :where(hr):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
--x: 1;
|
||||
@media (width >= 640px) {
|
||||
&.custom {
|
||||
--x: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'builds the `@tailwindcss/forms` plugin utilities',
|
||||
{
|
||||
|
|
@ -191,7 +117,7 @@ test(
|
|||
await fs.expectFileToContain('dist/out.css', [
|
||||
//
|
||||
`::-webkit-date-and-time-value`,
|
||||
`input:where([type='checkbox']):indeterminate`,
|
||||
`[type='checkbox']:indeterminate`,
|
||||
])
|
||||
|
||||
// No classes are included even though they are used in the HTML
|
||||
|
|
@ -281,88 +207,3 @@ test(
|
|||
])
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/15844
|
||||
test(
|
||||
'builds CSS with a custom plugin compiled from TypeScript',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tsconfig.json': json`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"composite": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./.build",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
`,
|
||||
'index.html': html`
|
||||
<div class="test-red"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@plugin '../.build/plugin.js';
|
||||
`,
|
||||
'src/plugin.ts': ts`
|
||||
import plugin from 'tailwindcss/plugin'
|
||||
|
||||
export const typedPlugin = plugin(() => {
|
||||
return ({ matchComponents }) => {
|
||||
matchComponents(
|
||||
{
|
||||
test: (content: string) => ({
|
||||
color: content,
|
||||
}),
|
||||
},
|
||||
{
|
||||
values: {
|
||||
red: 'red',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default plugin(({ matchComponents }) => {
|
||||
matchComponents(
|
||||
{
|
||||
test: (content: string) => ({
|
||||
color: content,
|
||||
}),
|
||||
},
|
||||
{
|
||||
values: {
|
||||
red: 'red',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
// We expect that these commands don't crash:
|
||||
await exec('pnpm tsc -b')
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`test-red`])
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import nodeFs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { candidate, css, html, IS_WINDOWS, js, json, test, ts } from '../utils'
|
||||
import { candidate, css, html, js, json, test } from '../utils'
|
||||
|
||||
const STANDALONE_BINARY = (() => {
|
||||
switch (os.platform()) {
|
||||
|
|
@ -16,41 +15,6 @@ const STANDALONE_BINARY = (() => {
|
|||
}
|
||||
})()
|
||||
|
||||
test(
|
||||
'does not scan itself for candidates',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {}
|
||||
}
|
||||
`,
|
||||
'src/index.css': css` @import 'tailwindcss'; `,
|
||||
},
|
||||
},
|
||||
async ({ root, fs, exec }) => {
|
||||
let sourceBinary = path.resolve(
|
||||
__dirname,
|
||||
`../../packages/@tailwindcss-standalone/dist/${STANDALONE_BINARY}`,
|
||||
)
|
||||
let binary = IS_WINDOWS ? 'tailwindcss.exe' : 'tailwindcss'
|
||||
let localBinary = path.join(root, binary)
|
||||
await nodeFs.copyFile(sourceBinary, localBinary)
|
||||
|
||||
if (!IS_WINDOWS) {
|
||||
await nodeFs.chmod(localBinary, 0o755)
|
||||
}
|
||||
|
||||
await exec(`${IS_WINDOWS ? binary : `./${binary}`} --input src/index.css --output dist/out.css`)
|
||||
|
||||
await fs.expectFileNotToContain('dist/out.css', [
|
||||
candidate`flex`,
|
||||
candidate`grid`,
|
||||
candidate`underline`,
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'includes first-party plugins',
|
||||
{
|
||||
|
|
@ -158,7 +122,7 @@ test(
|
|||
})
|
||||
})
|
||||
`,
|
||||
'src/plugin.ts': ts`
|
||||
'src/plugin.ts': js`
|
||||
import plugin from 'tailwindcss/plugin'
|
||||
|
||||
// Make sure all available JS APIs can be imported and used
|
||||
|
|
@ -168,7 +132,8 @@ test(
|
|||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
import * as pkg from 'tailwindcss/package.json'
|
||||
|
||||
export interface PluginOptions {}
|
||||
export interface PluginOptions {
|
||||
}
|
||||
|
||||
export default plugin(function ({ addUtilities }) {
|
||||
addUtilities({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { css, js, json, test, yaml } from '../utils'
|
||||
import { css, js, json, test } from '../utils'
|
||||
|
||||
// This test runs the wasm build using the `node:wasi` runtime.
|
||||
// This test runs the wasm build using the `node:wasi` runtine.
|
||||
//
|
||||
// There are currently a known problems that the Node WASI preview implementation does not properly
|
||||
// handle FS reads on macOS and it does not implement all APIs on Windows. Because of that, this
|
||||
|
|
@ -57,187 +57,3 @@ testFn(
|
|||
`)
|
||||
},
|
||||
)
|
||||
|
||||
testFn(
|
||||
'`@tailwindcss/oxide` falls back to the wasm build when no native binding is available',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
# Trick pnpm in only supporting an architecture that @tailwindcss/oxide
|
||||
# doesn't support, and therefore should fallback to the wasm version.
|
||||
supportedArchitectures:
|
||||
os:
|
||||
- openbsd
|
||||
cpu:
|
||||
- x64
|
||||
`,
|
||||
'src/index.js': js`
|
||||
const className = "content-['src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
'index.mjs': js`
|
||||
import { createRequire } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
|
||||
let require = createRequire(import.meta.url)
|
||||
let { Scanner } = require('@tailwindcss/oxide')
|
||||
|
||||
let loaded = Object.keys(require.cache)
|
||||
|
||||
let scanner = new Scanner({
|
||||
sources: [
|
||||
{
|
||||
base: join(process.cwd(), 'src'),
|
||||
pattern: '**/*',
|
||||
negated: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
native: loaded.filter((file) => file.endsWith('.node')),
|
||||
wasi: loaded.some((file) => file.endsWith('tailwindcss-oxide.wasi.cjs')),
|
||||
candidates: scanner.scan(),
|
||||
}),
|
||||
)
|
||||
process.exit()
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ expect, exec }) => {
|
||||
// Since vitest runs under `pnpm run`, pnpm's bin shims export a NODE_PATH
|
||||
// that includes the repository's hidden hoist directory
|
||||
// (`node_modules/.pnpm/node_modules`), which links every workspace package,
|
||||
// including all native `@tailwindcss/oxide-*` bindings.
|
||||
//
|
||||
// Node uses `NODE_PATH` exactly when the local `node_modules` lookup fails,
|
||||
// which would defeat the simulated unsupported platform, so clear it.
|
||||
let output = await exec(`node index.mjs`, { env: { NODE_PATH: '' } })
|
||||
let { native, wasi, candidates } = JSON.parse(output)
|
||||
|
||||
// No native binding was installed or loaded, ...
|
||||
expect(native).toEqual([])
|
||||
|
||||
// ... the wasm32-wasi binding is what actually loaded, ...
|
||||
expect(wasi).toBe(true)
|
||||
|
||||
// ... and scanning real files on disk works through it.
|
||||
expect(candidates).toMatchInlineSnapshot(`
|
||||
[
|
||||
"className",
|
||||
"const",
|
||||
"content-['src/index.js']",
|
||||
"exports",
|
||||
]
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
testFn(
|
||||
'the wasm build loads even when preopening the filesystem root is denied',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
# Trick pnpm in only supporting an architecture that @tailwindcss/oxide
|
||||
# doesn't support, and therefore should fallback to the wasm version.
|
||||
supportedArchitectures:
|
||||
os:
|
||||
- openbsd
|
||||
cpu:
|
||||
- x64
|
||||
`,
|
||||
// The wasm bindings generated by `@napi-rs/cli` preopen the filesystem
|
||||
// root, which sandboxed platforms (e.g. OpenHarmony, Android) deny with
|
||||
// `UVWASI_EACCES`, making the wasm fallback fail to load on exactly the
|
||||
// platforms that need it.
|
||||
//
|
||||
// We patch `@napi-rs/cli`'s templates to retry with narrower preopens
|
||||
// (see `patches/@napi-rs__cli@3.7.4.patch`). Simulate such a sandbox by
|
||||
// denying `/` preopens.
|
||||
'preload.cjs': js`
|
||||
const wasi = require('node:wasi')
|
||||
const RealWASI = wasi.WASI
|
||||
|
||||
wasi.WASI = class WASI extends RealWASI {
|
||||
constructor(options) {
|
||||
if (options?.preopens?.['/'] !== undefined) {
|
||||
const error = new Error('UVWASI_EACCES, uvwasi_init')
|
||||
error.code = 'UVWASI_EACCES'
|
||||
error.syscall = 'uvwasi_init'
|
||||
throw error
|
||||
}
|
||||
super(options)
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/index.js': js`
|
||||
const className = "content-['src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
'index.mjs': js`
|
||||
import { createRequire } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
|
||||
let require = createRequire(import.meta.url)
|
||||
let { Scanner } = require('@tailwindcss/oxide')
|
||||
|
||||
let loaded = Object.keys(require.cache)
|
||||
|
||||
let scanner = new Scanner({
|
||||
sources: [
|
||||
{
|
||||
base: join(process.cwd(), 'src'),
|
||||
pattern: '**/*',
|
||||
negated: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
wasi: loaded.some((file) => file.endsWith('tailwindcss-oxide.wasi.cjs')),
|
||||
candidates: scanner.scan(),
|
||||
}),
|
||||
)
|
||||
process.exit()
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ expect, exec }) => {
|
||||
// See the note about NODE_PATH in the test above.
|
||||
let output = await exec(`node --require ./preload.cjs index.mjs`, { env: { NODE_PATH: '' } })
|
||||
|
||||
// Only parse the first line, because Node prints an `ExperimentalWarning`
|
||||
// about WASI to stderr, which `exec` appends to the output.
|
||||
let { wasi, candidates } = JSON.parse(output.trim().split('\n')[0])
|
||||
|
||||
// The wasm32-wasi binding loaded despite `/` being denied, ...
|
||||
expect(wasi).toBe(true)
|
||||
|
||||
// ... and scanning files under the current working directory still works
|
||||
// through the narrower preopen.
|
||||
expect(candidates).toMatchInlineSnapshot(`
|
||||
[
|
||||
"className",
|
||||
"const",
|
||||
"content-['src/index.js']",
|
||||
"exports",
|
||||
]
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"dedent": "catalog:",
|
||||
"dedent": "1.7.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"source-map-js": "^1.2.1",
|
||||
"yaml": "^2.9.0"
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ describe.each(Object.keys(variantConfig))('%s', (variant) => {
|
|||
},
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
await expect(
|
||||
expect(
|
||||
exec('pnpm postcss src/index.css --output dist/out.css', undefined, { ignoreStdErr: true }),
|
||||
).rejects.toThrow(
|
||||
).rejects.toThrowError(
|
||||
`It looks like you're trying to use \`tailwindcss\` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install \`@tailwindcss/postcss\` and update your PostCSS configuration.`,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -698,7 +698,9 @@ test(
|
|||
<div class="flex underline"></div>
|
||||
`)
|
||||
|
||||
await process.onStderr((message) => message.includes('"./does-not-exist" is not exported'))
|
||||
await process.onStderr((message) =>
|
||||
message.includes('does-not-exist is not exported from package'),
|
||||
)
|
||||
|
||||
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -741,7 +743,9 @@ test(
|
|||
`,
|
||||
)
|
||||
|
||||
await process.onStderr((message) => message.includes('"./does-not-exist" is not exported'))
|
||||
await process.onStderr((message) =>
|
||||
message.includes('does-not-exist is not exported from package'),
|
||||
)
|
||||
|
||||
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -821,73 +825,3 @@ test(
|
|||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'source(…) and `@source` are relative to the file they are in',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"postcss": "^8",
|
||||
"postcss-cli": "^10",
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/postcss": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
'postcss.config.js': js`
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
`,
|
||||
|
||||
'index.css': css` @import './project-a/src/index.css'; `,
|
||||
|
||||
'project-a/src/index.css': css`
|
||||
/* Run auto-content detection in ../../project-b */
|
||||
@import 'tailwindcss/utilities' source('../../project-b');
|
||||
|
||||
/* Explicitly using node_modules in the @source allows git ignored folders */
|
||||
@source '../../project-c';
|
||||
`,
|
||||
|
||||
// Project A is the current folder, but we explicitly configured
|
||||
// `source(project-b)`, therefore project-a should not be included in
|
||||
// the output.
|
||||
'project-a/src/index.html': html`
|
||||
<div
|
||||
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
|
||||
></div>
|
||||
`,
|
||||
|
||||
// Project B is the configured `source(…)`, therefore auto source
|
||||
// detection should include known extensions and folders in the output.
|
||||
'project-b/src/index.html': html`
|
||||
<div
|
||||
class="content-['project-b/src/index.html']"
|
||||
></div>
|
||||
`,
|
||||
|
||||
// Project C should apply auto source detection, therefore known
|
||||
// extensions and folders should be included in the output.
|
||||
'project-c/src/index.html': html`
|
||||
<div
|
||||
class="content-['project-c/src/index.html']"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, root, expect }) => {
|
||||
await exec('pnpm postcss ./index.css --output dist/out.css', { cwd: root })
|
||||
|
||||
let content = await fs.dumpFiles('./dist/*.css')
|
||||
|
||||
expect(content).not.toContain(candidate`content-['project-a/src/index.html']`)
|
||||
expect(content).toContain(candidate`content-['project-b/src/index.html']`)
|
||||
expect(content).toContain(candidate`content-['project-c/src/index.html']`)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -468,7 +468,7 @@ test(
|
|||
// the CSS
|
||||
await fetchStyles(url)
|
||||
|
||||
// At this point, no changes should trigger a compile step. If we see any
|
||||
// At this point, no changes should triger a compile step. If we see any
|
||||
// changes, there is an infinite loop because we (the user) didn't write any
|
||||
// files to disk.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
import { candidate, css, fetchStyles, js, json, jsx, retryAssertion, test } from '../utils'
|
||||
|
||||
test(
|
||||
'@tailwindcss/turbopack loader (dev)',
|
||||
{
|
||||
timeout: 120_000,
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "^16.2.7",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/turbopack": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'next.config.mjs': js`
|
||||
export default {
|
||||
turbopack: {
|
||||
rules: {
|
||||
'*.css': {
|
||||
loaders: ['@tailwindcss/turbopack'],
|
||||
as: '*.css',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
'app/layout.js': jsx`
|
||||
import './globals.css'
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
`,
|
||||
'app/page.js': jsx`
|
||||
export default function Page() {
|
||||
return <div className="flex">Hello, Next.js!</div>
|
||||
}
|
||||
`,
|
||||
'app/globals.css': css` @import 'tailwindcss'; `,
|
||||
},
|
||||
},
|
||||
async ({ fs, spawn, expect }) => {
|
||||
let process = await spawn('pnpm next dev')
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((message) => {
|
||||
let match = /Local:\s*(http.*)/.exec(message)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
await process.onStdout((message) => message.includes('Ready in'))
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url)
|
||||
expect(styles).toContain(candidate`flex`)
|
||||
expect(styles).not.toContain(candidate`underline`)
|
||||
})
|
||||
|
||||
await fs.write(
|
||||
'app/page.js',
|
||||
jsx`
|
||||
export default function Page() {
|
||||
return <div className="flex underline">Hello, Next.js!</div>
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url)
|
||||
expect(styles).toContain(candidate`flex`)
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'node:path'
|
||||
import { isRepoDirty } from '../../packages/@tailwindcss-upgrade/src/utils/git'
|
||||
import { candidate, css, html, js, json, test, ts, txt, yaml } from '../utils'
|
||||
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
|
||||
|
||||
test(
|
||||
'error when no CSS file with @tailwind is used',
|
||||
|
|
@ -28,7 +28,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
let output = await exec('pnpm exec upgrade')
|
||||
let output = await exec('npx @tailwindcss/upgrade')
|
||||
expect(output).toContain('Cannot find any CSS files that reference Tailwind CSS.')
|
||||
|
||||
// Files should not be modified
|
||||
|
|
@ -100,7 +100,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -158,7 +158,7 @@ test(
|
|||
})
|
||||
|
||||
// Ensure the v4 project compiles correctly
|
||||
await exec('pnpm exec tailwindcss --input src/input.css --output dist/out.css')
|
||||
await exec('npx tailwindcss --input src/input.css --output dist/out.css')
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [
|
||||
candidate`flex!`,
|
||||
|
|
@ -171,62 +171,9 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'only migrates files matched by `config.content` when upgrading from v3 to v4',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.html'],
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
<div class="order-[0] bg-[--my-red]"></div>
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
'templates/email.php': html`
|
||||
<div class="order-[0] bg-[--my-red]"></div>
|
||||
`,
|
||||
'notes/unrelated.txt': `order-[0] bg-[--my-red]`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./**/*.{html,php,txt}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- notes/unrelated.txt ---
|
||||
order-[0] bg-[--my-red]
|
||||
|
||||
--- src/index.html ---
|
||||
<div class="order-0 bg-(--my-red)"></div>
|
||||
|
||||
--- templates/email.php ---
|
||||
<div class="order-[0] bg-[--my-red]"></div>
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
`upgrades a v3 project with prefixes to v4`,
|
||||
{
|
||||
// Somehow this test takes *way* longer than the rest (but not always?)
|
||||
timeout: 180_000,
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
|
|
@ -263,7 +210,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -335,7 +282,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -407,7 +354,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -484,7 +431,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -587,7 +534,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -654,7 +601,6 @@ test(
|
|||
test(
|
||||
'migrates a simple postcss setup',
|
||||
{
|
||||
timeout: 120_000,
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
|
|
@ -695,7 +641,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
await fs.expectFileToContain(
|
||||
'postcss.config.js',
|
||||
|
|
@ -766,7 +712,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
let packageJsonContent = await fs.read('package.json')
|
||||
let packageJson = JSON.parse(packageJsonContent)
|
||||
|
|
@ -813,7 +759,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
let packageJsonContent = await fs.read('package.json')
|
||||
let packageJson = JSON.parse(packageJsonContent)
|
||||
|
|
@ -826,7 +772,6 @@ test(
|
|||
test(
|
||||
'migrates a postcss setup using package.json config',
|
||||
{
|
||||
timeout: 120_000,
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
|
|
@ -865,7 +810,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
|
||||
await fs.expectFileToContain(
|
||||
|
|
@ -900,7 +845,6 @@ test(
|
|||
test(
|
||||
'migrates a postcss setup using a json based config file',
|
||||
{
|
||||
timeout: 120_000,
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
|
|
@ -941,7 +885,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
|
||||
await fs.expectFileToContain(
|
||||
|
|
@ -1008,7 +952,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1055,7 +999,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1099,7 +1043,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1169,10 +1113,9 @@ test(
|
|||
}
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1281,10 +1224,9 @@ test(
|
|||
}
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1417,7 +1359,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
let output = await exec('pnpm exec upgrade --force')
|
||||
let output = await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(output).toMatch(
|
||||
/You have one or more stylesheets that are imported into a utility layer and non-utility layer./,
|
||||
|
|
@ -1541,7 +1483,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1766,8 +1708,8 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
let output = await exec('pnpm exec upgrade --force', {}, { ignoreStdErr: true }).catch((e) =>
|
||||
e.toString(),
|
||||
let output = await exec('npx @tailwindcss/upgrade --force', {}, { ignoreStdErr: true }).catch(
|
||||
(e) => e.toString(),
|
||||
)
|
||||
|
||||
expect(output).toMatch('Could not determine configuration file for:')
|
||||
|
|
@ -1838,7 +1780,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1974,7 +1916,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2094,7 +2036,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2174,7 +2116,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2256,10 +2198,9 @@ test(
|
|||
}
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2313,7 +2254,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
let pkg = JSON.parse(await fs.read('package.json'))
|
||||
|
||||
|
|
@ -2392,7 +2333,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
// Files should not be modified
|
||||
expect(await fs.dumpFiles('./*.{js,css,html}')).toMatchInlineSnapshot(`
|
||||
|
|
@ -2404,7 +2345,8 @@ test(
|
|||
--shadow-*: initial;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
|
||||
--default-ring-width: 4px;
|
||||
--ring-width-*: initial;
|
||||
--ring-width: 4px;
|
||||
|
||||
--blur: var(--custom-default-blur);
|
||||
|
||||
|
|
@ -2490,7 +2432,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade --force')
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
// Files should not be modified
|
||||
expect(await fs.dumpFiles('./*.{js,css,html,tsx}')).toMatchInlineSnapshot(`
|
||||
|
|
@ -2557,7 +2499,7 @@ test(
|
|||
@import './utilities.css';
|
||||
@import './generated/ignore-me.css';
|
||||
`,
|
||||
'src/generated/.gitignore': txt`
|
||||
'src/generated/.gitignore': `
|
||||
*
|
||||
!.gitignore
|
||||
`,
|
||||
|
|
@ -2597,7 +2539,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade ./src/index.css')
|
||||
await exec('npx @tailwindcss/upgrade ./src/index.css')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2723,7 +2665,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2736,8 +2678,8 @@ test(
|
|||
text-red-500/10
|
||||
text-red-500/semitransparent
|
||||
text-red-500/transparent
|
||||
text-red-500/semitransparent
|
||||
text-red-500/50.50
|
||||
text-red-500/50
|
||||
text-red-500/50.5
|
||||
text-red-500/50.50
|
||||
text-red-500/50%
|
||||
text-red-500/100%"
|
||||
|
|
@ -2809,7 +2751,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2817,7 +2759,7 @@ test(
|
|||
<div class="ring-3"></div>
|
||||
|
||||
--- ./src/input.css ---
|
||||
@import 'tailwindcss';
|
||||
@import 'tailwindcss';
|
||||
|
||||
/*
|
||||
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
|
||||
|
|
@ -2876,8 +2818,8 @@ test(
|
|||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, root, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
let before = await fs.dumpFiles('./src/**/*.{css,html}')
|
||||
expect(before).toMatchInlineSnapshot(`
|
||||
|
|
@ -2913,13 +2855,13 @@ test(
|
|||
`)
|
||||
|
||||
// Commit the changes
|
||||
if (isRepoDirty(root)) {
|
||||
if (isRepoDirty()) {
|
||||
await exec('git add .')
|
||||
await exec('git commit -m "upgrade"')
|
||||
}
|
||||
|
||||
// Run the upgrade again
|
||||
let output = await exec('pnpm exec upgrade')
|
||||
let output = await exec('npx @tailwindcss/upgrade')
|
||||
expect(output).toContain('No changes were made to your repository')
|
||||
|
||||
let after = await fs.dumpFiles('./src/**/*.{css,html}')
|
||||
|
|
@ -2998,7 +2940,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -3026,202 +2968,6 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'v4 ignores .env files during template migration',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^4",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/app.css': css`@import 'tailwindcss';`,
|
||||
'src/index.html': html`
|
||||
<div class="order-[0]"></div>
|
||||
`,
|
||||
'src/.env': `TW_TEST_CLASS=order-[0]`,
|
||||
'src/.env.production': `TW_TEST_CLASS=order-[0]`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/{*,.env,.env.*}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./src/index.html ---
|
||||
<div class="order-0"></div>
|
||||
|
||||
--- ./src/.env ---
|
||||
TW_TEST_CLASS=order-[0]
|
||||
|
||||
--- ./src/.env.production ---
|
||||
TW_TEST_CLASS=order-[0]
|
||||
|
||||
--- ./src/app.css ---
|
||||
@import 'tailwindcss';
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'v4 linked configs respect `content` and still ignore gitignored files',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^4",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/*.html', './src/*.less'],
|
||||
}
|
||||
`,
|
||||
'src/app.css': css`
|
||||
@import 'tailwindcss';
|
||||
@config '../tailwind.config.js';
|
||||
`,
|
||||
|
||||
// Ignore all .html files
|
||||
'src/.gitignore': txt`
|
||||
*.html
|
||||
`,
|
||||
|
||||
// HTML files are in .gitignore, even though they are explicitly mentioned
|
||||
// in the `content` array. Still ignore them
|
||||
'src/do-not-migrate-me.html': html`
|
||||
<div class="order-[0]"></div>
|
||||
`,
|
||||
|
||||
// Should be picked up by auto-content detection
|
||||
'templates/migrate-me.php': html`
|
||||
<div class="order-[0]"></div>
|
||||
`,
|
||||
|
||||
// Does not get picked up by auto content detection (because it's a less
|
||||
// file), but was explicitly listed in the `content` array.
|
||||
//
|
||||
// A bit of a hacky way, I admit, but it allows us to differentiate
|
||||
// between git ignored files, auto content detection and explicitly listed
|
||||
// files.
|
||||
'src/migrate-me.less': html`
|
||||
<div class="order-[0]"></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./{src,templates}/**/*')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./src/app.css ---
|
||||
@import 'tailwindcss';
|
||||
@config '../tailwind.config.js';
|
||||
|
||||
--- ./src/do-not-migrate-me.html ---
|
||||
<div class="order-[0]"></div>
|
||||
|
||||
--- ./src/migrate-me.less ---
|
||||
<div class="order-0"></div>
|
||||
|
||||
--- ./templates/migrate-me.php ---
|
||||
<div class="order-0"></div>
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'interrupting template migration does not truncate files',
|
||||
{
|
||||
timeout: 180_000,
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^4",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/app.css': css` @import 'tailwindcss'; `,
|
||||
'src/index.html': html`
|
||||
<div class="order-[0]"></div>
|
||||
`,
|
||||
'hook.cjs': js`
|
||||
let fs = require('node:fs/promises')
|
||||
let path = require('node:path')
|
||||
let originalWriteFile = fs.writeFile.bind(fs)
|
||||
|
||||
fs.writeFile = async (file, contents, ...rest) => {
|
||||
// Mimic a bad write
|
||||
await originalWriteFile(file, '') // As-if we truncated first
|
||||
console.error('__TRUNCATED_TARGET__')
|
||||
await new Promise((r) => setTimeout(r, 50)) // Wait 50ms to allow us to kill the process
|
||||
await originalWriteFile(file, contents, ...rest) // Write the actual contents
|
||||
}
|
||||
`,
|
||||
'src/keep.php': `
|
||||
<?php
|
||||
|
||||
return [
|
||||
'keep' => 'this file should never be truncated',
|
||||
];
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ spawn, fs, expect }) => {
|
||||
let repeatedCandidates = Array.from(
|
||||
{ length: 250 },
|
||||
() => '<div class="order-[0]"></div>',
|
||||
).join('\n')
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await fs.write(
|
||||
`src/templates/template-${i}.php`,
|
||||
`<?php\n\n${repeatedCandidates}\n\nreturn ['template' => ${i}];\n`,
|
||||
)
|
||||
}
|
||||
|
||||
let originalKeepFile = await fs.read('src/keep.php')
|
||||
let originalTemplate = await fs.read('src/templates/template-0.php')
|
||||
|
||||
// NOTE: This test intentionally keeps `npx` instead of `pnpm exec`. It uses
|
||||
// a `NODE_OPTIONS` hook that overrides `fs.writeFile` to simulate a bad
|
||||
// write, and `pnpm exec` may write a bin shim on startup which would trip
|
||||
// that hook before template migration begins.
|
||||
let process = await spawn('npx @tailwindcss/upgrade --force', {
|
||||
env: {
|
||||
NODE_OPTIONS: '--require=./hook.cjs',
|
||||
},
|
||||
})
|
||||
|
||||
// We're only interested once we start migrating the templates
|
||||
await process.onStderr((message) => message.includes('Migrating templates'))
|
||||
|
||||
// Wait for the trigger that we are mid-write
|
||||
await process.onStderr((message) => message === '__TRUNCATED_TARGET__')
|
||||
|
||||
// Kill the process
|
||||
await process.dispose()
|
||||
|
||||
expect(await fs.read('src/keep.php')).toBe(originalKeepFile)
|
||||
expect(await fs.read('src/templates/template-0.php')).toBe(originalTemplate)
|
||||
|
||||
for (let [file, contents] of await fs.glob('src/**/*.{html,php,css}')) {
|
||||
expect(contents.trim(), `${file} should not be empty after interruption`).not.toBe('')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'upgrades can run in a pnpm workspace',
|
||||
{
|
||||
|
|
@ -3265,7 +3011,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ root, exec, fs, expect }) => {
|
||||
let stdout = await exec('pnpm exec upgrade', {
|
||||
let stdout = await exec('npx @tailwindcss/upgrade', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
|
||||
|
|
@ -3339,7 +3085,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
let stdout = await exec('pnpm exec upgrade')
|
||||
let stdout = await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(stdout).not.toContain(
|
||||
'Running this command will add the dependency to the workspace root',
|
||||
|
|
@ -3371,61 +3117,6 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20328
|
||||
test(
|
||||
'ignored files should not be touched when upgrading from a nested directory',
|
||||
{
|
||||
fs: {
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- packages/*
|
||||
`,
|
||||
'package.json': json`
|
||||
{
|
||||
"name": "root",
|
||||
"private": true
|
||||
}
|
||||
`,
|
||||
'packages/css/package.json': json`
|
||||
{
|
||||
"name": "css-pkg",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'packages/css/src/input.css': css`
|
||||
@import 'tailwindcss/utilities.css' layer(utilities) source(none);
|
||||
`,
|
||||
'packages/css/src/index.html': html`<div class="flex text-red-500">Hi</div>`,
|
||||
|
||||
// Ensure files/folders ignored by a `.gitignore` in the root take effect
|
||||
// when executing the upgrade tool from a sub-package.
|
||||
'.gitignore': txt`
|
||||
node_modules/
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, exec, fs, expect }) => {
|
||||
await exec('git init', { cwd: root })
|
||||
|
||||
await exec('pnpm exec upgrade --force', {
|
||||
cwd: path.join(root, 'packages/css'),
|
||||
})
|
||||
|
||||
expect(await fs.dumpFiles('packages/css/node_modules/tailwindcss/utilities.css'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
--- packages/css/node_modules/tailwindcss/utilities.css ---
|
||||
@tailwind utilities;
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'upgrade <style> blocks carefully',
|
||||
{
|
||||
|
|
@ -3470,7 +3161,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,vue}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'node:path'
|
||||
import { describe } from 'vitest'
|
||||
import { css, html, json, test, ts, txt } from '../utils'
|
||||
import { css, html, json, test, ts } from '../utils'
|
||||
|
||||
test(
|
||||
`upgrade JS config files with flat theme values, darkMode, and content fields`,
|
||||
|
|
@ -34,9 +34,6 @@ test(
|
|||
steel: 'rgb(70 130 180 / <alpha-value>)',
|
||||
smoke: 'rgba(245, 245, 245, var(--smoke-alpha, <alpha-value>))',
|
||||
},
|
||||
ringColor: {
|
||||
DEFAULT: '#c0ffee',
|
||||
},
|
||||
opacity: {
|
||||
superOpaque: '0.95',
|
||||
},
|
||||
|
|
@ -164,15 +161,15 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,js,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- src/index.html ---
|
||||
<div
|
||||
class="tracking-superWide leading-superLoose"
|
||||
class="tracking-super-wide leading-super-loose"
|
||||
></div>
|
||||
<div class="text-superRed/superOpaque leading-superLoose"></div>
|
||||
<div class="text-super-red/super-opaque leading-super-loose"></div>
|
||||
|
||||
--- src/input.css ---
|
||||
@import 'tailwindcss';
|
||||
|
|
@ -190,14 +187,12 @@ test(
|
|||
--color-red-500: #ef4444;
|
||||
--color-red-600: #dc2626;
|
||||
|
||||
--color-superRed: #ff0000;
|
||||
--color-super-red: #ff0000;
|
||||
--color-steel: rgb(70 130 180);
|
||||
--color-smoke: rgba(245, 245, 245, var(--smoke-alpha, 1));
|
||||
|
||||
--default-ring-color: #c0ffee;
|
||||
|
||||
--opacity-*: initial;
|
||||
--opacity-superOpaque: 95%;
|
||||
--opacity-super-opaque: 95%;
|
||||
|
||||
--text-*: initial;
|
||||
--text-xs: 0.75rem;
|
||||
|
|
@ -270,9 +265,9 @@ test(
|
|||
--animate-spin-clockwise: spin-clockwise 1s linear infinite;
|
||||
--animate-spin-counterclockwise: spin-counterclockwise 1s linear infinite;
|
||||
|
||||
--tracking-superWide: 0.25em;
|
||||
--tracking-super-wide: 0.25em;
|
||||
|
||||
--leading-superLoose: 3;
|
||||
--leading-super-loose: 3;
|
||||
|
||||
@keyframes spin-clockwise {
|
||||
0% {
|
||||
|
|
@ -322,105 +317,6 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'skips gitignored template files even when they are explicitly referenced in `content`',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'.gitignore': txt`
|
||||
node_modules
|
||||
src/ignored
|
||||
`,
|
||||
'tailwind.config.ts': ts`
|
||||
import { type Config } from 'tailwindcss'
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/migrate-me.html',
|
||||
'./src/ignored/do-not-migrate-me.html',
|
||||
'./node_modules/my-external-lib/template.html',
|
||||
],
|
||||
theme: {},
|
||||
plugins: [],
|
||||
} satisfies Config
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
'src/migrate-me.html': html`
|
||||
<div class="order-[0]"></div>
|
||||
`,
|
||||
'src/ignored/do-not-migrate-me.html': html`
|
||||
<div class="order-[0]"></div>
|
||||
`,
|
||||
'node_modules/my-external-lib/template.html': html`
|
||||
<div
|
||||
class="order-[0]"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(
|
||||
await fs.dumpFiles(
|
||||
'{src/**/*.{css,html},node_modules/my-external-lib/template.html,.gitignore}',
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- .gitignore ---
|
||||
node_modules
|
||||
src/ignored
|
||||
|
||||
--- src/input.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source './ignored/do-not-migrate-me.html';
|
||||
@source '../node_modules/my-external-lib/template.html';
|
||||
|
||||
/*
|
||||
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
--- src/migrate-me.html ---
|
||||
<div class="order-0"></div>
|
||||
|
||||
--- node_modules/my-external-lib/template.html ---
|
||||
<div
|
||||
class="order-[0]"
|
||||
></div>
|
||||
|
||||
--- src/ignored/do-not-migrate-me.html ---
|
||||
<div class="order-[0]"></div>
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'upgrades JS config files with plugins',
|
||||
{
|
||||
|
|
@ -484,7 +380,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -586,7 +482,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -594,17 +490,17 @@ test(
|
|||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-gray-50: oklch(98.5% 0 none);
|
||||
--color-gray-100: oklch(97% 0 none);
|
||||
--color-gray-200: oklch(92.2% 0 none);
|
||||
--color-gray-300: oklch(87% 0 none);
|
||||
--color-gray-400: oklch(70.8% 0 none);
|
||||
--color-gray-500: oklch(55.6% 0 none);
|
||||
--color-gray-600: oklch(43.9% 0 none);
|
||||
--color-gray-700: oklch(37.1% 0 none);
|
||||
--color-gray-800: oklch(26.9% 0 none);
|
||||
--color-gray-900: oklch(20.5% 0 none);
|
||||
--color-gray-950: oklch(14.5% 0 none);
|
||||
--color-gray-50: oklch(98.5% 0 0);
|
||||
--color-gray-100: oklch(97% 0 0);
|
||||
--color-gray-200: oklch(92.2% 0 0);
|
||||
--color-gray-300: oklch(87% 0 0);
|
||||
--color-gray-400: oklch(70.8% 0 0);
|
||||
--color-gray-500: oklch(55.6% 0 0);
|
||||
--color-gray-600: oklch(43.9% 0 0);
|
||||
--color-gray-700: oklch(37.1% 0 0);
|
||||
--color-gray-800: oklch(26.9% 0 0);
|
||||
--color-gray-900: oklch(20.5% 0 0);
|
||||
--color-gray-950: oklch(14.5% 0 0);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -672,7 +568,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -750,7 +646,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -824,7 +720,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -934,7 +830,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1032,7 +928,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ root, exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade', {
|
||||
await exec('npx @tailwindcss/upgrade', {
|
||||
cwd: path.join(root, 'frontend'),
|
||||
})
|
||||
|
||||
|
|
@ -1119,7 +1015,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1194,7 +1090,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1270,7 +1166,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1346,7 +1242,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1408,7 +1304,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1470,7 +1366,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1509,7 +1405,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1580,7 +1476,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1683,7 +1579,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1793,7 +1689,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1895,7 +1791,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1945,243 +1841,3 @@ describe('border compatibility', () => {
|
|||
},
|
||||
)
|
||||
})
|
||||
|
||||
test(
|
||||
`future and experimental keys are supported`,
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.ts': ts`
|
||||
import { type Config } from 'tailwindcss'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
|
||||
module.exports = {
|
||||
darkMode: 'selector',
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
future: {
|
||||
hoverOnlyWhenSupported: true,
|
||||
respectDefaultRingColorOpacity: true,
|
||||
disableColorOpacityUtilitiesByDefault: true,
|
||||
relativeContentPathsByDefault: true,
|
||||
},
|
||||
experimental: {
|
||||
generalizedModifiers: true,
|
||||
},
|
||||
theme: {
|
||||
colors: {
|
||||
red: {
|
||||
400: '#f87171',
|
||||
500: 'red',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- src/input.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--color-*: initial;
|
||||
--color-red-400: #f87171;
|
||||
--color-red-500: red;
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('')
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
`unknown future keys dont migrate the config`,
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.ts': ts`
|
||||
import { type Config } from 'tailwindcss'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
|
||||
module.exports = {
|
||||
darkMode: 'selector',
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
future: {
|
||||
something: true,
|
||||
},
|
||||
} satisfies Config
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- src/input.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config '../tailwind.config.ts';
|
||||
|
||||
/*
|
||||
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(`
|
||||
"--- tailwind.config.ts ---
|
||||
import { type Config } from 'tailwindcss'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
|
||||
module.exports = {
|
||||
darkMode: 'selector',
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
future: {
|
||||
something: true,
|
||||
},
|
||||
} satisfies Config"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
`unknown experimental keys dont migrate the config`,
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.ts': ts`
|
||||
import { type Config } from 'tailwindcss'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
|
||||
module.exports = {
|
||||
darkMode: 'selector',
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
experimental: {
|
||||
something: true,
|
||||
},
|
||||
} satisfies Config
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- src/input.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@config '../tailwind.config.ts';
|
||||
|
||||
/*
|
||||
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(`
|
||||
"--- tailwind.config.ts ---
|
||||
import { type Config } from 'tailwindcss'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
|
||||
module.exports = {
|
||||
darkMode: 'selector',
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
experimental: {
|
||||
something: true,
|
||||
},
|
||||
} satisfies Config"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ test(
|
|||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
// Ensure we are in a git repo
|
||||
|
|
@ -40,7 +39,7 @@ test(
|
|||
await exec('git commit -m "before migration"')
|
||||
|
||||
// Fully upgrade to v4
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
// Undo all changes to the current repo. This will bring the repo back to a
|
||||
// v3 state, but the `node_modules` will now have v4 installed.
|
||||
|
|
@ -48,7 +47,7 @@ test(
|
|||
|
||||
// Re-running the upgrade should result in an error
|
||||
return expect(() => {
|
||||
return exec('pnpm exec upgrade', {}, { ignoreStdErr: true }).catch((e) => {
|
||||
return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => {
|
||||
// Replacing the current version with a hardcoded `v4` to make it stable
|
||||
// when we release new minor/patch versions.
|
||||
return Promise.reject(
|
||||
|
|
@ -56,7 +55,7 @@ test(
|
|||
)
|
||||
})
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Command failed: pnpm exec upgrade
|
||||
"Command failed: npx @tailwindcss/upgrade
|
||||
≈ tailwindcss v4.0.0
|
||||
|
||||
│ ↳ Upgrading from Tailwind CSS \`v4.0.0\`
|
||||
|
|
@ -106,7 +105,6 @@ test(
|
|||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
// Use `bun` to install dependencies
|
||||
|
|
@ -194,7 +192,6 @@ test(
|
|||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
// Use `npm` to install dependencies
|
||||
|
|
|
|||
|
|
@ -1,27 +1,22 @@
|
|||
import dedent from 'dedent'
|
||||
import fastGlob from 'fast-glob'
|
||||
import { exec, execFile, spawn, type ChildProcess } from 'node:child_process'
|
||||
import { exec, spawn } from 'node:child_process'
|
||||
import fs from 'node:fs/promises'
|
||||
import { createServer } from 'node:net'
|
||||
import { platform, tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { promisify, stripVTControlCharacters } from 'node:util'
|
||||
import { stripVTControlCharacters } from 'node:util'
|
||||
import { RawSourceMap, SourceMapConsumer } from 'source-map-js'
|
||||
import { test as defaultTest, type ExpectStatic } from 'vitest'
|
||||
import * as Yaml from 'yaml'
|
||||
import { createLineTable } from '../packages/tailwindcss/src/source-maps/line-table'
|
||||
import { escape } from '../packages/tailwindcss/src/utils/escape'
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..')
|
||||
const ROOT_PNPM_WORKSPACE = Yaml.parse(
|
||||
await fs.readFile(path.join(REPO_ROOT, 'pnpm-workspace.yaml'), 'utf8'),
|
||||
)
|
||||
const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((name) =>
|
||||
name.replace('tailwindcss-', '@tailwindcss/').replace('.tgz', ''),
|
||||
)
|
||||
|
||||
interface SpawnedProcess {
|
||||
dispose: () => Promise<void>
|
||||
dispose: () => void
|
||||
flush: () => void
|
||||
onStdout: (predicate: (message: string) => boolean) => Promise<void>
|
||||
onStderr: (predicate: (message: string) => boolean) => Promise<void>
|
||||
|
|
@ -42,9 +37,7 @@ interface TestConfig {
|
|||
[filePath: string]: string | Uint8Array
|
||||
}
|
||||
|
||||
timeout?: number
|
||||
installDependencies?: boolean
|
||||
retry?: number
|
||||
}
|
||||
interface TestContext {
|
||||
root: string
|
||||
|
|
@ -54,17 +47,15 @@ interface TestContext {
|
|||
parseSourceMap(opts: string | SourceMapOptions): SourceMap
|
||||
fs: {
|
||||
write(filePath: string, content: string, encoding?: BufferEncoding): Promise<void>
|
||||
symlink(dst: string, src: string): Promise<void>
|
||||
create(filePaths: string[]): Promise<void>
|
||||
read(filePath: string): Promise<string>
|
||||
delete(filePath: string): Promise<void>
|
||||
glob(pattern: string): Promise<[string, string][]>
|
||||
dumpFiles(pattern: string): Promise<string>
|
||||
expectFileToContain(
|
||||
filePath: string,
|
||||
contents: string | RegExp | (string | RegExp)[],
|
||||
): Promise<void>
|
||||
expectFileNotToContain(filePath: string, contents: string[]): Promise<void>
|
||||
expectFileNotToContain(filePath: string, contents: string | string[]): Promise<void>
|
||||
}
|
||||
}
|
||||
type TestCallback = (context: TestContext) => Promise<void> | void
|
||||
|
|
@ -72,15 +63,12 @@ interface TestFlags {
|
|||
only?: boolean
|
||||
skip?: boolean
|
||||
debug?: boolean
|
||||
concurrent?: boolean
|
||||
}
|
||||
|
||||
type SpawnActor = { predicate: (message: string) => boolean; resolve: () => void }
|
||||
|
||||
export const IS_WINDOWS = platform() === 'win32'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
const TEST_TIMEOUT = IS_WINDOWS ? 120000 : 60000
|
||||
const ASSERTION_TIMEOUT = IS_WINDOWS ? 10000 : 5000
|
||||
|
||||
|
|
@ -93,16 +81,16 @@ export function test(
|
|||
name: string,
|
||||
config: TestConfig,
|
||||
testCallback: TestCallback,
|
||||
{ only = false, skip = false, debug = false, concurrent = false }: TestFlags = {},
|
||||
{ only = false, skip = false, debug = false }: TestFlags = {},
|
||||
) {
|
||||
return defaultTest(
|
||||
name,
|
||||
{
|
||||
timeout: config.timeout ?? TEST_TIMEOUT,
|
||||
retry: config.retry ?? (process.env.CI ? 2 : 0),
|
||||
timeout: TEST_TIMEOUT,
|
||||
retry: process.env.CI ? 2 : 0,
|
||||
only: only || (!process.env.CI && debug),
|
||||
skip,
|
||||
concurrent,
|
||||
concurrent: true,
|
||||
},
|
||||
async (options) => {
|
||||
let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT
|
||||
|
|
@ -126,39 +114,6 @@ export function test(
|
|||
execOptions: ExecOptions = {},
|
||||
) {
|
||||
let cwd = childProcessOptions.cwd ?? root
|
||||
let originalCommand = command
|
||||
|
||||
// Avoid `pnpm exec upgrade` on Windows because the upgrader runs
|
||||
// `pnpm add`, which rewrites Windows `.cmd` shims while the current
|
||||
// shim is still executing.
|
||||
//
|
||||
// Pretty sure this is a pnpm v11 bug on Windows.
|
||||
if (IS_WINDOWS && command.includes('pnpm exec upgrade')) {
|
||||
for (let base = cwd; ; base = path.dirname(base)) {
|
||||
let upgradeBin = path.join(
|
||||
base,
|
||||
'node_modules',
|
||||
'@tailwindcss',
|
||||
'upgrade',
|
||||
'dist',
|
||||
'index.mjs',
|
||||
)
|
||||
|
||||
try {
|
||||
await fs.access(upgradeBin)
|
||||
command = command.replace('pnpm exec upgrade', `node "${upgradeBin}"`)
|
||||
break
|
||||
} catch (error: any) {
|
||||
if (error?.code !== 'ENOENT') throw error
|
||||
}
|
||||
|
||||
let parent = path.dirname(base)
|
||||
if (parent === base) {
|
||||
throw new Error(`Unable to resolve @tailwindcss/upgrade from ${cwd}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debug && cwd !== root) {
|
||||
let relative = path.relative(root, cwd)
|
||||
if (relative[0] !== '.') relative = `./${relative}`
|
||||
|
|
@ -178,11 +133,6 @@ export function test(
|
|||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
if (command !== originalCommand) {
|
||||
error.message = error.message.replace(command, originalCommand)
|
||||
let mappedError = error as any
|
||||
mappedError.cmd = originalCommand
|
||||
}
|
||||
if (execOptions.ignoreStdErr !== true) console.error(stderr)
|
||||
if (only || debug) {
|
||||
console.error(stdout)
|
||||
|
|
@ -219,7 +169,6 @@ export function test(
|
|||
if (debug) console.log(`>& ${command}`)
|
||||
let child = spawn(command, {
|
||||
cwd,
|
||||
detached: !IS_WINDOWS,
|
||||
shell: true,
|
||||
...childProcessOptions,
|
||||
env: {
|
||||
|
|
@ -228,26 +177,22 @@ export function test(
|
|||
},
|
||||
})
|
||||
|
||||
let disposed = false
|
||||
function dispose() {
|
||||
if (!child.kill()) {
|
||||
child.kill('SIGKILL')
|
||||
}
|
||||
|
||||
async function dispose() {
|
||||
if (disposed) return disposePromise
|
||||
disposed = true
|
||||
|
||||
await killProcessTree(child)
|
||||
|
||||
let timer = setTimeout(() => {
|
||||
forceKillProcessTree(child)
|
||||
rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`))
|
||||
}, ASSERTION_TIMEOUT)
|
||||
disposePromise.then(
|
||||
() => clearTimeout(timer),
|
||||
() => clearTimeout(timer),
|
||||
let timer = setTimeout(
|
||||
() =>
|
||||
rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)),
|
||||
ASSERTION_TIMEOUT,
|
||||
)
|
||||
disposePromise.finally(() => {
|
||||
clearTimeout(timer)
|
||||
})
|
||||
return disposePromise
|
||||
}
|
||||
disposables.push(dispose)
|
||||
|
||||
function onExit() {
|
||||
resolveDisposal?.()
|
||||
}
|
||||
|
|
@ -338,7 +283,11 @@ export function test(
|
|||
}
|
||||
},
|
||||
fs: {
|
||||
async write(filename, content, encoding = 'utf8') {
|
||||
async write(
|
||||
filename: string,
|
||||
content: string | Uint8Array,
|
||||
encoding: BufferEncoding = 'utf8',
|
||||
): Promise<void> {
|
||||
let full = path.join(root, filename)
|
||||
let dir = path.dirname(full)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
|
|
@ -349,8 +298,6 @@ export function test(
|
|||
|
||||
if (filename.endsWith('package.json')) {
|
||||
content = await overwriteVersionsInPackageJson(content)
|
||||
} else if (filename.endsWith('pnpm-workspace.yaml')) {
|
||||
content = overwriteVersionsInPnpmWorkspace(content)
|
||||
}
|
||||
|
||||
// Ensure that files written on Windows use \r\n line ending
|
||||
|
|
@ -361,19 +308,7 @@ export function test(
|
|||
await fs.writeFile(full, content, encoding)
|
||||
},
|
||||
|
||||
async symlink(target, src) {
|
||||
let targetAbsolute = path.join(root, target)
|
||||
let targetParent = path.dirname(targetAbsolute)
|
||||
await fs.mkdir(targetParent, { recursive: true })
|
||||
|
||||
let srcAbsolute = path.join(root, src)
|
||||
let srcParent = path.dirname(srcAbsolute)
|
||||
await fs.mkdir(srcParent, { recursive: true })
|
||||
|
||||
await fs.symlink(targetAbsolute, srcAbsolute)
|
||||
},
|
||||
|
||||
async create(filenames) {
|
||||
async create(filenames: string[]): Promise<void> {
|
||||
for (let filename of filenames) {
|
||||
let full = path.join(root, filename)
|
||||
|
||||
|
|
@ -383,11 +318,7 @@ export function test(
|
|||
}
|
||||
},
|
||||
|
||||
async delete(filename) {
|
||||
await fs.unlink(path.join(root, filename))
|
||||
},
|
||||
|
||||
async read(filePath) {
|
||||
async read(filePath: string) {
|
||||
let content = await fs.readFile(path.resolve(root, filePath), 'utf8')
|
||||
|
||||
// Ensure that files read on Windows have \r\n line endings removed
|
||||
|
|
@ -397,7 +328,7 @@ export function test(
|
|||
|
||||
return content
|
||||
},
|
||||
async glob(pattern) {
|
||||
async glob(pattern: string) {
|
||||
let files = await fastGlob(pattern, { cwd: root })
|
||||
return Promise.all(
|
||||
files.map(async (file) => {
|
||||
|
|
@ -410,7 +341,7 @@ export function test(
|
|||
}),
|
||||
)
|
||||
},
|
||||
async dumpFiles(pattern) {
|
||||
async dumpFiles(pattern: string) {
|
||||
let files = await context.fs.glob(pattern)
|
||||
return `\n${files
|
||||
.slice()
|
||||
|
|
@ -469,44 +400,21 @@ export function test(
|
|||
node_modules/
|
||||
`
|
||||
|
||||
// Disable Git's automatic line ending conversion in these throwaway test
|
||||
// repositories. Tools like pnpm write files (e.g. `pnpm-lock.yaml`) with
|
||||
// LF endings, which on Windows (where `core.autocrlf=true` is common)
|
||||
// makes Git emit noisy "LF will be replaced by CRLF" warnings when it
|
||||
// touches those files.
|
||||
config.fs['.gitattributes'] ??= txt`
|
||||
* -text
|
||||
`
|
||||
|
||||
// Ensure there is always a root `pnpm-workspace.yaml` so that transitive
|
||||
// dependency overrides (and the build allow list) are injected into it.
|
||||
// As of pnpm v10, these can no longer live in the `pnpm.overrides` field
|
||||
// of `package.json`.
|
||||
config.fs['pnpm-workspace.yaml'] ??= ''
|
||||
|
||||
for (let [filename, content] of Object.entries(config.fs)) {
|
||||
if (content.toString().startsWith('symlink:')) {
|
||||
// The symlink path is relative to the target destination's path
|
||||
let target = path.join(
|
||||
filename,
|
||||
content.toString().slice('symlink:'.length), // Relative path
|
||||
)
|
||||
|
||||
await context.fs.symlink(target, filename)
|
||||
} else {
|
||||
await context.fs.write(filename, content)
|
||||
}
|
||||
await context.fs.write(filename, content)
|
||||
}
|
||||
|
||||
let shouldInstallDependencies = config.installDependencies ?? true
|
||||
|
||||
try {
|
||||
// Every test project gets its own root `pnpm-workspace.yaml` (see
|
||||
// above). This makes the test directory its own workspace root, so even
|
||||
// in debug mode — where the directory lives inside this repository's
|
||||
// pnpm workspace — `pnpm install` won't attach to the parent workspace.
|
||||
// In debug mode, the directory is going to be inside the pnpm workspace
|
||||
// of the tailwindcss package. This means that `pnpm install` will run
|
||||
// pnpm install on the workspace instead (expect if the root dir defines
|
||||
// a separate workspace). We work around this by using the
|
||||
// `--ignore-workspace` flag.
|
||||
if (shouldInstallDependencies) {
|
||||
await context.exec(`pnpm install`)
|
||||
let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml']
|
||||
await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
|
|
@ -518,18 +426,11 @@ export function test(
|
|||
let disposables: (() => Promise<void>)[] = []
|
||||
|
||||
async function dispose() {
|
||||
let results = await Promise.allSettled(disposables.map((dispose) => dispose()))
|
||||
await Promise.all(disposables.map((dispose) => dispose()))
|
||||
|
||||
if (!debug) {
|
||||
await gracefullyRemove(root)
|
||||
}
|
||||
|
||||
let errors = results.flatMap((result) =>
|
||||
result.status === 'rejected' ? [result.reason] : [],
|
||||
)
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors, 'Failed to clean up spawned processes')
|
||||
}
|
||||
}
|
||||
|
||||
options.onTestFinished(dispose)
|
||||
|
|
@ -539,7 +440,7 @@ export function test(
|
|||
try {
|
||||
await context.exec('git init', { cwd: root })
|
||||
await context.exec('git add --all', { cwd: root })
|
||||
await context.exec('git commit -m "before migration" --allow-empty', { cwd: root })
|
||||
await context.exec('git commit -m "before migration"', { cwd: root })
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
console.error(error.stdout?.toString())
|
||||
|
|
@ -558,9 +459,6 @@ test.only = (name: string, config: TestConfig, testCallback: TestCallback) => {
|
|||
test.skip = (name: string, config: TestConfig, testCallback: TestCallback) => {
|
||||
return test(name, config, testCallback, { skip: true })
|
||||
}
|
||||
test.concurrent = (name: string, config: TestConfig, testCallback: TestCallback) => {
|
||||
return test(name, config, testCallback, { concurrent: true })
|
||||
}
|
||||
test.debug = (name: string, config: TestConfig, testCallback: TestCallback) => {
|
||||
return test(name, config, testCallback, { debug: true })
|
||||
}
|
||||
|
|
@ -584,43 +482,27 @@ async function overwriteVersionsInPackageJson(content: string): Promise<string>
|
|||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(json, null, 2)
|
||||
}
|
||||
|
||||
function overwriteVersionsInPnpmWorkspace(content: string): string {
|
||||
let workspace = content.trim() === '' ? {} : Yaml.parse(content)
|
||||
|
||||
// Inherit information from the root workspace
|
||||
workspace.allowBuilds = {
|
||||
...ROOT_PNPM_WORKSPACE.allowBuilds,
|
||||
...workspace.allowBuilds,
|
||||
}
|
||||
|
||||
// Inject transitive dependency overwrite. This is necessary because
|
||||
// @tailwindcss/vite internally depends on a specific version of
|
||||
// @tailwindcss/oxide and we instead want to resolve it to the locally built
|
||||
// version.
|
||||
//
|
||||
// As of pnpm v10, the `pnpm.overrides` field in `package.json` is no longer
|
||||
// read. Overrides have to be defined in `pnpm-workspace.yaml` instead.
|
||||
workspace.overrides ||= {}
|
||||
json.pnpm ||= {}
|
||||
json.pnpm.overrides ||= {}
|
||||
for (let pkg of PUBLIC_PACKAGES) {
|
||||
if (pkg === 'tailwindcss') {
|
||||
// We want to be explicit about the `tailwindcss` package so our tests can
|
||||
// also import v3 without conflicting v4 tarballs.
|
||||
workspace.overrides['@tailwindcss/node>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/turbopack>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/node>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg)
|
||||
} else {
|
||||
workspace.overrides[pkg] = resolveVersion(pkg)
|
||||
json.pnpm.overrides[pkg] = resolveVersion(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
return Yaml.stringify(workspace)
|
||||
return JSON.stringify(json, null, 2)
|
||||
}
|
||||
|
||||
function resolveVersion(dependency: string) {
|
||||
|
|
@ -670,7 +552,6 @@ export async function retryAssertion<T>(
|
|||
try {
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
Error.captureStackTrace(err, retryAssertion)
|
||||
error = err
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
|
|
@ -686,7 +567,7 @@ export async function fetchStyles(base: string, path = '/'): Promise<string> {
|
|||
let index = await fetch(`${base}${path}`)
|
||||
let html = await index.text()
|
||||
|
||||
let linkRegex = /<link rel="stylesheet" href="([a-zA-Z0-9/_.?=%-]+)"/gi
|
||||
let linkRegex = /<link rel="stylesheet" href="([a-zA-Z0-9\/_\.\?=%-]+)"/gi
|
||||
let styleRegex = /<style\b[^>]*>([\s\S]*?)<\/style>/gi
|
||||
|
||||
let stylesheets: string[] = []
|
||||
|
|
@ -723,65 +604,6 @@ export async function fetchStyles(base: string, path = '/'): Promise<string> {
|
|||
}, '')
|
||||
}
|
||||
|
||||
export async function getRandomPort() {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
let server = createServer()
|
||||
server.unref()
|
||||
server.on('error', reject)
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
let address = server.address()
|
||||
server.close(() => {
|
||||
if (address && typeof address === 'object') {
|
||||
resolve(address.port)
|
||||
} else {
|
||||
reject(new Error('Unable to allocate random port'))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function killProcessTree(child: ChildProcess) {
|
||||
if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
await execFileAsync('taskkill', ['/pid', String(child.pid), '/T', '/F'], {
|
||||
timeout: ASSERTION_TIMEOUT,
|
||||
windowsHide: true,
|
||||
}).catch(() => {})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(-child.pid, 'SIGTERM')
|
||||
} catch (error: any) {
|
||||
if (error?.code !== 'ESRCH') {
|
||||
child.kill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function forceKillProcessTree(child: ChildProcess) {
|
||||
if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
execFile('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true }, () => {})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(-child.pid, 'SIGKILL')
|
||||
} catch (error: any) {
|
||||
if (error?.code !== 'ESRCH') {
|
||||
child.kill('SIGKILL')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function gracefullyRemove(dir: string) {
|
||||
// Skip removing the directory in CI because it can stall on Windows
|
||||
if (!process.env.CI) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils'
|
||||
import { candidate, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils'
|
||||
|
||||
test(
|
||||
'dev mode',
|
||||
|
|
@ -129,73 +129,3 @@ test(
|
|||
await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`overline`])
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19677
|
||||
test(
|
||||
'import aliases should work in <style> blocks',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"astro": "^5",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'astro.config.mjs': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'astro/config'
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
vite: { plugins: [tailwindcss()] },
|
||||
})
|
||||
`,
|
||||
'tsconfig.json': json`
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@styles/*": ["./src/styles/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
// prettier-ignore
|
||||
'src/pages/index.astro': html`
|
||||
---
|
||||
import '@styles/global.css'
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro</title>
|
||||
<style>
|
||||
@reference "@styles/global.css";
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="underline">Astro</h1>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'src/styles/global.css': css`@import 'tailwindcss';`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm astro build')
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
|
||||
await fs.expectFileToContain(files[0][0], [candidate`underline`])
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,193 +0,0 @@
|
|||
import path from 'node:path'
|
||||
import { candidate, css, html, json, retryAssertion, test, ts, txt, yaml } from '../utils'
|
||||
|
||||
// Vite's experimental `bundledDev` mode invokes the `hotUpdate` hook without a
|
||||
// `server`, which used to crash the plugin on every file edit.
|
||||
//
|
||||
// - https://github.com/tailwindlabs/tailwindcss/issues/20378
|
||||
// - https://vite.dev/blog/announcing-vite8-1#experimental-bundled-dev-mode
|
||||
test(
|
||||
'dev mode (experimental `bundledDev`)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': txt`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8.1"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
bundledDev: true,
|
||||
},
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'project-a/index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@reference 'tailwindcss/theme';
|
||||
@import 'tailwindcss/utilities';
|
||||
@source '../../project-b/src/**/*.html';
|
||||
`,
|
||||
'project-b/src/index.html': html`
|
||||
<div class="flex" />
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, spawn, fs, expect }) => {
|
||||
let process = await spawn('pnpm vite dev', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
env: {
|
||||
// Some CI machines have an upstream rolldown bug where its file watcher
|
||||
// never delivers a single event. The debug output is the only way to
|
||||
// tell that apart from a real regression (see below).
|
||||
DEBUG: 'vite:full-bundle-mode',
|
||||
},
|
||||
})
|
||||
|
||||
// Any debug output beyond the "INITIAL:" startup lines proves rolldown's
|
||||
// watcher delivered at least one file event.
|
||||
let watcherAlive = false
|
||||
|
||||
// `hotUpdate` errors don't kill the dev server, they are only printed to
|
||||
// stderr. Track them explicitly so a crash fails the test even if the
|
||||
// rebuild happens to succeed anyway.
|
||||
let pluginErrors: string[] = []
|
||||
process.onStderr((message) => {
|
||||
if (message.includes('vite:full-bundle-mode')) {
|
||||
if (!message.includes('INITIAL:')) watcherAlive = true
|
||||
} else if (message.includes('@tailwindcss/vite')) {
|
||||
pluginErrors.push(message)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)\//.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
// In `bundledDev` mode the stylesheet is not served separately. Instead the
|
||||
// generated CSS is embedded in the bundled JS and injected at runtime, so
|
||||
// extract the bundle from the served HTML. While the bundle is being built,
|
||||
// Vite serves a temporary fallback page instead.
|
||||
//
|
||||
// The bundle can be split into multiple chunks (e.g. the HMR client runtime
|
||||
// and the app itself), and the chunk containing the CSS is not always the
|
||||
// first one, so fetch every referenced script and stylesheet.
|
||||
async function fetchBundledStyles(): Promise<string> {
|
||||
let index = await fetch(`${url}/`)
|
||||
let html = await index.text()
|
||||
if (html.includes('__vite_is_fallback_page__')) {
|
||||
throw new Error('Bundling still in progress')
|
||||
}
|
||||
|
||||
let sources = [
|
||||
...html.matchAll(/<script[^>]*\ssrc="([^"]+)"/g),
|
||||
...html.matchAll(/<link[^>]*\srel="stylesheet"[^>]*\shref="([^"]+)"/g),
|
||||
].map((match) => match[1])
|
||||
if (sources.length === 0) throw new Error(`No scripts or stylesheets found in:\n\n${html}`)
|
||||
|
||||
let contents = await Promise.all(
|
||||
sources.map(async (src) => {
|
||||
let response = await fetch(new URL(src, `${url}/`))
|
||||
return await response.text()
|
||||
}),
|
||||
)
|
||||
return contents.join('\n')
|
||||
}
|
||||
|
||||
await retryAssertion(
|
||||
async () => {
|
||||
let styles = await fetchBundledStyles()
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
expect(styles).toContain(candidate`flex`)
|
||||
},
|
||||
{ timeout: 10_000, delay: 100 },
|
||||
)
|
||||
|
||||
// Edit a module file and a manually `@source`d file. Without a connected
|
||||
// HMR websocket client (this test only uses HTTP fetches), a file change
|
||||
// does not trigger an eager rebuild — it only marks the bundle as stale,
|
||||
// and the next fetch kicks off the rebuild. So poll with fetches without
|
||||
// touching the files again: rewriting would re-mark the bundle stale right
|
||||
// before each fetch and the rebuild could never be observed.
|
||||
await fs.write(
|
||||
'project-a/index.html',
|
||||
html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline m-2">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
)
|
||||
|
||||
await fs.write('project-b/src/index.html', html`
|
||||
<div class="flex font-bold" />
|
||||
`)
|
||||
|
||||
try {
|
||||
await retryAssertion(
|
||||
async () => {
|
||||
let styles = await fetchBundledStyles()
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
expect(styles).toContain(candidate`flex`)
|
||||
expect(styles).toContain(candidate`m-2`)
|
||||
expect(styles).toContain(candidate`font-bold`)
|
||||
},
|
||||
{ timeout: 15_000, delay: 100 },
|
||||
)
|
||||
} catch (error) {
|
||||
// If rolldown's watcher delivered any file event, the updates were
|
||||
// genuinely dropped somewhere along the way — a real failure.
|
||||
if (watcherAlive) throw error
|
||||
|
||||
// Otherwise the watcher never delivered a single event, so the updates
|
||||
// can never be observed no matter how long we wait — an upstream
|
||||
// rolldown bug on some CI machines, not a plugin regression. The crash
|
||||
// regression this test guards is still covered: `hotUpdate` is driven
|
||||
// by Vite's own (working) chokidar watcher and asserted via
|
||||
// `pluginErrors` below.
|
||||
//
|
||||
// Skipping is only sound while the server still healthily serves the
|
||||
// original bundle. If it stopped serving (crashed, or wedged on the
|
||||
// fallback page), that's real breakage a silent watcher must not mask.
|
||||
console.log(error)
|
||||
let styles = await fetchBundledStyles()
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
expect(styles).toContain(candidate`flex`)
|
||||
console.warn(
|
||||
'Skipping update assertions: rolldown’s file watcher delivered no events on this machine.',
|
||||
)
|
||||
}
|
||||
|
||||
expect(pluginErrors).toEqual([])
|
||||
},
|
||||
)
|
||||
|
|
@ -7,7 +7,6 @@ import {
|
|||
html,
|
||||
js,
|
||||
json,
|
||||
jsx,
|
||||
retryAssertion,
|
||||
test,
|
||||
ts,
|
||||
|
|
@ -294,113 +293,6 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20346
|
||||
test(
|
||||
'dev mode + symlinked `@source` files pick up changes to the real file',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
- packages/*
|
||||
`,
|
||||
'project-a/package.json': txt`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"repro-package": "workspace:*",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''}
|
||||
"vite": "^7"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'project-a/index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
|
||||
// The `@source` points through the `node_modules` symlink that pnpm
|
||||
// creates for the workspace package. Vite's file watcher ignores
|
||||
// `node_modules` by default, so the symlinked path can not be watched.
|
||||
// The real file (in `packages/repro-package`) has to be watched
|
||||
// instead.
|
||||
'project-a/src/index.css': css`
|
||||
@reference 'tailwindcss/theme';
|
||||
@import 'tailwindcss/utilities';
|
||||
@source '../node_modules/repro-package/source.html';
|
||||
`,
|
||||
'packages/repro-package/package.json': json`
|
||||
{
|
||||
"name": "repro-package",
|
||||
"private": true,
|
||||
"version": "1.0.0"
|
||||
}
|
||||
`,
|
||||
'packages/repro-package/source.html': html`
|
||||
<div class="content-['v1']">
|
||||
Hello, world!
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, spawn, fs, expect }) => {
|
||||
let process = await spawn('pnpm vite dev', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
await process.onStdout((m) => m.includes('ready in'))
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)\//.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
expect(styles).toContain(candidate`content-['v1']`)
|
||||
})
|
||||
|
||||
await retryAssertion(async () => {
|
||||
// Changes to the real file (behind the symlink) should be picked up
|
||||
await fs.write(
|
||||
'packages/repro-package/source.html',
|
||||
html`
|
||||
<div class="content-['v1'] content-['v2']">
|
||||
Hello, world!
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
|
||||
let styles = await fetchStyles(url)
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
expect(styles).toContain(candidate`content-['v1']`)
|
||||
expect(styles).toContain(candidate`content-['v2']`)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'watch mode',
|
||||
{
|
||||
|
|
@ -580,352 +472,6 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
},
|
||||
)
|
||||
|
||||
describe.each([['^6'], ['7.0.8'], ['7.1.12'], ['7.3.1'], ['8.0.0']])(
|
||||
'Using Vite %s',
|
||||
(version) => {
|
||||
test(
|
||||
'external source file changes update the CSS',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''}
|
||||
"vite": "${version}"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/vite.config.ts': ts`
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
logLevel: 'info',
|
||||
})
|
||||
`,
|
||||
'project-a/index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'project-a/src/main.ts': jsx`import { classes } from './app'`,
|
||||
'project-a/src/app.ts': jsx`export let classes = "content-['project-a/src/app.ts']"`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@source '../../project-b/**/*.php';
|
||||
`,
|
||||
'project-b/src/index.php': html`
|
||||
<div
|
||||
class="content-['project-b/src/index.php']"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, spawn, fs, expect }) => {
|
||||
let process = await spawn('pnpm vite dev --debug hmr', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
await process.onStdout((m) => m.includes('ready in'))
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)\//.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['project-b/src/index.php']`)
|
||||
})
|
||||
|
||||
// Flush all messages so that we can be sure the next messages are
|
||||
// from the file changes we're about to make
|
||||
process.flush()
|
||||
|
||||
// Changing an external .php file hot-updates the generated CSS
|
||||
{
|
||||
await fs.write(
|
||||
'project-b/src/index.php',
|
||||
txt`<div class="content-['updated:project-b/src/index.php']"></div>`,
|
||||
)
|
||||
|
||||
// On Vite < 7.1, Vite itself hard-invalidates watched files that
|
||||
// aren't part of the module graph and reloads the page.
|
||||
//
|
||||
// On newer versions nothing reloads the page: the CSS hot-updates
|
||||
// through the regular pipeline because the changed file is a
|
||||
// watch dependency of the CSS root.
|
||||
//
|
||||
// Reloading the page for external template changes is the
|
||||
// responsibility of the backend integration (e.g. `laravel-vite-plugin`'s `refresh` option, or `vite-plugin-full-reload`).
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20411
|
||||
if (version === '^6' || version === '7.0.8') {
|
||||
await process.onStdout((m) => m.includes('page reload') && m.includes('index.php'))
|
||||
} else {
|
||||
await process.onStdout((m) => m.includes('hmr update') && m.includes('index.css'))
|
||||
}
|
||||
|
||||
// Ensure the styles were regenerated with the new content
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:project-b/src/index.php']`)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20320
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19903
|
||||
test(
|
||||
'editing scanned files that Vite can process as modules does not trigger a full reload',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''}
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/vite.config.ts': ts`
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
|
||||
build: { cssMinify: false },
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
{
|
||||
// A plugin that processes a custom file type into a JS
|
||||
// module, similar to e.g. \`.vue\` or \`.svelte\` files
|
||||
name: 'custom-file-type',
|
||||
transform(code, id) {
|
||||
if (id.endsWith('.custom')) {
|
||||
return { code: 'export default ' + JSON.stringify(code), map: null }
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
// Log all HMR payloads to a file so the test can assert on
|
||||
// them
|
||||
name: 'hmr-wiretap',
|
||||
configureServer(server) {
|
||||
let logFile = path.resolve('hmr.log')
|
||||
fs.writeFileSync(logFile, '')
|
||||
for (let environment of Object.values(server.environments)) {
|
||||
let send = environment.hot.send.bind(environment.hot)
|
||||
environment.hot.send = (payload) => {
|
||||
fs.appendFileSync(logFile, JSON.stringify(payload) + '\\n')
|
||||
return send(payload)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
`,
|
||||
'project-a/index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'project-a/src/main.ts': ts`
|
||||
import compA from './comp-a.custom'
|
||||
import snippet from './snippet.php?raw'
|
||||
console.log(compA, snippet)
|
||||
`,
|
||||
'project-a/src/snippet.php': html`
|
||||
<div
|
||||
class="content-['src/snippet.php']"
|
||||
></div>
|
||||
`,
|
||||
'project-a/src/comp-a.custom': html`
|
||||
<div
|
||||
class="content-['src/comp-a.custom']"
|
||||
></div>
|
||||
`,
|
||||
'project-a/src/comp-b.custom': html`
|
||||
<div
|
||||
class="content-['src/comp-b.custom']"
|
||||
></div>
|
||||
`,
|
||||
'project-a/src/lazy.tsx': jsx`
|
||||
export default function Lazy() {
|
||||
return <div className="content-['src/lazy.tsx']" />
|
||||
}
|
||||
`,
|
||||
'project-a/src/unimported.css': css`
|
||||
.unimported {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@source '../../project-b/**/*.php';
|
||||
`,
|
||||
'project-b/src/index.php': html`
|
||||
<div
|
||||
class="content-['project-b/src/index.php']"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, spawn, fs, expect }) => {
|
||||
let process = await spawn('pnpm vite dev', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
await process.onStdout((m) => m.includes('ready in'))
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)\//.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['src/lazy.tsx']`)
|
||||
expect(styles).toContain(candidate`content-['src/comp-b.custom']`)
|
||||
expect(styles).toContain(candidate`content-['project-b/src/index.php']`)
|
||||
})
|
||||
|
||||
// Load `main.ts`, `comp-a.custom`, and `snippet.php?raw` as real
|
||||
// modules, like a browser visiting the page would
|
||||
await fetch(`${url}/src/main.ts`)
|
||||
await fetch(`${url}/src/comp-a.custom?import`)
|
||||
await fetch(`${url}/src/snippet.php?raw`)
|
||||
|
||||
// Changing a scanned `.tsx` file that is not part of the module graph
|
||||
// (e.g. a lazily-loaded route that hasn't been visited yet) should not
|
||||
// trigger a full reload, but new classes should still apply
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20320
|
||||
{
|
||||
await fs.write(
|
||||
'project-a/src/lazy.tsx',
|
||||
jsx`
|
||||
export default function Lazy() {
|
||||
return <div className="content-['updated:src/lazy.tsx']" />
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/lazy.tsx']`)
|
||||
})
|
||||
}
|
||||
|
||||
// The same holds for a custom file type as long as some file of the
|
||||
// same type was processed as a module before
|
||||
{
|
||||
await fs.write(
|
||||
'project-a/src/comp-b.custom',
|
||||
html`<div class="content-['updated:src/comp-b.custom']"></div>`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/comp-b.custom']`)
|
||||
})
|
||||
}
|
||||
|
||||
// Changing a scanned stylesheet that is not part of the module graph
|
||||
// (e.g. a component stylesheet that a framework plugin compiles into
|
||||
// the component) should not trigger a full reload either
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19903
|
||||
{
|
||||
let updates = (await fs.read('project-a/hmr.log')).split('"type":"update"').length
|
||||
|
||||
await fs.write(
|
||||
'project-a/src/unimported.css',
|
||||
css`
|
||||
.unimported {
|
||||
color: blue;
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
// Wait until the change was handled and an update was pushed
|
||||
await retryAssertion(async () => {
|
||||
let log = await fs.read('project-a/hmr.log')
|
||||
expect(log.split('"type":"update"').length).toBeGreaterThan(updates)
|
||||
})
|
||||
}
|
||||
|
||||
// Changing an external file (e.g. a PHP template) hot-updates the
|
||||
// generated CSS but does not trigger a full reload either. Reloading the
|
||||
// page for external template changes is the responsibility of the backend
|
||||
// integration (e.g. `laravel-vite-plugin`'s `refresh` option, or
|
||||
// `vite-plugin-full-reload`).
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20411
|
||||
{
|
||||
let updates = (await fs.read('project-a/hmr.log')).split('"type":"update"').length
|
||||
|
||||
await fs.write(
|
||||
'project-b/src/index.php',
|
||||
html`<div class="content-['updated:project-b/src/index.php']"></div>`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let log = await fs.read('project-a/hmr.log')
|
||||
expect(log.split('"type":"update"').length).toBeGreaterThan(updates)
|
||||
})
|
||||
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:project-b/src/index.php']`)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
`source(none) disables looking at the module graph`,
|
||||
{
|
||||
|
|
@ -1178,92 +724,12 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
async ({ root, fs, exec, expect }) => {
|
||||
await expect(() =>
|
||||
exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }),
|
||||
).rejects.toThrow('The `source(../i-do-not-exist)` does not exist')
|
||||
).rejects.toThrowError('The `source(../i-do-not-exist)` does not exist')
|
||||
|
||||
let files = await fs.glob('project-a/dist/**/*.css')
|
||||
expect(files).toHaveLength(0)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'source(…) and `@source` are relative to the file they are in',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''}
|
||||
"vite": "^7"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="/index.css" />
|
||||
</head>
|
||||
<body></body>
|
||||
`,
|
||||
'index.css': css` @import './project-a/src/index.css'; `,
|
||||
|
||||
'project-a/src/index.css': css`
|
||||
/* Run auto-content detection in ../../project-b */
|
||||
@import 'tailwindcss/utilities' source('../../project-b');
|
||||
|
||||
/* Explicitly using node_modules in the @source allows git ignored folders */
|
||||
@source '../../project-c';
|
||||
`,
|
||||
|
||||
// Project A is the current folder, but we explicitly configured
|
||||
// `source(project-b)`, therefore project-a should not be included in
|
||||
// the output.
|
||||
'project-a/src/index.html': html`
|
||||
<div
|
||||
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
|
||||
></div>
|
||||
`,
|
||||
|
||||
// Project B is the configured `source(…)`, therefore auto source
|
||||
// detection should include known extensions and folders in the output.
|
||||
'project-b/src/index.html': html`
|
||||
<div
|
||||
class="content-['project-b/src/index.html']"
|
||||
></div>
|
||||
`,
|
||||
|
||||
// Project C should apply auto source detection, therefore known
|
||||
// extensions and folders should be included in the output.
|
||||
'project-c/src/index.html': html`
|
||||
<div
|
||||
class="content-['project-c/src/index.html']"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, spawn, root, expect }) => {
|
||||
await exec('pnpm vite build', { cwd: root })
|
||||
|
||||
let content = await fs.dumpFiles('./dist/assets/*.css')
|
||||
|
||||
expect(content).not.toContain(candidate`content-['project-a/src/index.html']`)
|
||||
expect(content).toContain(candidate`content-['project-b/src/index.html']`)
|
||||
expect(content).toContain(candidate`content-['project-c/src/index.html']`)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test(
|
||||
|
|
@ -1432,14 +898,6 @@ test(
|
|||
</head>
|
||||
`,
|
||||
'src/index.js': js`import Plotly from 'plotly.js/lib/core'`,
|
||||
|
||||
// `plotly.js` pulls in `es5-ext`, which has a build script. As of pnpm
|
||||
// v10, build scripts only run when explicitly allowed.
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
allowBuilds:
|
||||
es5-ext: true
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, expect, fs }) => {
|
||||
|
|
@ -1487,7 +945,7 @@ test(
|
|||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="focus:text-[black]">Hello, world!</div>
|
||||
<div class="hover:flex">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
|
|
@ -1499,16 +957,16 @@ test(
|
|||
async ({ exec, expect, fs }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
// Should not be minified when optimize is disabled
|
||||
expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-<hash>.css'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/assets/index-<hash>.css ---
|
||||
.focus\\:text-\\[black\\]:focus {
|
||||
color: black;
|
||||
}
|
||||
"
|
||||
`)
|
||||
let content = await fs.read(filename)
|
||||
expect(content).toContain('.hover\\:flex {')
|
||||
expect(content).toContain('&:hover {')
|
||||
expect(content).toContain('@media (hover: hover) {')
|
||||
expect(content).toContain('display: flex;')
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -1542,7 +1000,7 @@ test(
|
|||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="hover:text-[black]">Hello, world!</div>
|
||||
<div class="hover:flex">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
|
|
@ -1554,173 +1012,14 @@ test(
|
|||
async ({ exec, expect, fs }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-<hash>.css'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/assets/index-<hash>.css ---
|
||||
@media (hover: hover) {
|
||||
.hover\\:text-\\[black\\]:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
`the plugin works when using the environment API`,
|
||||
{
|
||||
fs: {
|
||||
'package.json': txt`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss()],
|
||||
builder: {},
|
||||
environments: {
|
||||
server: {
|
||||
build: {
|
||||
cssMinify: false,
|
||||
emitAssets: true,
|
||||
rollupOptions: { input: './src/server.ts' },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
`,
|
||||
// Has to exist or the build fails
|
||||
'index.html': html`
|
||||
<div class="content-['index.html']"></div>
|
||||
`,
|
||||
'src/server.ts': js`
|
||||
// Import the stylesheet in the server build
|
||||
import a from './index.css?url'
|
||||
console.log(a)
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@reference 'tailwindcss/theme';
|
||||
@import 'tailwindcss/utilities';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, fs, exec, expect }) => {
|
||||
await exec('pnpm vite build', { cwd: root })
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
await fs.expectFileToContain(filename, [candidate`content-['index.html']`])
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17532
|
||||
test(
|
||||
'deleting a file should not crash Vite',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "6.2.5"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<body>
|
||||
<div id="app" class="underline"></div>
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
`,
|
||||
'src/index.css': css`@import 'tailwindcss';`,
|
||||
'src/main.js': js`
|
||||
import iconUrl from './assets/icon.svg?url'
|
||||
import './index.css'
|
||||
|
||||
document.querySelector('#app').innerHTML = iconUrl
|
||||
`,
|
||||
'src/assets/icon.svg': html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1">
|
||||
<rect class="fill-red-500" width="1" height="1" />
|
||||
</svg>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, spawn, expect }) => {
|
||||
let process = await spawn('pnpm vite dev')
|
||||
await process.onStdout((m) => m.includes('ready in'))
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)\//.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let [main, css] = await Promise.all([
|
||||
fetch(`${url}/src/main.js`).then((r) => r.text()),
|
||||
fetch(`${url}/src/index.css`).then((r) => r.text()),
|
||||
])
|
||||
|
||||
expect(main).toContain('/src/assets/icon.svg?import&url')
|
||||
expect(main).toContain('/src/index.css')
|
||||
expect(css).toContain('.fill-red-500')
|
||||
})
|
||||
|
||||
process.flush()
|
||||
|
||||
let unexpectedError = process.onStderr((m) => {
|
||||
return /error|crash|panic/i.test(m)
|
||||
})
|
||||
|
||||
await fs.write(
|
||||
'src/main.js',
|
||||
js`
|
||||
import './index.css'
|
||||
document.querySelector('#app').innerHTML = 'Hello World'
|
||||
`,
|
||||
)
|
||||
|
||||
await fs.delete('src/assets/icon.svg')
|
||||
|
||||
let result = await Promise.race([
|
||||
unexpectedError.then(() => 'stderr'),
|
||||
new Promise((resolve) => setTimeout(() => resolve('timeout'), 500)),
|
||||
])
|
||||
|
||||
expect(result).toBe('timeout')
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let response = await fetch(`${url}/src/index.css?t=${Date.now()}`)
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
// Should be optimized but not minified
|
||||
let content = await fs.read(filename)
|
||||
expect(content).toContain('@media (hover: hover) {')
|
||||
expect(content).toContain('.hover\\:flex:hover {')
|
||||
expect(content).toContain('display: flex;')
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue