Compare commits
No commits in common. "main" and "v4.2.2" have entirely different histories.
225 changed files with 20172 additions and 38131 deletions
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
|
|
@ -27,10 +27,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 +47,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@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
|
|
@ -72,7 +70,7 @@ jobs:
|
|||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
|
|
@ -90,7 +88,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
|
||||
|
|
@ -118,7 +116,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
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.'
|
||||
|
|
|
|||
16
.github/workflows/integration-tests.yml
vendored
16
.github/workflows/integration-tests.yml
vendored
|
|
@ -55,25 +55,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@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
|
|
@ -82,7 +82,7 @@ jobs:
|
|||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
|
|
@ -119,7 +119,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
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.'
|
||||
|
|
|
|||
213
.github/workflows/prepare-release.yml
vendored
213
.github/workflows/prepare-release.yml
vendored
|
|
@ -2,12 +2,6 @@ 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*'
|
||||
|
|
@ -15,7 +9,6 @@ on:
|
|||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '11.9.0'
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
permissions:
|
||||
|
|
@ -54,40 +47,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@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v6
|
||||
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 +92,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@v5
|
||||
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@v5
|
||||
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@v6
|
||||
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@v6
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -154,11 +157,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v6
|
||||
- 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 +175,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 +191,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@v6
|
||||
with:
|
||||
name: bindings-x86_64-unknown-freebsd
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -201,17 +202,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@v6
|
||||
with:
|
||||
fetch-depth: 20
|
||||
persist-credentials: false
|
||||
|
||||
- run: git fetch --tags -f
|
||||
|
||||
|
|
@ -220,25 +222,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@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v5
|
||||
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@v5
|
||||
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@v7
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
|
|
@ -277,31 +305,30 @@ jobs:
|
|||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload standalone artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: tailwindcss-standalone
|
||||
path: packages/@tailwindcss-standalone/dist/
|
||||
|
||||
- name: Upload npm package tarballs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v6
|
||||
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
|
||||
|
|
|
|||
322
.github/workflows/release-insiders.yml
vendored
Normal file
322
.github/workflows/release-insiders.yml
vendored
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
name: Release Insiders
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
RELEASE_CHANNEL: insiders
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v6
|
||||
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@v5
|
||||
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@v5
|
||||
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@v6
|
||||
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@v6
|
||||
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@v6
|
||||
- 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@v6
|
||||
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@v6
|
||||
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@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v5
|
||||
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@v5
|
||||
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@v7
|
||||
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@v6
|
||||
with:
|
||||
name: npm-package-tarballs
|
||||
path: dist/*.tgz
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
|
||||
# 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@v8
|
||||
with:
|
||||
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'tailwindlabs',
|
||||
repo: 'upgrades',
|
||||
ref: 'main',
|
||||
workflow_id: 'upgrade-tailwindcss.yml'
|
||||
})
|
||||
268
.github/workflows/release.yml
vendored
268
.github/workflows/release.yml
vendored
|
|
@ -1,20 +1,9 @@
|
|||
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
|
||||
|
|
@ -22,13 +11,8 @@ permissions:
|
|||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '11.9.0'
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
|
|
@ -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@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v6
|
||||
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@v5
|
||||
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@v5
|
||||
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@v6
|
||||
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@v6
|
||||
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@v6
|
||||
- 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@v6
|
||||
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@v6
|
||||
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@v6
|
||||
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@v5
|
||||
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@v5
|
||||
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@v7
|
||||
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@v8
|
||||
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.'
|
||||
|
|
|
|||
173
CHANGELOG.md
173
CHANGELOG.md
|
|
@ -9,169 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### 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))
|
||||
- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
|
||||
|
||||
## [4.2.2] - 2026-03-18
|
||||
|
||||
|
|
@ -184,7 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- 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
|
||||
|
||||
|
|
@ -4123,13 +3960,7 @@ 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
|
||||
[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.2...HEAD
|
||||
[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
|
||||
|
|
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -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.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-android-arm64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-arm64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-x64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-freebsd-x64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-gnu",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-musl",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-gnu",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-musl",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
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.2.2",
|
||||
"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,11 +27,11 @@
|
|||
},
|
||||
"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",
|
||||
"@napi-rs/wasm-runtime": "^1.1.1",
|
||||
"@emnapi/core": "^1.8.1",
|
||||
"@emnapi/runtime": "^1.8.1",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
|
|
|
|||
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.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-win32-x64-msvc",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -33,11 +33,9 @@
|
|||
},
|
||||
"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.1.1",
|
||||
"emnapi": "1.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ impl BracketStack {
|
|||
b'(' => b')',
|
||||
b'[' => b']',
|
||||
b'{' => b'}',
|
||||
b'<' => b'>',
|
||||
_ => std::hint::unreachable_unchecked(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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' ';
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -34,47 +34,45 @@ 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().to_vec(), &lang.to_ascii_lowercase());
|
||||
|
||||
result.replace_range(body_start..body_end, replaced);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,15 +156,6 @@ impl PreProcessor for Ruby {
|
|||
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'}')
|
||||
{
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor.advance_twice();
|
||||
|
||||
// Boundary character
|
||||
|
|
@ -174,13 +163,8 @@ impl PreProcessor for Ruby {
|
|||
b'[' => b']',
|
||||
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;
|
||||
|
|
@ -197,8 +181,8 @@ impl PreProcessor for Ruby {
|
|||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter itself
|
||||
b'\\' if boundary != b'\\' => {
|
||||
// Skip escaped characters
|
||||
b'\\' => {
|
||||
// Use backslash to embed spaces in the strings.
|
||||
if cursor.next() == b' ' {
|
||||
result[cursor.pos] = b' ';
|
||||
|
|
@ -212,11 +196,6 @@ impl PreProcessor for Ruby {
|
|||
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());
|
||||
}
|
||||
|
||||
// End of a nested bracket
|
||||
b']' | b')' | b'}' if !bracket_stack.is_empty() => {
|
||||
if !bracket_stack.pop(cursor.curr()) {
|
||||
|
|
@ -225,14 +204,6 @@ impl PreProcessor for Ruby {
|
|||
}
|
||||
}
|
||||
|
||||
// End of a nested `<…>`
|
||||
b'>' if boundary == b'>' && !bracket_stack.is_empty() => {
|
||||
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' {
|
||||
|
|
@ -280,24 +251,6 @@ 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"),
|
||||
|
|
@ -375,20 +328,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]",
|
||||
|
|
@ -505,24 +444,4 @@ mod tests {
|
|||
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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,12 +104,6 @@ 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' ';
|
||||
}
|
||||
}
|
||||
|
||||
b']' if !bracket_stack.is_empty() => {
|
||||
|
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'}') =>
|
||||
&& 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.
|
||||
|
|
@ -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"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -74,8 +74,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 +101,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 +119,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!
|
||||
^^^ ^^^^ ^^^^^
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,16 +14,13 @@ pub struct Vue;
|
|||
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().to_vec(), lang);
|
||||
result = result.replace(body, replaced);
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -45,16 +42,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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ pub fn resolve_globs(
|
|||
}
|
||||
|
||||
if !dirs.contains(path) {
|
||||
it.skip_current_dir();
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,5 +2,3 @@ package-lock.json
|
|||
pnpm-lock.yaml
|
||||
bun.lockb
|
||||
.gitignore
|
||||
.env
|
||||
.env.*
|
||||
|
|
|
|||
|
|
@ -37,52 +37,20 @@ pub fn init_tracing() {
|
|||
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()
|
||||
let file_path = format!("tailwindcss-{}.log", std::process::id());
|
||||
let file = 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;
|
||||
}
|
||||
};
|
||||
.unwrap_or_else(|_| panic!("Failed to open {file_path}"));
|
||||
|
||||
let absolute_file_path = dunce::canonicalize(&file_path).unwrap_or_else(|_| file_path.clone());
|
||||
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.display().to_string())
|
||||
highlight(absolute_file_path.as_path().to_str().unwrap())
|
||||
);
|
||||
|
||||
let file = Arc::new(Mutex::new(file));
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ 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::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
|
|
@ -78,12 +79,11 @@ pub struct Scanner {
|
|||
/// Track unique set of candidates
|
||||
candidates: FxHashSet<String>,
|
||||
|
||||
/// Track mtimes for files so incremental scans can skip unchanged files.
|
||||
/// Track mtimes for files so re-scans can skip unchanged files.
|
||||
/// Only populated after the first scan completes (to avoid unnecessary
|
||||
/// metadata calls on initial build).
|
||||
mtimes: FxHashMap<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,
|
||||
|
|
@ -123,10 +123,9 @@ impl Scanner {
|
|||
pub fn scan(&mut self) -> Vec<String> {
|
||||
self.sources_scanned = false;
|
||||
|
||||
let (scanned_blobs, css_files, files) = self.discover_sources();
|
||||
let (scanned_blobs, css_files) = self.discover_sources();
|
||||
|
||||
self.extract_candidates(scanned_blobs, css_files);
|
||||
self.scanned_files = files;
|
||||
|
||||
// Return all candidates sorted
|
||||
let mut result = self.candidates.iter().cloned().collect::<Vec<_>>();
|
||||
|
|
@ -181,11 +180,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,15 +188,7 @@ 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 {
|
||||
if file == path {
|
||||
self.files.insert(path.to_path_buf()); // Track for future use
|
||||
content_to_scan.push(changed_file.clone()); // Track for parsing
|
||||
drop_file_indexes.push(idx);
|
||||
|
|
@ -271,11 +257,6 @@ 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 {
|
||||
|
|
@ -356,14 +337,15 @@ 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,
|
||||
|
|
@ -372,14 +354,14 @@ impl Scanner {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn discover_sources(&mut self) -> (Vec<Vec<u8>>, Vec<PathBuf>, Vec<String>) {
|
||||
fn discover_sources(&mut self) -> (Vec<Vec<u8>>, Vec<PathBuf>) {
|
||||
if self.sources_scanned {
|
||||
return (vec![], vec![], vec![]);
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
self.sources_scanned = true;
|
||||
|
||||
let Some(walker) = &mut self.walker else {
|
||||
return (vec![], vec![], vec![]);
|
||||
return (vec![], vec![]);
|
||||
};
|
||||
|
||||
// Use synchronous walk for the initial build (lower overhead) and parallel
|
||||
|
|
@ -391,109 +373,57 @@ impl Scanner {
|
|||
};
|
||||
|
||||
let mut css_files: Vec<PathBuf> = vec![];
|
||||
let mut content_paths: Vec<(PathBuf, String)> = vec![];
|
||||
let mut changed_files = vec![];
|
||||
let mut content_paths: Vec<(PathBuf, String)> = Vec::new();
|
||||
let mut seen_files: FxHashSet<PathBuf> = FxHashSet::default();
|
||||
|
||||
// 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);
|
||||
for (path, is_dir, extension) in all_entries {
|
||||
if is_dir {
|
||||
self.dirs.insert(path);
|
||||
} else {
|
||||
// Deduplicate: parallel walk can visit the same file from multiple threads
|
||||
if !seen_files.insert(path.clone()) {
|
||||
continue;
|
||||
}
|
||||
WalkEntry::File {
|
||||
path,
|
||||
mtime,
|
||||
is_symlink,
|
||||
} => {
|
||||
// Deduplicate: parallel walk can visit the same file from multiple threads
|
||||
if !self.files.insert(path.clone()) {
|
||||
continue;
|
||||
|
||||
// On re-scans, check mtime to skip unchanged files.
|
||||
// On the first scan we skip this entirely to avoid extra
|
||||
// metadata syscalls.
|
||||
let changed = if self.has_scanned_once {
|
||||
let current_mtime = std::fs::metadata(&path)
|
||||
.ok()
|
||||
.and_then(|m| m.modified().ok());
|
||||
|
||||
match current_mtime {
|
||||
Some(mtime) => {
|
||||
let prev = self.mtimes.insert(path.clone(), mtime);
|
||||
prev.is_none_or(|prev| prev != mtime)
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
// 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);
|
||||
match extension.as_str() {
|
||||
// Special handing for CSS files, we don't want to extract candidates from
|
||||
// these files, but we do want to extract used CSS variables.
|
||||
"css" => {
|
||||
if changed {
|
||||
css_files.push(path.clone());
|
||||
}
|
||||
}
|
||||
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,
|
||||
_ => {
|
||||
if changed {
|
||||
content_paths.push((path.clone(), extension.clone()));
|
||||
}
|
||||
} 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)),
|
||||
}
|
||||
}
|
||||
|
||||
self.extensions.insert(extension);
|
||||
self.files.insert(path);
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
|
@ -513,9 +443,7 @@ impl Scanner {
|
|||
self.has_scanned_once = true;
|
||||
}
|
||||
|
||||
changed_files.par_sort_unstable();
|
||||
|
||||
(scanned_blobs, css_files, changed_files)
|
||||
(scanned_blobs, css_files)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -546,15 +474,13 @@ pub fn pre_process_input(content: Vec<u8>, extension: &str) -> Vec<u8> {
|
|||
"heex" | "eex" | "ex" | "exs" => Elixir.process(&content),
|
||||
"cshtml" | "razor" => Razor.process(&content),
|
||||
"haml" => Haml.process(&content),
|
||||
"json" | "jsonl" | "ndjson" => Json.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),
|
||||
"tt" | "tt2" | "tx" => TemplateToolkit.process(&content),
|
||||
"twig" => Twig.process(&content),
|
||||
"vue" => Vue.process(&content),
|
||||
_ => content,
|
||||
}
|
||||
|
|
@ -616,50 +542,35 @@ where
|
|||
a
|
||||
})
|
||||
.into_iter()
|
||||
.filter_map(|s| String::from_utf8(s.to_vec()).ok())
|
||||
.map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type WalkEntry = (PathBuf, bool, String);
|
||||
|
||||
/// 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()
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for entry in walker.build().filter_map(Result::ok) {
|
||||
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
|
||||
let path = entry.into_path();
|
||||
|
||||
if is_dir {
|
||||
entries.push((path, true, String::new()));
|
||||
} else {
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
entries.push((path, false, ext));
|
||||
}
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
/// Walk the file system in parallel. Used in watch mode where the parallel walker overhead is
|
||||
|
|
@ -679,7 +590,7 @@ fn walk_parallel(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
|||
}
|
||||
}
|
||||
|
||||
let collected: Arc<Mutex<Vec<WalkEntry>>> = Arc::new(Mutex::new(vec![]));
|
||||
let collected: Arc<Mutex<Vec<WalkEntry>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
walker.build_parallel().run(|| {
|
||||
let mut buf = FlushOnDrop {
|
||||
|
|
@ -692,7 +603,19 @@ fn walk_parallel(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
|||
return ignore::WalkState::Continue;
|
||||
};
|
||||
|
||||
buf.local.push(WalkEntry::from(entry));
|
||||
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
|
||||
let path = entry.into_path();
|
||||
|
||||
if is_dir {
|
||||
buf.local.push((path, true, String::new()));
|
||||
} else {
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
buf.local.push((path, false, ext));
|
||||
}
|
||||
|
||||
if buf.local.len() >= 256 {
|
||||
buf.shared.lock().unwrap().append(&mut buf.local);
|
||||
|
|
@ -713,16 +636,7 @@ fn walk_parallel(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
|||
fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
||||
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 +648,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 +657,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 +695,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -857,7 +793,14 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
let pattern_sources: Vec<(PathBuf, String)> = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Pattern { base, pattern } => Some((base.into(), pattern.into())),
|
||||
SourceEntry::Pattern { base, pattern } => {
|
||||
let normalized = if pattern.starts_with("/") {
|
||||
pattern.to_string()
|
||||
} else {
|
||||
format!("/{pattern}")
|
||||
};
|
||||
Some((base.clone(), normalized))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
{
|
||||
|
|
|
|||
|
|
@ -375,120 +375,6 @@ describe.each([
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'watch mode with polling',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/index.css': css`@import 'tailwindcss/utilities';`,
|
||||
'src/index.html': html`
|
||||
<div class="underline"></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, spawn }) => {
|
||||
let process = await spawn(
|
||||
`${command} --input src/index.css --output dist/out.css --watch --poll=50`,
|
||||
)
|
||||
await process.onStderr((m) => m.includes('Done in'))
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`underline`])
|
||||
|
||||
await fs.write('src/index.html', html`
|
||||
<div class="underline flex"></div>
|
||||
`)
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
"watch mode with unknown @source paths shouldn't crash on Windows",
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'index.html': html`
|
||||
<div class="underline"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@source "unknown-folder/**/*";
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, spawn }) => {
|
||||
let process = await spawn(`${command} --input src/index.css --output dist/out.css --watch`)
|
||||
await process.onStderr((m) => m.includes('Done in'))
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`underline`])
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17632
|
||||
test(
|
||||
'watch mode rebuilds when the input file is in an ignored folder',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'assets/tailwind.config.js': js`
|
||||
module.exports = {
|
||||
content: {
|
||||
relative: true,
|
||||
files: ['html/*.html'],
|
||||
},
|
||||
}
|
||||
`,
|
||||
'assets/html/index.html': html`
|
||||
<div class="underline"></div>
|
||||
`,
|
||||
'assets/css/index.css': css`
|
||||
@import 'tailwindcss' source(none);
|
||||
@config '../tailwind.config.js';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, spawn }) => {
|
||||
let process = await spawn(
|
||||
`${command} --input assets/css/index.css --output dist/out.css --watch`,
|
||||
)
|
||||
await process.onStderr((m) => m.includes('Done in'))
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`underline`])
|
||||
|
||||
await fs.write(
|
||||
'assets/css/index.css',
|
||||
css`
|
||||
@import 'tailwindcss' source(none);
|
||||
@config '../tailwind.config.js';
|
||||
@source inline("flex");
|
||||
`,
|
||||
)
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'production build (stdin)',
|
||||
{
|
||||
|
|
@ -673,15 +559,6 @@ describe.each([
|
|||
'git ignore files outside of a repo are not considered',
|
||||
{
|
||||
fs: {
|
||||
// Make the test root a pnpm workspace that includes `home/project` so
|
||||
// the install below resolves the transitive dependency overrides that
|
||||
// the test harness injects into this `pnpm-workspace.yaml`.
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- home/project
|
||||
`,
|
||||
|
||||
// Ignore everything in the "home" directory
|
||||
'home/.gitignore': '*',
|
||||
|
||||
|
|
@ -719,7 +596,7 @@ describe.each([
|
|||
installDependencies: false,
|
||||
},
|
||||
async ({ fs, root, exec }) => {
|
||||
await exec(`pnpm install`, {
|
||||
await exec(`pnpm install --ignore-workspace`, {
|
||||
cwd: path.join(root, 'home/project'),
|
||||
})
|
||||
|
||||
|
|
@ -767,7 +644,7 @@ describe.each([
|
|||
}
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
'ssrc/index.html': html`
|
||||
<div class="flex"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
|
|
@ -828,7 +705,7 @@ describe.each([
|
|||
}
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
'ssrc/index.html': html`
|
||||
<div class="flex"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
|
|
@ -894,7 +771,7 @@ describe.each([
|
|||
}
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
'ssrc/index.html': html`
|
||||
<div class="flex"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
|
|
@ -955,7 +832,7 @@ describe.each([
|
|||
}
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
'ssrc/index.html': html`
|
||||
<div class="flex"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
|
|
@ -1188,7 +1065,7 @@ describe.each([
|
|||
}
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
'ssrc/index.html': html`
|
||||
<div class="flex"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
|
|
@ -1417,86 +1294,6 @@ describe.each([
|
|||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'watch mode should trigger a full rebuild when a dependency is removed',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
<div class="flex text-primary"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss/utilities';
|
||||
@config '../tailwind.config.js';
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
const myColor = require('./my-color')
|
||||
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: myColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
'my-color.js': js`
|
||||
//
|
||||
module.exports = 'blue'
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ spawn, fs, expect }) => {
|
||||
let process = await spawn(`${command} --input src/index.css --output dist/out.css --watch`)
|
||||
await process.onStderr((m) => m.includes('Done in'))
|
||||
|
||||
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/out.css ---
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.text-primary {
|
||||
color: blue;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
// Remove the dependency of the tailwind.config.js file
|
||||
await fs.delete('my-color.js')
|
||||
|
||||
// We expect an error
|
||||
await process.onStderr((m) => m.includes('Error'))
|
||||
await process.onStderr((m) => m.includes('Done in'))
|
||||
|
||||
// Re-create the file to resolve the issue
|
||||
await fs.write('my-color.js', js`module.exports = 'red'`)
|
||||
await process.onStderr((m) => m.includes('Done in'))
|
||||
|
||||
// Expect a full rebuild
|
||||
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/out.css ---
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.text-primary {
|
||||
color: red;
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test(
|
||||
|
|
@ -2061,7 +1858,7 @@ test(
|
|||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, root, expect }) => {
|
||||
async ({ fs, exec, spawn, root, expect }) => {
|
||||
await exec('pnpm tailwindcss --input ./index.css --output dist/out.css', { cwd: root })
|
||||
|
||||
let content = await fs.dumpFiles('./dist/*.css')
|
||||
|
|
@ -2072,335 +1869,6 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@source order is important (referencing sibling project)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
|
||||
@source '../../project-b';
|
||||
@source not '../../project-b/ignored';
|
||||
@source '../../project-b/ignored/except.html';
|
||||
`,
|
||||
|
||||
'project-b/keep/keep.html': html`<div class="content-['GOOD-1']"></div>`,
|
||||
'project-b/ignored/ignored.html': html`<div class="content-['BAD-1']"></div>`,
|
||||
'project-b/ignored/except.html': html`<div class="content-['GOOD-2']"></div>`,
|
||||
},
|
||||
},
|
||||
async ({ fs, root, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
|
||||
expect(await fs.dumpFiles('./project-a/dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./project-a/dist/out.css ---
|
||||
@layer properties;
|
||||
.content-\\[\\'GOOD-1\\'\\] {
|
||||
--tw-content: 'GOOD-1';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'GOOD-2\\'\\] {
|
||||
--tw-content: 'GOOD-2';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@source works with symlinks (referencing sibling project)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
|
||||
/* Same as the previous test, but using symlinks instead */
|
||||
@source '../../project-b';
|
||||
@source not '../../project-b/ignored';
|
||||
@source '../../project-b/ignored/except.html';
|
||||
`,
|
||||
|
||||
'project-b/keep/keep.html': html`<div class="content-['GOOD-1']"></div>`,
|
||||
'project-c/ignored/ignored.html': html`<div class="content-['BAD-1']"></div>`,
|
||||
'project-c/ignored/except.html': html`<div class="content-['GOOD-2']"></div>`,
|
||||
|
||||
// Symlink the ignored folder to another project
|
||||
'project-b/ignored': 'symlink:../../project-c/ignored/',
|
||||
},
|
||||
},
|
||||
async ({ fs, root, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
|
||||
expect(await fs.dumpFiles('./project-a/dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./project-a/dist/out.css ---
|
||||
@layer properties;
|
||||
.content-\\[\\'GOOD-1\\'\\] {
|
||||
--tw-content: 'GOOD-1';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'GOOD-2\\'\\] {
|
||||
--tw-content: 'GOOD-2';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19844
|
||||
test(
|
||||
'@source scans directories ignored by allow-list .gitignore files',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'.gitignore': txt`
|
||||
*
|
||||
!/app
|
||||
!/app/design
|
||||
!/app/design/**
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
@source '../vendor/acme/theme';
|
||||
`,
|
||||
// 1. Ignored by the `*` in `.gitignore`
|
||||
// 2. Included by the `!` pattern in `.gitignore`
|
||||
// 3. Ignored by `source(none)`
|
||||
//
|
||||
// → Should be ignored
|
||||
'app/design/frontend/theme/templates/component.phtml': html`
|
||||
<div
|
||||
class="content-['app/design/frontend/theme/templates/component.phtml']"
|
||||
></div>
|
||||
`,
|
||||
// 1. Ignored by the `*` in `.gitignore`
|
||||
// 2. Included by the `!` pattern in `.gitignore`
|
||||
// 3. Ignored by `source(none)`
|
||||
// 4. Included by the `@source` directive
|
||||
//
|
||||
// → Should be included
|
||||
'vendor/acme/theme/module/templates/component.phtml': html`
|
||||
<div
|
||||
class="content-['vendor/acme/theme/module/templates/component.phtml']"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
// 1. Ignored by the `*` in `.gitignore`
|
||||
// 2. Included by the `!` pattern in `.gitignore`
|
||||
// 3. Ignored by `source(none)`
|
||||
//
|
||||
// → Should be ignored
|
||||
await fs.expectFileNotToContain('dist/out.css', [
|
||||
candidate`content-['app/design/frontend/theme/templates/component.phtml']`,
|
||||
])
|
||||
|
||||
// 1. Ignored by the `*` in `.gitignore`
|
||||
// 2. Included by the `!` pattern in `.gitignore`
|
||||
// 3. Ignored by `source(none)`
|
||||
// 4. Included by the `@source` directive
|
||||
//
|
||||
// → Should be included
|
||||
await fs.expectFileToContain('dist/out.css', [
|
||||
candidate`content-['vendor/acme/theme/module/templates/component.phtml']`,
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/discussions/20382
|
||||
test(
|
||||
'auto source detection respects allow-list .gitignore files',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'.gitignore': txt`
|
||||
/*
|
||||
!/.gitignore
|
||||
!/app
|
||||
!/public
|
||||
!/package.json
|
||||
`,
|
||||
'app/root.css': css` @import 'tailwindcss/utilities'; `,
|
||||
// Included by the `!/app` pattern in `.gitignore`
|
||||
//
|
||||
// → Should be included
|
||||
'app/index.html': html`
|
||||
<div class="content-['app/index.html']"></div>
|
||||
`,
|
||||
// Included by the `!/public` pattern in `.gitignore`
|
||||
//
|
||||
// → Should be included
|
||||
'public/index.html': html`
|
||||
<div class="content-['public/index.html']"></div>
|
||||
`,
|
||||
// Ignored by the `/*` in `.gitignore`
|
||||
//
|
||||
// → Should be ignored
|
||||
'build/index.html': html`
|
||||
<div class="content-['build/index.html']"></div>
|
||||
`,
|
||||
// Ignored by the `/*` in `.gitignore` and the default `node_modules` rules
|
||||
//
|
||||
// → Should be ignored
|
||||
'node_modules/my-ui-lib/index.html': html`
|
||||
<div
|
||||
class="content-['node_modules/my-ui-lib/index.html']"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
await exec('pnpm tailwindcss --input app/root.css --output dist/out.css')
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [
|
||||
candidate`content-['app/index.html']`,
|
||||
candidate`content-['public/index.html']`,
|
||||
])
|
||||
|
||||
await fs.expectFileNotToContain('dist/out.css', [
|
||||
candidate`content-['build/index.html']`,
|
||||
candidate`content-['node_modules/my-ui-lib/index.html']`,
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@source works with symlinks (referencing folder in current folder)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
|
||||
/* Same as the previous test, but using symlinks instead */
|
||||
@source '../lib';
|
||||
@source not '../lib/ignored';
|
||||
@source '../lib/ignored/except.html';
|
||||
`,
|
||||
|
||||
'lib/keep/keep.html': html`<div class="content-['GOOD-1']"></div>`,
|
||||
'vendor/ignored/ignored.html': html`<div class="content-['BAD-1']"></div>`,
|
||||
'vendor/ignored/except.html': html`<div class="content-['GOOD-2']"></div>`,
|
||||
|
||||
// Symlink the ignored folder to another folder
|
||||
'lib/ignored': 'symlink:../../vendor/ignored/',
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./dist/out.css ---
|
||||
@layer properties;
|
||||
.content-\\[\\'GOOD-1\\'\\] {
|
||||
--tw-content: 'GOOD-1';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'GOOD-2\\'\\] {
|
||||
--tw-content: 'GOOD-2';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'auto source detection disabled',
|
||||
{
|
||||
|
|
@ -2749,49 +2217,6 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'suppress output by using the --silent flag',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'index.css': css` @import 'tailwindcss/utilities';`,
|
||||
'index.html': html`<div class="flex"></div>`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
let output = await exec('pnpm tailwindcss --input index.css --output dist/loud.css')
|
||||
expect(output).toContain('Done in')
|
||||
|
||||
let silentOutput = await exec(
|
||||
'pnpm tailwindcss --input index.css --output dist/silent.css --silent',
|
||||
)
|
||||
expect(silentOutput).not.toContain('Done in')
|
||||
expect(silentOutput).not.toContain('tailwindcss v')
|
||||
|
||||
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./dist/loud.css ---
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
--- ./dist/silent.css ---
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'changes to CSS files should pick up new CSS variables (if any)',
|
||||
{
|
||||
|
|
@ -2895,8 +2320,8 @@ test(
|
|||
@layer theme, base, components, utilities;
|
||||
@layer theme {
|
||||
:root, :host {
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial,
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
|
|
@ -2915,7 +2340,7 @@ test(
|
|||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
|
||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
|
||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
|
@ -2967,7 +2392,7 @@ test(
|
|||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
:-moz-focusring:where(:not(iframe)) {
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
progress {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
{
|
||||
|
|
@ -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, ts } 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',
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
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.
|
||||
//
|
||||
|
|
@ -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.1",
|
||||
"fast-glob": "^3.3.3",
|
||||
"source-map-js": "^1.2.1",
|
||||
"yaml": "^2.9.0"
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ describe.each(Object.keys(variantConfig))('%s', (variant) => {
|
|||
async ({ exec, expect }) => {
|
||||
await 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.`,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,57 +171,6 @@ 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`,
|
||||
{
|
||||
|
|
@ -263,7 +212,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 +284,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 +356,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 +433,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 +536,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -695,7 +644,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
await fs.expectFileToContain(
|
||||
'postcss.config.js',
|
||||
|
|
@ -766,7 +715,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 +762,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)
|
||||
|
|
@ -865,7 +814,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(
|
||||
|
|
@ -941,7 +890,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 +957,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 +1004,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 +1048,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 +1118,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 +1229,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 +1364,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 +1488,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 +1713,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 +1785,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 +1921,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 +2041,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 +2121,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 +2203,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 +2259,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 +2338,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(`
|
||||
|
|
@ -2490,7 +2436,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 +2503,7 @@ test(
|
|||
@import './utilities.css';
|
||||
@import './generated/ignore-me.css';
|
||||
`,
|
||||
'src/generated/.gitignore': txt`
|
||||
'src/generated/.gitignore': `
|
||||
*
|
||||
!.gitignore
|
||||
`,
|
||||
|
|
@ -2597,7 +2543,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 +2669,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 +2682,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 +2755,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 +2763,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,
|
||||
|
|
@ -2877,7 +2823,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, root, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
let before = await fs.dumpFiles('./src/**/*.{css,html}')
|
||||
expect(before).toMatchInlineSnapshot(`
|
||||
|
|
@ -2919,7 +2865,7 @@ test(
|
|||
}
|
||||
|
||||
// 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 +2944,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 +2972,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 +3015,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 +3089,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 +3121,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 +3165,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`,
|
||||
|
|
@ -164,7 +164,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,js,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -322,105 +322,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 +385,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 +487,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 +495,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 +573,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 +651,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 +725,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 +835,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 +933,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ root, exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade', {
|
||||
await exec('npx @tailwindcss/upgrade', {
|
||||
cwd: path.join(root, 'frontend'),
|
||||
})
|
||||
|
||||
|
|
@ -1119,7 +1020,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 +1095,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 +1171,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 +1247,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 +1309,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 +1371,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 +1410,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 +1481,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 +1584,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 +1694,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 +1796,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(`
|
||||
"
|
||||
|
|
@ -1993,7 +1894,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2064,7 +1965,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2141,7 +2042,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm exec upgrade')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -44,7 +39,6 @@ interface TestConfig {
|
|||
|
||||
timeout?: number
|
||||
installDependencies?: boolean
|
||||
retry?: number
|
||||
}
|
||||
interface TestContext {
|
||||
root: string
|
||||
|
|
@ -54,17 +48,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 +64,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 +82,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),
|
||||
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 +115,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 +134,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 +170,6 @@ export function test(
|
|||
if (debug) console.log(`>& ${command}`)
|
||||
let child = spawn(command, {
|
||||
cwd,
|
||||
detached: !IS_WINDOWS,
|
||||
shell: true,
|
||||
...childProcessOptions,
|
||||
env: {
|
||||
|
|
@ -228,26 +178,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 +284,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 +299,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 +309,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 +319,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 +329,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 +342,7 @@ export function test(
|
|||
}),
|
||||
)
|
||||
},
|
||||
async dumpFiles(pattern) {
|
||||
async dumpFiles(pattern: string) {
|
||||
let files = await context.fs.glob(pattern)
|
||||
return `\n${files
|
||||
.slice()
|
||||
|
|
@ -469,44 +401,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 +427,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 +441,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 +460,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 +483,28 @@ 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)
|
||||
json.pnpm.overrides['@tailwindcss/webpack>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 +554,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))
|
||||
}
|
||||
|
|
@ -723,65 +606,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,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([])
|
||||
},
|
||||
)
|
||||
|
|
@ -294,113 +294,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,11 +473,11 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
},
|
||||
)
|
||||
|
||||
describe.each([['^6'], ['7.0.8'], ['7.1.12'], ['7.3.1'], ['8.0.0']])(
|
||||
describe.sequential.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',
|
||||
'external source file changes trigger a full reload',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
|
|
@ -661,33 +554,26 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
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
|
||||
// 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
|
||||
// Changing an external .php file should trigger a full reload
|
||||
{
|
||||
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
|
||||
// Ensure the page reloaded
|
||||
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'))
|
||||
await process.onStderr(
|
||||
(m) => m.includes('vite:hmr (client)') && m.includes('index.php'),
|
||||
)
|
||||
}
|
||||
await process.onStderr((m) => m.includes('vite:hmr (ssr)') && m.includes('index.php'))
|
||||
|
||||
// Ensure the styles were regenerated with the new content
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
|
|
@ -698,234 +584,6 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
},
|
||||
)
|
||||
|
||||
// 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,7 +836,7 @@ 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)
|
||||
|
|
@ -1432,14 +1090,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 +1137,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 +1149,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 +1192,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,17 +1204,15 @@ 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;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
// 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;')
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -1627,100 +1275,3 @@ test(
|
|||
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)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,4 @@
|
|||
import {
|
||||
candidate,
|
||||
css,
|
||||
fetchStyles,
|
||||
getRandomPort,
|
||||
html,
|
||||
json,
|
||||
retryAssertion,
|
||||
test,
|
||||
ts,
|
||||
yaml,
|
||||
} from '../utils'
|
||||
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
|
||||
|
||||
const SETUP = {
|
||||
fs: {
|
||||
|
|
@ -22,14 +11,14 @@ const SETUP = {
|
|||
"nitropack": "2.11.0",
|
||||
"tailwindcss": "workspace:^",
|
||||
"vue": "latest"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"nuxi": "3.28.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
overrides:
|
||||
nuxi: 3.28.0
|
||||
`,
|
||||
'nuxt.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
|
|
@ -95,8 +84,7 @@ test('build', SETUP, async ({ spawn, exec, expect }) => {
|
|||
await exec('pnpm nuxt build')
|
||||
// The Nuxt preview server does not automatically assign a free port if 3000
|
||||
// is taken, so we use a random port instead.
|
||||
let port = await getRandomPort()
|
||||
let process = await spawn(`pnpm nuxt preview --port ${port}`, {
|
||||
let process = await spawn(`pnpm nuxt preview --port 8724`, {
|
||||
env: {
|
||||
TEST: 'false',
|
||||
NODE_ENV: 'development',
|
||||
|
|
|
|||
|
|
@ -1,165 +0,0 @@
|
|||
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts, txt } from '../utils'
|
||||
|
||||
test(
|
||||
'dev mode',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"preact": "^10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^",
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import preact from '@preact/preset-vite'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
preact(),
|
||||
{
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'src/main.tsx': ts`
|
||||
import { render } from 'preact'
|
||||
import { App } from './app'
|
||||
|
||||
render(<App />, document.getElementById('app')!)
|
||||
`,
|
||||
'src/app.tsx': ts`
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
export function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
return (
|
||||
<button className="underline" onClick={() => setCount((c) => c + 1)}>
|
||||
Count: {count}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
`,
|
||||
'src/index.css': css`@import 'tailwindcss';`,
|
||||
},
|
||||
},
|
||||
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 styles = await fetchStyles(url)
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
})
|
||||
|
||||
// Load the component modules, like a browser visiting the page would
|
||||
await fetch(`${url}/src/main.tsx`)
|
||||
await fetch(`${url}/src/app.tsx`)
|
||||
|
||||
// Editing a component keeps HMR intact: new classes are delivered through
|
||||
// a regular update, not a full page reload (which would lose all state)
|
||||
{
|
||||
await fs.write(
|
||||
'src/app.tsx',
|
||||
ts`
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
export function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
return (
|
||||
<button className="underline flex" onClick={() => setCount((c) => c + 1)}>
|
||||
Count: {count}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url)
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
expect(styles).toContain(candidate`flex`)
|
||||
})
|
||||
expect(await fs.read('hmr.log')).toContain('"type":"update"')
|
||||
expect(await fs.read('hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// Changing a scanned file that is not part of the module graph (e.g.
|
||||
// `package.json`, which package managers and other tooling write to while
|
||||
// the dev server is running) should not trigger a full reload either —
|
||||
// that would destroy client state. New candidates should still be picked
|
||||
// up because the file is a watch dependency of the CSS root, so the CSS
|
||||
// hot-updates through Vite's regular pipeline.
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20411
|
||||
{
|
||||
await fs.write(
|
||||
'package.json',
|
||||
txt`
|
||||
{
|
||||
"type": "module",
|
||||
"description": "content-['package.json']",
|
||||
"dependencies": {
|
||||
"preact": "^10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^",
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url)
|
||||
expect(styles).toContain(candidate`content-['package.json']`)
|
||||
})
|
||||
expect(await fs.read('hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -1,661 +1,5 @@
|
|||
import { describe } from 'vitest'
|
||||
import {
|
||||
candidate,
|
||||
css,
|
||||
fetchStyles,
|
||||
html,
|
||||
js,
|
||||
json,
|
||||
retryAssertion,
|
||||
test,
|
||||
ts,
|
||||
txt,
|
||||
yaml,
|
||||
} from '../utils'
|
||||
|
||||
test(
|
||||
'resolves tsconfig paths in production build',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tsconfig.json': json`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline custom-underline">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import '@/styles/base.css';
|
||||
@plugin '@/plugin.js';
|
||||
`,
|
||||
'src/styles/base.css': css`
|
||||
@reference 'tailwindcss/theme';
|
||||
@import 'tailwindcss/utilities';
|
||||
`,
|
||||
'src/plugin.js': js`
|
||||
export default function ({ addUtilities }) {
|
||||
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'resolves tsconfig paths in dev mode',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tsconfig.json': json`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline custom-underline">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import '@/styles/base.css';
|
||||
@plugin '@/plugin.js';
|
||||
`,
|
||||
'src/styles/base.css': css`
|
||||
@reference 'tailwindcss/theme';
|
||||
@import 'tailwindcss/utilities';
|
||||
`,
|
||||
'src/plugin.js': js`
|
||||
export default function ({ addUtilities }) {
|
||||
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ 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 styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
expect(styles).toContain(candidate`custom-underline`)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'resolves at-sign aliases in production build',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
alias: [{ find: '@', replacement: fileURLToPath(new URL('.', import.meta.url)) }],
|
||||
},
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline custom-underline">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import '@/src/styles/base.css';
|
||||
@plugin '@/src/plugin.js';
|
||||
`,
|
||||
'src/styles/base.css': css`
|
||||
@reference 'tailwindcss/theme';
|
||||
@import 'tailwindcss/utilities';
|
||||
`,
|
||||
'src/plugin.js': js`
|
||||
export default function ({ addUtilities }) {
|
||||
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'resolves package plugins in production build with at-sign aliases',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss-motion": "^1.1.1",
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
alias: [{ find: '@', replacement: fileURLToPath(new URL('.', import.meta.url)) }],
|
||||
},
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="scrollbar scrollbar-thumb-red-500 motion-preset-fade">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@plugin 'tailwind-scrollbar';
|
||||
@plugin 'tailwindcss-motion';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
await fs.expectFileToContain(filename, [
|
||||
candidate`scrollbar`,
|
||||
candidate`scrollbar-thumb-red-500`,
|
||||
candidate`motion-preset-fade`,
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'resolves package plugins to JS entries in production build when browser points to CSS',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"plugin-browser-css": "workspace:*",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- packages/*
|
||||
`,
|
||||
'packages/plugin-browser-css/package.json': json`
|
||||
{
|
||||
"name": "plugin-browser-css",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./index.js",
|
||||
"module": "./index.js",
|
||||
"browser": "./browser.css"
|
||||
}
|
||||
`,
|
||||
'packages/plugin-browser-css/index.js': js`
|
||||
export default function ({ addUtilities }) {
|
||||
addUtilities({ '.browser-css-plugin': { 'border-bottom': '1px solid green' } })
|
||||
}
|
||||
`,
|
||||
'packages/plugin-browser-css/browser.css': css`
|
||||
.should-not-be-loaded-as-a-plugin {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="browser-css-plugin">Hello, world!</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@plugin 'plugin-browser-css';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
await fs.expectFileToContain(filename, [candidate`browser-css-plugin`])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'resolves package plugins to JS entries in dev mode when browser points to CSS',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"plugin-browser-css": "workspace:*",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- packages/*
|
||||
`,
|
||||
'packages/plugin-browser-css/package.json': json`
|
||||
{
|
||||
"name": "plugin-browser-css",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./index.js",
|
||||
"module": "./index.js",
|
||||
"browser": "./browser.css"
|
||||
}
|
||||
`,
|
||||
'packages/plugin-browser-css/index.js': js`
|
||||
export default function ({ addUtilities }) {
|
||||
addUtilities({ '.browser-css-plugin': { 'border-bottom': '1px solid green' } })
|
||||
}
|
||||
`,
|
||||
'packages/plugin-browser-css/browser.css': css`
|
||||
.should-not-be-loaded-as-a-plugin {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="browser-css-plugin">Hello, world!</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@plugin 'plugin-browser-css';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ 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 styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`browser-css-plugin`)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'resolves package plugins in dev mode when package exports CSS files',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"daisyui": "^5",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<button class="btn btn-primary">Hello, world!</button>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@plugin 'daisyui';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ 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 styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain('.btn')
|
||||
expect(styles).toContain('.btn-primary')
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'resolve relative CSS files correctly',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@reference 'tailwindcss/theme';
|
||||
@import 'tailwindcss/utilities';
|
||||
@import './themes/glow.css';
|
||||
`,
|
||||
// References a file in the current folder, which names happens to match a
|
||||
// file in the parent folder as well.
|
||||
'src/themes/glow.css': css`@import './entry.css';`,
|
||||
'src/themes/entry.css': css`
|
||||
.do-include-me {
|
||||
color: green;
|
||||
}
|
||||
`,
|
||||
|
||||
// Never rerefenced, so should not be included
|
||||
'src/entry.css': css`
|
||||
.do-not-include-me {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
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 ---
|
||||
.do-include-me {
|
||||
color: green;
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'resolve relative JS files correctly',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@reference 'tailwindcss/theme';
|
||||
@import 'tailwindcss/utilities';
|
||||
@import './themes/glow.css';
|
||||
`,
|
||||
// References a file in the current folder, which names happens to match a
|
||||
// file in the parent folder as well.
|
||||
'src/themes/glow.css': css`@plugin "./my-plugin.js";`,
|
||||
'src/themes/my-plugin.js': ts`
|
||||
export default function ({ addBase }) {
|
||||
addBase({ '.do-include-me': { color: 'green' } })
|
||||
}
|
||||
`,
|
||||
|
||||
// Never rerefenced, so should not be included
|
||||
'src/my-plugin.js': css`
|
||||
export default function ({ addBase }) {
|
||||
addBase({ '.do-not-include-me': { 'color': 'red' } })
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
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 ---
|
||||
@layer base {
|
||||
.do-include-me {
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils'
|
||||
|
||||
describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
||||
test(
|
||||
|
|
|
|||
|
|
@ -91,67 +91,3 @@ test(
|
|||
})
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19930
|
||||
test(
|
||||
'production build without Tailwind roots should not result in source map warnings',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import { defineConfig } from 'vite'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
{
|
||||
name: 'inspect-source-map-chain',
|
||||
enforce: 'pre',
|
||||
transform(_, id) {
|
||||
if (id.includes('.css')) {
|
||||
// Force Rollup to collapse the sourcemap chain during this build.
|
||||
this.getCombinedSourcemap()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
css: {
|
||||
devSourcemap: true,
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<body>
|
||||
<script type="module" src="./src/index.js"></script>
|
||||
<div>Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'src/index.js': ts` import './index.css' `,
|
||||
'src/index.css': css`
|
||||
body {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
let output = await exec('pnpm vite build')
|
||||
|
||||
expect(output).not.toContain('Sourcemap is likely to be incorrect')
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import { candidate, css, html, js, json, test, ts } from '../utils'
|
||||
|
||||
test(
|
||||
'sveltekit build resolves package plugins',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prepare": "svelte-kit sync || echo ''"
|
||||
},
|
||||
"dependencies": {
|
||||
"svelte": "^5",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6",
|
||||
"@sveltejs/kit": "^2",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'svelte.config.js': js`
|
||||
import adapter from '@sveltejs/adapter-auto'
|
||||
|
||||
export default {
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
},
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
})
|
||||
`,
|
||||
'src/app.html': html`
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'src/routes/+layout.svelte': html`
|
||||
<script>
|
||||
import '../app.css'
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
`,
|
||||
'src/routes/+page.svelte': html`
|
||||
<div class="form-input">Hello, world!</div>
|
||||
`,
|
||||
'src/app.css': css`
|
||||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/forms';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
let files = await fs.glob('.svelte-kit/output/client/**/*.css')
|
||||
expect(files.length).toBeGreaterThan(0)
|
||||
|
||||
await fs.expectFileToContain(files[0][0], [candidate`form-input`])
|
||||
},
|
||||
)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { stripVTControlCharacters } from 'node:util'
|
||||
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
|
||||
import { candidate, html, json, test, ts } from '../utils'
|
||||
|
||||
test(
|
||||
'production build',
|
||||
|
|
@ -73,110 +73,6 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
{
|
||||
const VUE_COMPONENT_COUNT = 1_000
|
||||
|
||||
let vueComponentsWithReferences = Object.fromEntries(
|
||||
Array.from({ length: VUE_COMPONENT_COUNT }, (_, idx) => [
|
||||
`src/components/Component${idx}.vue`,
|
||||
html`
|
||||
<template>
|
||||
<div class="content-['component-${idx}']">Component ${idx}</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@reference '../main.css';
|
||||
|
||||
.component-${idx} {
|
||||
@apply text-red-500;
|
||||
}
|
||||
</style>
|
||||
`,
|
||||
]),
|
||||
)
|
||||
|
||||
let vueComponentImports = Array.from(
|
||||
{ length: VUE_COMPONENT_COUNT },
|
||||
(_, idx) => `import Component${idx} from './components/Component${idx}.vue'`,
|
||||
).join('\n')
|
||||
|
||||
let vueComponentUsages = Array.from(
|
||||
{ length: VUE_COMPONENT_COUNT },
|
||||
(_, idx) => `<Component${idx} class="component-${idx}" />`,
|
||||
).join('\n')
|
||||
|
||||
test(
|
||||
'production build with many Vue style blocks referencing the main stylesheet',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"vue": "^3.4.37",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"vite": "^7"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'src/main.css': css`@import 'tailwindcss';`,
|
||||
'src/main.ts': ts`
|
||||
import { createApp } from 'vue'
|
||||
import './main.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
`,
|
||||
'src/App.vue': html`
|
||||
<script setup>
|
||||
${vueComponentImports}
|
||||
</script>
|
||||
|
||||
<template>${vueComponentUsages}</template>
|
||||
`,
|
||||
...vueComponentsWithReferences,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
|
||||
await fs.expectFileToContain(files[0][0], [
|
||||
candidate`content-['component-0']`,
|
||||
candidate`component-0`,
|
||||
candidate`content-['component-99']`,
|
||||
candidate`component-99`,
|
||||
candidate`content-['component-999']`,
|
||||
candidate`component-999`,
|
||||
])
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
test(
|
||||
'error when using `@apply` without `@reference`',
|
||||
{
|
||||
|
|
@ -236,7 +132,7 @@ test(
|
|||
expect.assertions(1)
|
||||
|
||||
try {
|
||||
await exec('pnpm vite build', {}, { ignoreStdErr: true })
|
||||
await exec('pnpm vite build')
|
||||
} catch (error) {
|
||||
let [, message] =
|
||||
/error during build:([\s\S]*?)file:/g.exec(
|
||||
|
|
@ -248,133 +144,3 @@ test(
|
|||
}
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20320
|
||||
test(
|
||||
'editing a scanned `.vue` file that is not loaded as a module does not trigger a full reload',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"vue": "^3.4.37",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwindcss(),
|
||||
{
|
||||
// Log update and full-reload HMR payloads to a file so the
|
||||
// test can assert on them. Custom events are not logged
|
||||
// because \`@vitejs/plugin-vue\` sends a \`file-changed\` event
|
||||
// for every file change, including changes to the log file
|
||||
// itself, which would cause an infinite feedback loop.
|
||||
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) => {
|
||||
if (payload.type === 'update' || payload.type === 'full-reload') {
|
||||
fs.appendFileSync(logFile, JSON.stringify(payload) + '\\n')
|
||||
}
|
||||
return send(payload)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<!doctype 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>
|
||||
`,
|
||||
'src/index.css': css`@import 'tailwindcss';`,
|
||||
'src/main.ts': ts`
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
`,
|
||||
'src/App.vue': html`
|
||||
<template>
|
||||
<div class="content-['src/App.vue']">Hello Vue!</div>
|
||||
</template>
|
||||
`,
|
||||
|
||||
// This file is scanned by Tailwind but never imported, so it is not
|
||||
// part of the loaded module graph (e.g. a lazy route that hasn't been
|
||||
// visited yet)
|
||||
'src/LazyRoute.vue': html`
|
||||
<template>
|
||||
<div class="content-['src/LazyRoute.vue']">Lazy</div>
|
||||
</template>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ spawn, fs, 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 styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['src/App.vue']`)
|
||||
expect(styles).toContain(candidate`content-['src/LazyRoute.vue']`)
|
||||
})
|
||||
|
||||
// Load `main.ts` and `App.vue` as real modules, like a browser visiting
|
||||
// the page would
|
||||
await fetch(`${url}/src/main.ts`)
|
||||
await fetch(`${url}/src/App.vue`)
|
||||
|
||||
// Changing the scanned but unloaded `.vue` file should not trigger a
|
||||
// full reload, but new classes should still apply
|
||||
await fs.write(
|
||||
'src/LazyRoute.vue',
|
||||
html`
|
||||
<template>
|
||||
<div class="content-['updated:src/LazyRoute.vue']">Lazy</div>
|
||||
</template>
|
||||
`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/LazyRoute.vue']`)
|
||||
})
|
||||
expect(await fs.read('hmr.log')).not.toContain('full-reload')
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -418,78 +418,3 @@ test(
|
|||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@tailwindcss/webpack loader isolates cache by resource including query',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"main": "./src/index.js",
|
||||
"browser": "./src/index.js",
|
||||
"dependencies": {
|
||||
"css-loader": "^6",
|
||||
"webpack": "^5",
|
||||
"webpack-cli": "^5",
|
||||
"mini-css-extract-plugin": "^2",
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/webpack": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'webpack.config.js': js`
|
||||
let MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
let path = require('node:path')
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
a: './src/a.js',
|
||||
b: './src/b.js',
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
},
|
||||
plugins: [new MiniCssExtractPlugin()],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /.css$/i,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'@tailwindcss/webpack',
|
||||
path.resolve(__dirname, 'query-loader.js'),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`,
|
||||
'query-loader.js': js`
|
||||
module.exports = function (source) {
|
||||
if (this.resourceQuery.includes('a')) {
|
||||
return '@import "tailwindcss/utilities"; @utility only-a {color: var(--color-red-500);}'
|
||||
}
|
||||
|
||||
if (this.resourceQuery.includes('b')) {
|
||||
return '@import "tailwindcss/utilities"; @utility only-b {color: var(--color-blue-500);}'
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
`,
|
||||
'src/a.js': js`import './index.css?a'`,
|
||||
'src/b.js': js`import './index.css?b'`,
|
||||
'src/index.css': css``,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
await exec('pnpm webpack --mode=development')
|
||||
|
||||
await fs.expectFileToContain('dist/a.css', ['only-a', '--color-red-500'])
|
||||
await fs.expectFileToContain('dist/b.css', ['only-b', '--color-blue-500'])
|
||||
await fs.expectFileNotToContain('dist/a.css', ['only-b', '--color-blue-500'])
|
||||
await fs.expectFileNotToContain('dist/b.css', ['only-a', '--color-red-500'])
|
||||
},
|
||||
)
|
||||
|
|
|
|||
18
package.json
18
package.json
|
|
@ -48,17 +48,23 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@playwright/test": "^1.58.0",
|
||||
"@types/node": "catalog:",
|
||||
"postcss": "catalog:",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-import": "^16.1.1",
|
||||
"prettier": "catalog:",
|
||||
"prettier-plugin-embed": "^0.5.1",
|
||||
"prettier-plugin-organize-imports": "^4.3.0",
|
||||
"tsup": "^8.5.1",
|
||||
"turbo": "^2.10.8",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.10"
|
||||
"turbo": "^2.7.6",
|
||||
"typescript": "^5.5.4",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"packageManager": "pnpm@11.9.0"
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@parcel/watcher@2.5.1": "patches/@parcel__watcher@2.5.1.patch",
|
||||
"lightningcss@1.32.0": "patches/lightningcss@1.32.0.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/browser",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.global.js",
|
||||
|
|
@ -30,8 +30,8 @@
|
|||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"h3": "^1.15.11",
|
||||
"listhen": "^1.10.1",
|
||||
"h3": "^1.15.5",
|
||||
"listhen": "^1.9.0",
|
||||
"tailwindcss": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default defineConfig({
|
|||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
|
|
@ -30,11 +30,10 @@ export default defineConfig({
|
|||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
// Playwright 1.60's Windows WebKit worker hangs on shutdown after all tests
|
||||
// pass, causing CI to fail with a worker force-kill error.
|
||||
...(process.platform === 'win32'
|
||||
? []
|
||||
: [{ name: 'webkit', use: { ...devices['Desktop Safari'] } }]),
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,6 @@ test.beforeAll(async ({}, info) => {
|
|||
server = await createServer()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await server.close()
|
||||
})
|
||||
|
||||
test('basic', async ({ page }) => {
|
||||
await server.render({
|
||||
page,
|
||||
|
|
@ -170,7 +166,6 @@ async function createServer() {
|
|||
return {
|
||||
app,
|
||||
url: listener.url,
|
||||
close: listener.close,
|
||||
render,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/cli",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
@ -29,10 +29,10 @@
|
|||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@parcel/watcher": "catalog:",
|
||||
"@parcel/watcher": "^2.5.1",
|
||||
"@tailwindcss/node": "workspace:*",
|
||||
"@tailwindcss/oxide": "workspace:*",
|
||||
"enhanced-resolve": "catalog:",
|
||||
"enhanced-resolve": "^5.19.0",
|
||||
"mri": "^1.2.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"tailwindcss": "workspace:*"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import watcher from '@parcel/watcher'
|
||||
import {
|
||||
compile,
|
||||
env,
|
||||
|
|
@ -20,13 +21,11 @@ import {
|
|||
highlight,
|
||||
println,
|
||||
relative,
|
||||
wordWrap,
|
||||
} from '../../utils/renderer'
|
||||
import { drainStdin, outputFile } from './utils'
|
||||
|
||||
const css = String.raw
|
||||
const DEBUG = env.DEBUG
|
||||
const DEFAULT_POLL_INTERVAL_MS = 250
|
||||
|
||||
export function options() {
|
||||
return {
|
||||
|
|
@ -48,12 +47,6 @@ export function options() {
|
|||
alias: '-w',
|
||||
values: ['always'],
|
||||
},
|
||||
'--poll': {
|
||||
type: 'boolean | number',
|
||||
description: 'Use polling instead of filesystem events when watching',
|
||||
default: false,
|
||||
values: ['ms'],
|
||||
},
|
||||
'--minify': {
|
||||
type: 'boolean',
|
||||
description: 'Optimize and minify the output',
|
||||
|
|
@ -73,58 +66,23 @@ export function options() {
|
|||
description: 'Generate a source map',
|
||||
default: false,
|
||||
},
|
||||
'--silent': {
|
||||
type: 'boolean',
|
||||
description: 'Suppress non-error output',
|
||||
},
|
||||
} satisfies Arg
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
let seen = new Set() // Track seen errors to prevent circular errors
|
||||
function render(err: unknown, depth: number): string[] {
|
||||
let indent = ' '.repeat(depth)
|
||||
let width = (process.stderr.columns ?? Infinity) - 2 * (depth + 1)
|
||||
|
||||
let output = [
|
||||
`${indent}${red(depth === 0 ? 'Error:' : 'Caused by:')}`,
|
||||
`${indent}${dim('\u250C')}`,
|
||||
]
|
||||
|
||||
for (let line of `${err}`.split('\n')) {
|
||||
let wrapped = wordWrap(line, width)
|
||||
if (wrapped.length === 0) wrapped = ['']
|
||||
for (let chunk of wrapped) {
|
||||
output.push(`${indent}${dim('\u2502')} ${chunk}`)
|
||||
}
|
||||
}
|
||||
|
||||
output.push(`${indent}${dim('\u2514')}`)
|
||||
|
||||
if (typeof err === 'object' && err !== null && 'cause' in err && err.cause != null) {
|
||||
if (!seen.add(err).has(err.cause)) {
|
||||
output.push(...render(err.cause, depth + 1))
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
return render(error, 0).join('\n')
|
||||
}
|
||||
|
||||
async function handleError<T>(fn: () => T): Promise<T> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
eprintln(formatError(err))
|
||||
if (err instanceof Error) {
|
||||
eprintln(err.toString())
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
export async function handle(args: Result<ReturnType<typeof options>>) {
|
||||
if (!args['--silent']) eprintln(header())
|
||||
if (!args['--silent']) eprintln()
|
||||
eprintln(header())
|
||||
eprintln()
|
||||
|
||||
using I = new Instrumentation()
|
||||
DEBUG && I.start('[@tailwindcss/cli] (initial build)')
|
||||
|
|
@ -163,17 +121,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
// If the user passes `{bin} build --map -` then this likely means they want to output the map inline
|
||||
// this is the default behavior of `{bin build} --map` to inform the user of that
|
||||
if (args['--map'] === '-') {
|
||||
eprintln(`Use --map without a value to inline the source map.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (args['--poll'] === undefined) {
|
||||
eprintln(`Use --poll with a non-zero value in milliseconds.`)
|
||||
process.exit(1)
|
||||
}
|
||||
let pollInterval = args['--poll'] === true ? DEFAULT_POLL_INTERVAL_MS : args['--poll']
|
||||
if (pollInterval !== false && pollInterval <= 0) {
|
||||
eprintln(`Specified polling interval must be a positive number.`)
|
||||
eprintln(`Use --map without a value to inline the source map`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +144,6 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
let previous = {
|
||||
css: '',
|
||||
optimizedCss: '',
|
||||
output: null as string | null,
|
||||
}
|
||||
|
||||
async function write(
|
||||
|
|
@ -253,18 +200,13 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
}
|
||||
}
|
||||
|
||||
let didChange = output !== previous.output
|
||||
|
||||
DEBUG && I.start('Write output')
|
||||
if (args['--output'] && args['--output'] !== '-') {
|
||||
await outputFile(args['--output'], output)
|
||||
} else if (didChange) {
|
||||
} else {
|
||||
println(output)
|
||||
}
|
||||
DEBUG && I.end('Write output')
|
||||
|
||||
previous.output = output
|
||||
return didChange
|
||||
}
|
||||
|
||||
let inputFilePath =
|
||||
|
|
@ -273,7 +215,6 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
let inputBasePath = inputFilePath ? path.dirname(inputFilePath) : process.cwd()
|
||||
|
||||
let fullRebuildPaths: string[] = inputFilePath ? [inputFilePath] : []
|
||||
let backupRebuildPaths = fullRebuildPaths
|
||||
|
||||
async function createCompiler(css: string, I: Instrumentation) {
|
||||
DEBUG && I.start('Setup compiler')
|
||||
|
|
@ -300,24 +241,6 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
return [{ ...compiler.root, negated: false }]
|
||||
})().concat(compiler.sources)
|
||||
|
||||
// Do not scan the current executable. Otherwise when using the standalone
|
||||
// CLI, if the CLI lives in the current repo we would be scanning that file.
|
||||
//
|
||||
// This is also immune against renames of the executable file.
|
||||
sources.push({
|
||||
base: path.dirname(process.execPath),
|
||||
pattern: path.basename(process.execPath),
|
||||
negated: true,
|
||||
})
|
||||
|
||||
if (inputFilePath !== null) {
|
||||
sources.push({
|
||||
base: path.dirname(inputFilePath),
|
||||
pattern: path.basename(inputFilePath),
|
||||
negated: false,
|
||||
})
|
||||
}
|
||||
|
||||
let scanner = new Scanner({ sources })
|
||||
DEBUG && I.end('Setup compiler')
|
||||
|
||||
|
|
@ -325,16 +248,12 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
}
|
||||
|
||||
let [compiler, scanner] = await handleError(() => createCompiler(input, I))
|
||||
let cleanupWatchers: (() => Promise<void>)[] = []
|
||||
|
||||
// Watch for changes
|
||||
if (args['--watch'] && pollInterval === false) {
|
||||
// Ensure the file watcher can be loaded before setting up any watchers,
|
||||
// such that we can present a helpful error message if needed.
|
||||
await handleError(() => loadWatcher())
|
||||
|
||||
if (args['--watch']) {
|
||||
let cleanupWatchers: (() => Promise<void>)[] = []
|
||||
cleanupWatchers.push(
|
||||
await createWatchers(await watchDirectories(scanner), async function handle(files) {
|
||||
await createWatchers(watchDirectories(scanner), async function handle(files) {
|
||||
try {
|
||||
// If the only change happened to the output file, then we don't want to
|
||||
// trigger a rebuild because that will result in an infinite loop.
|
||||
|
|
@ -346,15 +265,36 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
// Re-compile the input
|
||||
let start = process.hrtime.bigint()
|
||||
|
||||
let changedFiles: ChangedContent[] = []
|
||||
let rebuildStrategy: 'incremental' | 'full' = 'incremental'
|
||||
|
||||
let resolvedFullRebuildPaths = fullRebuildPaths
|
||||
let rebuildStrategy = getRebuildStrategy(files, resolvedFullRebuildPaths)
|
||||
|
||||
for (let file of files) {
|
||||
// If one of the changed files is related to the input CSS or JS
|
||||
// config/plugin files, then we need to do a full rebuild because
|
||||
// the theme might have changed.
|
||||
if (resolvedFullRebuildPaths.includes(file)) {
|
||||
rebuildStrategy = 'full'
|
||||
|
||||
// No need to check the rest of the events, because we already know we
|
||||
// need to do a full rebuild.
|
||||
break
|
||||
}
|
||||
|
||||
// Track new and updated files for incremental rebuilds.
|
||||
changedFiles.push({
|
||||
file,
|
||||
extension: path.extname(file).slice(1),
|
||||
} satisfies ChangedContent)
|
||||
}
|
||||
|
||||
// Track the compiled CSS
|
||||
let compiledCss = ''
|
||||
let compiledMap: SourceMap | null = null
|
||||
|
||||
// Scan the entire `base` directory for full rebuilds.
|
||||
if (rebuildStrategy.kind === 'full') {
|
||||
if (rebuildStrategy === 'full') {
|
||||
// Read the new `input`.
|
||||
let input = args['--input']
|
||||
? args['--input'] === '-'
|
||||
|
|
@ -364,23 +304,11 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
@import 'tailwindcss';
|
||||
`
|
||||
clearRequireCache(resolvedFullRebuildPaths)
|
||||
|
||||
// Track current rebuild paths in case something goes wrong when
|
||||
// performing a full rebuild.
|
||||
backupRebuildPaths = fullRebuildPaths.slice()
|
||||
|
||||
// The `inputFilePath`, if provided, will be the only known full
|
||||
// rebuild path before the compiler is re-created.
|
||||
fullRebuildPaths = inputFilePath ? [inputFilePath] : []
|
||||
|
||||
// Create a new compiler, given the new `input`
|
||||
;[compiler, scanner] = await createCompiler(input, I)
|
||||
|
||||
// Successfully created a new compiler, so the `fullRebuildPaths`
|
||||
// will be updated. If other errors occur, we should be able to
|
||||
// restore the paths unconditionally.
|
||||
backupRebuildPaths = fullRebuildPaths.slice()
|
||||
|
||||
// Scan the directory for candidates
|
||||
DEBUG && I.start('Scan for candidates')
|
||||
let candidates = scanner.scan()
|
||||
|
|
@ -388,7 +316,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
|
||||
// Setup new watchers
|
||||
DEBUG && I.start('Setup new watchers')
|
||||
let newCleanupFunction = await createWatchers(await watchDirectories(scanner), handle)
|
||||
let newCleanupFunction = await createWatchers(watchDirectories(scanner), handle)
|
||||
DEBUG && I.end('Setup new watchers')
|
||||
|
||||
// Clear old watchers
|
||||
|
|
@ -411,16 +339,16 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
}
|
||||
|
||||
// Scan changed files only for incremental rebuilds.
|
||||
else if (rebuildStrategy.kind === 'incremental') {
|
||||
else if (rebuildStrategy === 'incremental') {
|
||||
DEBUG && I.start('Scan for candidates')
|
||||
let newCandidates = scanner.scanFiles(rebuildStrategy.changedFiles)
|
||||
let newCandidates = scanner.scanFiles(changedFiles)
|
||||
DEBUG && I.end('Scan for candidates')
|
||||
|
||||
// No new candidates found which means we don't need to write to
|
||||
// disk, and can return early.
|
||||
if (newCandidates.length <= 0) {
|
||||
let end = process.hrtime.bigint()
|
||||
if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -438,48 +366,13 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
await write(compiledCss, compiledMap, args, I)
|
||||
|
||||
let end = process.hrtime.bigint()
|
||||
if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
} catch (err) {
|
||||
// It's important that we perform a full rebuild when any of the
|
||||
// dependencies tracked in `fullRebuildPaths` has been changed.
|
||||
//
|
||||
// If we remove one of those files, then a subsequent build will be
|
||||
// triggered, but it will fail because the dependency is gone. The
|
||||
// compiler itself will be in a broken state and won't be able to
|
||||
// register any dependencies therefore we want to restore all the
|
||||
// dependencies from before. If we don't do that, then we won't be
|
||||
// able to recover from a bug in a transitive dependency.
|
||||
//
|
||||
// E.g.:
|
||||
// ```css
|
||||
// /* input.css — known full rebuild path */
|
||||
// @import 'tailwindcss';
|
||||
// @config "./tailwind.config.js";
|
||||
// ```
|
||||
//
|
||||
// ```js
|
||||
// // tailwind.config.js
|
||||
// const theme = require('./my-theme.js');
|
||||
//
|
||||
// module.exports = {
|
||||
// theme
|
||||
// }
|
||||
// ```
|
||||
// In this case `my-theme.js` is a transitive dependency of
|
||||
// `input.css` via `tailwind.config.js`. Removing `my-theme.js` will
|
||||
// result in an error, restoring `my-theme.js` should trigger a
|
||||
// fresh build even though the compiler didn't restore.
|
||||
//
|
||||
// Once the build error is fixed, a fresh full rebuild will happen
|
||||
// which in turn will fixup the full rebuild paths.
|
||||
fullRebuildPaths = backupRebuildPaths
|
||||
|
||||
// Catch any errors and print them to stderr, but don't exit the process
|
||||
// and keep watching.
|
||||
eprintln(formatError(err))
|
||||
|
||||
let end = process.hrtime.bigint()
|
||||
if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
if (err instanceof Error) {
|
||||
eprintln(err.toString())
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
|
@ -517,185 +410,10 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
await write(output, map, args, I)
|
||||
|
||||
let end = process.hrtime.bigint()
|
||||
if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
|
||||
// Watch for changes, using polling
|
||||
if (args['--watch'] && pollInterval !== false) {
|
||||
let cleanupPollingIndicator = () => {}
|
||||
|
||||
function logPollingMessage(message: string) {
|
||||
if (!args['--silent']) {
|
||||
process.stderr.write('\r\x1B[2K')
|
||||
}
|
||||
|
||||
eprintln(message)
|
||||
|
||||
if (!args['--silent']) {
|
||||
process.stderr.write(`\r\x1B[2K${dim(`Polling for changes…`)}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function fullRebuild(I: Instrumentation) {
|
||||
clearRequireCache(fullRebuildPaths)
|
||||
backupRebuildPaths = fullRebuildPaths.slice()
|
||||
fullRebuildPaths = inputFilePath ? [inputFilePath] : []
|
||||
|
||||
let input = args['--input']
|
||||
? args['--input'] === '-'
|
||||
? await drainStdin()
|
||||
: await fs.readFile(args['--input'], 'utf-8')
|
||||
: css`
|
||||
@import 'tailwindcss';
|
||||
`
|
||||
|
||||
;[compiler, scanner] = await createCompiler(input, I)
|
||||
backupRebuildPaths = fullRebuildPaths.slice()
|
||||
|
||||
DEBUG && I.start('Scan for candidates')
|
||||
let candidates = scanner.scan()
|
||||
DEBUG && I.end('Scan for candidates')
|
||||
|
||||
DEBUG && I.start('Build CSS')
|
||||
let output = compiler.build(candidates)
|
||||
DEBUG && I.end('Build CSS')
|
||||
|
||||
let map: SourceMap | null = null
|
||||
|
||||
if (args['--map']) {
|
||||
DEBUG && I.start('Build Source Map')
|
||||
map = toSourceMap(compiler.buildSourceMap())
|
||||
DEBUG && I.end('Build Source Map')
|
||||
}
|
||||
|
||||
await write(output, map, args, I)
|
||||
}
|
||||
|
||||
if (!args['--silent']) {
|
||||
let restored = false
|
||||
|
||||
function restoreCursor() {
|
||||
if (restored) return
|
||||
restored = true
|
||||
process.stderr.write('\r\x1B[2K\x1B[?25h')
|
||||
process.off('exit', restoreCursor)
|
||||
process.off('SIGINT', onSigint)
|
||||
process.off('SIGTERM', onSigterm)
|
||||
}
|
||||
|
||||
function onSigint() {
|
||||
restoreCursor()
|
||||
process.exit(130)
|
||||
}
|
||||
|
||||
function onSigterm() {
|
||||
restoreCursor()
|
||||
process.exit(143)
|
||||
}
|
||||
|
||||
process.stderr.write('\x1B[?25l')
|
||||
process.on('exit', restoreCursor)
|
||||
process.on('SIGINT', onSigint)
|
||||
process.on('SIGTERM', onSigterm)
|
||||
|
||||
cleanupPollingIndicator = restoreCursor
|
||||
cleanupWatchers.push(async () => cleanupPollingIndicator())
|
||||
}
|
||||
|
||||
cleanupWatchers.push(
|
||||
createPollingWatcher(async () => {
|
||||
using I = new Instrumentation()
|
||||
|
||||
DEBUG && I.start('Scan for candidates')
|
||||
let candidates = scanner.scan()
|
||||
DEBUG && I.end('Scan for candidates')
|
||||
|
||||
let files = scanner.scannedFiles.filter(
|
||||
(file) => file !== args['--output'] && file !== args['--map'],
|
||||
)
|
||||
|
||||
if (files.length <= 0) return
|
||||
|
||||
let start = process.hrtime.bigint()
|
||||
|
||||
let strategy = getRebuildStrategy(files, fullRebuildPaths)
|
||||
|
||||
if (strategy.kind === 'full') {
|
||||
try {
|
||||
await fullRebuild(I)
|
||||
} catch (err) {
|
||||
fullRebuildPaths = backupRebuildPaths
|
||||
|
||||
logPollingMessage(formatError(err))
|
||||
}
|
||||
|
||||
if (!args['--silent']) {
|
||||
logPollingMessage(`Done in ${formatDuration(process.hrtime.bigint() - start)}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// No candidates found which means we don't need to write to disk, and
|
||||
// can return early.
|
||||
if (candidates.length <= 0) {
|
||||
if (!args['--silent']) {
|
||||
logPollingMessage(`Done in ${formatDuration(process.hrtime.bigint() - start)}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DEBUG && I.start('Build CSS')
|
||||
let output = compiler.build(candidates)
|
||||
DEBUG && I.end('Build CSS')
|
||||
|
||||
let map: SourceMap | null = null
|
||||
|
||||
if (args['--map']) {
|
||||
DEBUG && I.start('Build Source Map')
|
||||
map = toSourceMap(compiler.buildSourceMap())
|
||||
DEBUG && I.end('Build Source Map')
|
||||
}
|
||||
|
||||
await write(output, map, args, I)
|
||||
|
||||
if (!args['--silent']) {
|
||||
logPollingMessage(`Done in ${formatDuration(process.hrtime.bigint() - start)}`)
|
||||
}
|
||||
}, pollInterval),
|
||||
)
|
||||
|
||||
if (args['--watch'] !== 'always') {
|
||||
process.stdin.on('end', () => {
|
||||
Promise.all(cleanupWatchers.map((fn) => fn())).then(
|
||||
() => process.exit(0),
|
||||
() => process.exit(1),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
process.stdin.resume()
|
||||
if (!args['--silent']) {
|
||||
process.stderr.write(`\r\x1B[2K${dim(`Polling for changes…`)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load `@parcel/watcher` lazily so a missing or broken native binding only
|
||||
// affects `--watch` (without `--poll`), instead of crashing one-off builds and
|
||||
// polling mode as well.
|
||||
async function loadWatcher(): Promise<typeof import('@parcel/watcher')> {
|
||||
try {
|
||||
return (await import('@parcel/watcher')).default
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to load the file watcher. Your platform may not be supported by \`@parcel/watcher\`. As a workaround, you can use polling instead by passing the \`--watch --poll\` flags.`,
|
||||
{ cause: err },
|
||||
)
|
||||
}
|
||||
eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
}
|
||||
|
||||
async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
|
||||
let watcher = await loadWatcher()
|
||||
|
||||
// Remove any directories that are children of an already watched directory.
|
||||
// If we don't we may not get notified of certain filesystem events regardless
|
||||
// of whether or not they are for the directory that is duplicated.
|
||||
|
|
@ -757,16 +475,10 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
|
|||
|
||||
await Promise.all(
|
||||
events.map(async (event) => {
|
||||
// When a file is deleted, a rebuild should be triggered such that we
|
||||
// can figure out whether this file must trigger a fresh build or not.
|
||||
//
|
||||
// If it must trigger a fresh build, then we will temporarily end up
|
||||
// in a broken state, but an error will be shown to the user. Once the
|
||||
// user resolves the issue, the CLI will recover.
|
||||
if (event.type === 'delete') {
|
||||
files.add(event.path)
|
||||
return
|
||||
}
|
||||
// We currently don't handle deleted files because it doesn't influence
|
||||
// the CSS output. This is because we currently keep all scanned
|
||||
// candidates in a cache for performance reasons.
|
||||
if (event.type === 'delete') return
|
||||
|
||||
// Ignore directory changes. We only care about file changes
|
||||
let stats: Stats | null = null
|
||||
|
|
@ -797,78 +509,6 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
|
|||
}
|
||||
}
|
||||
|
||||
function getRebuildStrategy(
|
||||
files: string[],
|
||||
fullRebuildPaths: string[],
|
||||
): { kind: 'incremental'; changedFiles: ChangedContent[] } | { kind: 'full' } {
|
||||
let changedFiles: ChangedContent[] = []
|
||||
|
||||
for (let file of files) {
|
||||
// If one of the changed files is related to the input CSS or JS
|
||||
// config/plugin files, then we need to do a full rebuild because
|
||||
// the theme might have changed.
|
||||
if (fullRebuildPaths.includes(file)) {
|
||||
return { kind: 'full' }
|
||||
}
|
||||
|
||||
// Track new and updated files for incremental rebuilds.
|
||||
changedFiles.push({
|
||||
file,
|
||||
extension: path.extname(file).slice(1),
|
||||
} satisfies ChangedContent)
|
||||
}
|
||||
|
||||
return { kind: 'incremental', changedFiles }
|
||||
}
|
||||
|
||||
function createPollingWatcher(cb: () => Promise<void>, pollInterval: number) {
|
||||
let disposed = false
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function poll() {
|
||||
if (disposed) return
|
||||
|
||||
try {
|
||||
await cb()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
if (!disposed) {
|
||||
timer = setTimeout(poll, pollInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timer = setTimeout(poll, pollInterval)
|
||||
|
||||
return async () => {
|
||||
disposed = true
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
async function watchDirectories(scanner: Scanner) {
|
||||
let directories = (
|
||||
await Promise.all(
|
||||
scanner.normalizedSources.map(async (globEntry) => {
|
||||
let resolvedPath = path.resolve(globEntry.base)
|
||||
let realPath = await fs.realpath(resolvedPath).catch(() => resolvedPath)
|
||||
|
||||
return fs
|
||||
.stat(realPath)
|
||||
.then((stat) => (stat.isDirectory() ? [realPath] : []))
|
||||
.catch(() => [])
|
||||
}),
|
||||
)
|
||||
).flat(1)
|
||||
|
||||
return Array.from(new Set(directories))
|
||||
}
|
||||
|
||||
function dim(str: string) {
|
||||
return `\x1B[2m${str}\x1B[22m`
|
||||
}
|
||||
|
||||
function red(str: string) {
|
||||
return `\x1B[31m${str}\x1B[39m`
|
||||
function watchDirectories(scanner: Scanner) {
|
||||
return [...new Set(scanner.normalizedSources.flatMap((globEntry) => globEntry.base))]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const sharedOptions = {
|
|||
} satisfies Arg
|
||||
|
||||
function buildUsage(command = 'tailwindcss') {
|
||||
return `${command} [--input input.css] [--output output.css] [--watch] [--poll=ms] [options…]`
|
||||
return `${command} [--input input.css] [--output output.css] [--watch] [options…]`
|
||||
}
|
||||
|
||||
function rootHelp({ invalid }: { invalid?: string } = {}) {
|
||||
|
|
|
|||
|
|
@ -153,34 +153,3 @@ it('should be possible to provide multiple types, and convert the value to that
|
|||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to provide boolean or number arguments', () => {
|
||||
let options = {
|
||||
'--poll': {
|
||||
type: 'boolean | number',
|
||||
description: 'Use polling instead of filesystem events when watching',
|
||||
default: false,
|
||||
},
|
||||
} satisfies Arg
|
||||
|
||||
expect(args(options, ['--poll'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--poll": true,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
|
||||
expect(args(options, ['--poll=50'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--poll": 50,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
|
||||
expect(args(options, ['--poll=foo'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--poll": undefined,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -49,9 +49,7 @@ export function wordWrap(text: string, width: number) {
|
|||
for (let word of words) {
|
||||
let wordLength = stripVTControlCharacters(word).length
|
||||
|
||||
// A word longer than `width` is kept on its own line rather than
|
||||
// introducing an empty line before it.
|
||||
if (lineLength > 0 && lineLength + wordLength + 1 > width) {
|
||||
if (lineLength + wordLength + 1 > width) {
|
||||
lines.push(line)
|
||||
line = ''
|
||||
lineLength = 0
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/node",
|
||||
"version": "4.3.3",
|
||||
"version": "4.2.2",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
@ -19,45 +19,29 @@
|
|||
],
|
||||
"publishConfig": {
|
||||
"provenance": true,
|
||||
"access": "public",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./require-cache": {
|
||||
"types": "./dist/require-cache.d.ts",
|
||||
"default": "./dist/require-cache.js"
|
||||
},
|
||||
"./esm-cache-loader": {
|
||||
"types": "./dist/esm-cache.loader.d.mts",
|
||||
"default": "./dist/esm-cache.loader.mjs"
|
||||
}
|
||||
}
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts",
|
||||
"require": "./src/index.cts"
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./require-cache": {
|
||||
"types": "./src/require-cache.ts",
|
||||
"import": "./src/require-cache.ts",
|
||||
"require": "./src/require-cache.cts"
|
||||
"types": "./dist/require-cache.d.ts",
|
||||
"default": "./dist/require-cache.js"
|
||||
},
|
||||
"./esm-cache-loader": {
|
||||
"types": "./src/esm-cache.loader.mts",
|
||||
"default": "./src/esm-cache.loader.mts"
|
||||
"types": "./dist/esm-cache.loader.d.mts",
|
||||
"default": "./dist/esm-cache.loader.mjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"enhanced-resolve": "catalog:",
|
||||
"jiti": "^2.7.0",
|
||||
"enhanced-resolve": "^5.19.0",
|
||||
"jiti": "^2.6.1",
|
||||
"lightningcss": "catalog:",
|
||||
"magic-string": "^1.1.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "workspace:*"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,10 +194,7 @@ async function importModule(path: string): Promise<any> {
|
|||
}
|
||||
}
|
||||
|
||||
const modules = [
|
||||
'node_modules',
|
||||
...(process.env.NODE_PATH ? [...process.env.NODE_PATH.split(path.delimiter)] : []),
|
||||
]
|
||||
const modules = ['node_modules', ...(process.env.NODE_PATH ? [process.env.NODE_PATH] : [])]
|
||||
|
||||
const cssResolver = EnhancedResolve.ResolverFactory.createResolver({
|
||||
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
|
||||
|
|
|
|||
|
|
@ -1,40 +1,22 @@
|
|||
import {
|
||||
isBuiltin,
|
||||
type ResolveFnOutput,
|
||||
type ResolveHook,
|
||||
type ResolveHookContext,
|
||||
type ResolveHookSync,
|
||||
} from 'node:module'
|
||||
import { isBuiltin, type ResolveHook } from 'node:module'
|
||||
|
||||
export let resolve: ResolveHook = async (specifier, context, nextResolve) => {
|
||||
let result = await nextResolve(specifier, context)
|
||||
return processResolve(context, result)
|
||||
}
|
||||
|
||||
export let resolveSync: ResolveHookSync = (specifier, context, nextResolve) => {
|
||||
let result = nextResolve(specifier, context)
|
||||
return processResolve(context, result)
|
||||
}
|
||||
if (result.url === import.meta.url) return result
|
||||
if (isBuiltin(result.url)) return result
|
||||
if (!context.parentURL) return result
|
||||
|
||||
function processResolve(context: ResolveHookContext, result: ResolveFnOutput) {
|
||||
try {
|
||||
if (result.url === import.meta.url) return result
|
||||
if (isBuiltin(result.url)) return result
|
||||
if (!context.parentURL) return result
|
||||
let parent = new URL(context.parentURL)
|
||||
|
||||
let parent = new URL(context.parentURL)
|
||||
let id = parent.searchParams.get('id')
|
||||
if (id === null) return result
|
||||
|
||||
let id = parent.searchParams.get('id')
|
||||
if (id === null) return result
|
||||
let url = new URL(result.url)
|
||||
url.searchParams.set('id', id)
|
||||
|
||||
let url = new URL(result.url)
|
||||
url.searchParams.set('id', id)
|
||||
|
||||
return {
|
||||
...result,
|
||||
url: `${url}`,
|
||||
}
|
||||
} catch {
|
||||
return result
|
||||
return {
|
||||
...result,
|
||||
url: `${url}`,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import * as Module from 'node:module'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import * as env from './env'
|
||||
import { resolveSync } from './esm-cache.loader.mjs'
|
||||
export * from './compile'
|
||||
export * from './instrumentation'
|
||||
export * from './normalize-path'
|
||||
|
|
@ -13,14 +12,8 @@ export { env }
|
|||
// not necessary.
|
||||
if (!process.versions.bun) {
|
||||
// `Module#register` was added in Node v18.19.0 and v20.6.0
|
||||
// `Module#registerHooks` was added in Node v22.15.0 and v23.5.0 and is the preferred API since v25.9.0,
|
||||
// runtime-deprecating `Module#register` since v26
|
||||
//
|
||||
// Not calling it means that while ESM dependencies don't get reloaded, the
|
||||
// actual included files will because they cache bust directly via `?id=…`
|
||||
if (Module.registerHooks) {
|
||||
Module.registerHooks({ resolve: resolveSync })
|
||||
} else {
|
||||
Module.register?.(pathToFileURL(require.resolve('@tailwindcss/node/esm-cache-loader')))
|
||||
}
|
||||
Module.register?.(pathToFileURL(require.resolve('@tailwindcss/node/esm-cache-loader')))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import * as Module from 'node:module'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import * as env from './env'
|
||||
import { resolveSync } from './esm-cache.loader.mjs'
|
||||
export * from './compile'
|
||||
export * from './instrumentation'
|
||||
export * from './normalize-path'
|
||||
|
|
@ -12,16 +11,11 @@ export { env }
|
|||
// In Bun, ESM modules will also populate `require.cache`, so the module hook is
|
||||
// not necessary.
|
||||
if (!process.versions.bun) {
|
||||
let localRequire = Module.createRequire(import.meta.url)
|
||||
|
||||
// `Module#register` was added in Node v18.19.0 and v20.6.0
|
||||
// `Module#registerHooks` was added in Node v22.15.0 and v23.5.0 and is the preferred API since v25.9.0,
|
||||
// runtime-deprecating `Module#register` since v26
|
||||
//
|
||||
// Not calling it means that while ESM dependencies don't get reloaded, the
|
||||
// actual included files will because they cache bust directly via `?id=…`
|
||||
if (Module.registerHooks) {
|
||||
Module.registerHooks({ resolve: resolveSync })
|
||||
} else {
|
||||
let localRequire = Module.createRequire(import.meta.url)
|
||||
Module.register?.(pathToFileURL(localRequire.resolve('@tailwindcss/node/esm-cache-loader')))
|
||||
}
|
||||
Module.register?.(pathToFileURL(localRequire.resolve('@tailwindcss/node/esm-cache-loader')))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { stripVTControlCharacters } from 'util'
|
|||
import { expect, it } from 'vitest'
|
||||
import { Instrumentation } from './instrumentation'
|
||||
|
||||
it('should add instrumentation using start/end markers', () => {
|
||||
it('should add instrumentation', () => {
|
||||
let I = new Instrumentation()
|
||||
|
||||
I.start('Foo')
|
||||
|
|
@ -35,83 +35,6 @@ it('should add instrumentation using start/end markers', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('should measure callbacks via the `span` api', () => {
|
||||
let I = new Instrumentation()
|
||||
|
||||
I.span('Foo', () => {
|
||||
let x = 1
|
||||
for (let i = 0; i < 100; i++) {
|
||||
I.span('Bar', () => {
|
||||
x **= 2
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
expect.assertions(1)
|
||||
|
||||
I.report((output) => {
|
||||
expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(`
|
||||
"
|
||||
[0.xxms] Foo
|
||||
[0.xxms] ↳ Bar × 100
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should measure async callbacks via the `span` api', async () => {
|
||||
let I = new Instrumentation()
|
||||
|
||||
await I.span('Foo', async () => {
|
||||
let x = 1
|
||||
for (let i = 0; i < 100; i++) {
|
||||
I.span('Bar', () => {
|
||||
x **= 2
|
||||
})
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500))
|
||||
})
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
I.report((output) => {
|
||||
expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(`
|
||||
"
|
||||
[0.xxms] Foo
|
||||
[0.xxms] ↳ Bar × 100
|
||||
"
|
||||
`)
|
||||
|
||||
let [, duration] = stripVTControlCharacters(output).match(/\[(.*)\]/)!
|
||||
expect(parseFloat(duration)).toBeGreaterThanOrEqual(500 - 2 /* Margin of error */)
|
||||
})
|
||||
})
|
||||
|
||||
it('should measure blocks until they go out of scope via `using`', () => {
|
||||
let I = new Instrumentation()
|
||||
|
||||
{
|
||||
using _ = I.track('Foo')
|
||||
|
||||
let x = 1
|
||||
for (let i = 0; i < 100; i++) {
|
||||
using _ = I.track('Bar')
|
||||
x **= 2
|
||||
}
|
||||
}
|
||||
|
||||
expect.assertions(1)
|
||||
|
||||
I.report((output) => {
|
||||
expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(`
|
||||
"
|
||||
[0.xxms] Foo
|
||||
[0.xxms] ↳ Bar × 100
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should auto end pending timers when reporting', () => {
|
||||
let I = new Instrumentation()
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export class Instrumentation implements Disposable {
|
|||
#timerStack: { id: string; label: string; namespace: string; value: bigint }[] = []
|
||||
|
||||
constructor(
|
||||
private shouldReport = env.DEBUG,
|
||||
private defaultFlush = (message: string) => void process.stderr.write(`${message}\n`),
|
||||
) {}
|
||||
|
||||
|
|
@ -49,41 +48,6 @@ export class Instrumentation implements Disposable {
|
|||
this.#timers.get(parent.id).value += elapsed
|
||||
}
|
||||
|
||||
track(label: string) {
|
||||
this.start(label)
|
||||
let done = false
|
||||
|
||||
return {
|
||||
[Symbol.dispose]: () => {
|
||||
if (!done) {
|
||||
this.end(label)
|
||||
done = true
|
||||
}
|
||||
},
|
||||
[Symbol.asyncDispose]: () => {
|
||||
if (!done) {
|
||||
this.end(label)
|
||||
done = true
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
span<T>(label: string, fn: () => T): T {
|
||||
this.start(label)
|
||||
let isPromise = false
|
||||
try {
|
||||
let result = fn()
|
||||
|
||||
isPromise = result && typeof (result as any).then === 'function'
|
||||
|
||||
// @ts-expect-error — TS can't infer that result is a Promise here
|
||||
return isPromise ? result.finally(() => this.end(label)) : result
|
||||
} finally {
|
||||
if (!isPromise) this.end(label)
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#hits.clear()
|
||||
this.#timers.clear()
|
||||
|
|
@ -136,7 +100,7 @@ export class Instrumentation implements Disposable {
|
|||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.shouldReport && this.report()
|
||||
env.DEBUG && this.report()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,14 +61,10 @@ export function optimize(
|
|||
map = result.map?.toString()
|
||||
|
||||
result.warnings = result.warnings.filter((warning) => {
|
||||
// Ignore warnings about unknown pseudo-classes that are used in frameworks
|
||||
// such as Vue or Angular and are handled by their own compilers, before
|
||||
// reaching the browser.
|
||||
if (
|
||||
/'(deep|slotted|global|ng-deep|host-context)' is not recognized as a valid pseudo-/.test(
|
||||
warning.message,
|
||||
)
|
||||
) {
|
||||
// Ignore warnings about unknown pseudo-classes as they are likely caused
|
||||
// by the use of `:deep()`, `:slotted()`, and `:global()` which are not
|
||||
// standard CSS but are commonly used in frameworks like Vue.
|
||||
if (/'(deep|slotted|global)' is not recognized as a valid pseudo-/.test(warning.message)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
export function clearRequireCache(files: string[]) {
|
||||
for (let key of files) {
|
||||
delete require.cache[key]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"verbatimModuleSyntax": false,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
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