diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 63f79fa10..42645aa49 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -50,7 +50,7 @@ If you open a pull request for a new feature, we're likely to close it not becau ## Coding standards -Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindlabs/tailwindcss/blob/main/package.json). You can check your code against these standards by running: +Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindcss/tailwindcss/blob/main/package.json). You can check your code against these standards by running: ```sh pnpm run lint @@ -76,7 +76,7 @@ To run the integration tests, use: pnpm build && pnpm test:integrations ``` -Additionally, some features require testing in browsers (i.e. to ensure CSS variable resolution works as expected). These can be run via: +Additionally, some features require testing in browsers (i.e to ensure CSS variable resolution works as expected). These can be run via: ```sh pnpm build && pnpm test:ui diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index aa0793aa3..ee9d3f9ab 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,7 +8,7 @@ It's never a fun experience to have your pull request declined after investing a For more info, check out the contributing guide: -https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md +https://github.com/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md --> diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ae59e80f..e369c64fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,18 +8,12 @@ on: permissions: contents: read -env: - NODE_VERSION: 24 - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: tests: strategy: fail-fast: false matrix: + node-version: [20] runner: - name: Windows os: windows-latest @@ -27,10 +21,8 @@ jobs: - name: Linux os: namespace-profile-default - # Playwright 1.62+ dropped WebKit support for macOS 14, and hangs - # instead of failing when launching WebKit on a macos-14 runner. - name: macOS - os: macos-15 + os: macos-14 # Exclude windows and macos from being built on feature branches run-all: @@ -49,21 +41,21 @@ jobs: name: ${{ matrix.runner.name }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} + node-version: ${{ matrix.node-version }} + cache: 'pnpm' # Cargo already skips downloading dependencies if they already exist - name: Cache cargo - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + uses: actions/cache@v4 with: path: | + ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ @@ -72,7 +64,7 @@ jobs: # Cache the `oxide` Rust build - name: Cache oxide build - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + uses: actions/cache@v4 with: path: | ./crates/node/*.node @@ -90,7 +82,7 @@ jobs: run: rustup target add wasm32-wasip1-threads - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install - name: Build run: pnpm run build @@ -112,13 +104,9 @@ jobs: - name: Run Playwright tests run: npm run test:ui - notify: - if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }} - needs: tests - runs-on: ubuntu-latest - steps: - name: Notify Discord - uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2 + if: failure() && github.ref == 'refs/heads/main' + uses: discord-actions/message@v2 with: webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }} - message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' + message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 457c38424..b84ba2140 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -8,18 +8,13 @@ on: permissions: contents: read -env: - NODE_VERSION: 24 - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: tests: strategy: fail-fast: false matrix: + node-version: [20] + runner: - name: Windows os: windows-latest @@ -55,25 +50,25 @@ jobs: name: ${{ matrix.runner.name }} / ${{ matrix.integration }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 - run: | git config --global user.name "github-actions[bot]" git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} + node-version: ${{ matrix.node-version }} + cache: 'pnpm' # Cargo already skips downloading dependencies if they already exist - name: Cache cargo - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + uses: actions/cache@v4 with: path: | + ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ @@ -82,7 +77,7 @@ jobs: # Cache the `oxide` Rust build - name: Cache oxide build - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + uses: actions/cache@v4 with: path: | ./crates/node/*.node @@ -108,18 +103,24 @@ jobs: CARGO_PROFILE_RELEASE_LTO: 'off' CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link' - - name: Test ${{ matrix.integration }} - run: pnpm run test:integrations ./integrations/${{ matrix.integration }} + - name: Test ${{ matrix.integration }} 1/3 + run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 1/3 + env: + GITHUB_WORKSPACE: ${{ github.workspace }} + + - name: Test ${{ matrix.integration }} 2/3 + run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 2/3 + env: + GITHUB_WORKSPACE: ${{ github.workspace }} + + - name: Test ${{ matrix.integration }} 3/3 + run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 3/3 env: GITHUB_WORKSPACE: ${{ github.workspace }} - notify: - if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }} - needs: tests - runs-on: ubuntu-latest - steps: - name: Notify Discord - uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2 + if: failure() && github.ref == 'refs/heads/main' + uses: discord-actions/message@v2 with: webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }} - message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' + message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 24e829afd..be1050b03 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -2,29 +2,18 @@ name: Prepare Release on: workflow_dispatch: - inputs: - dry_run: - description: Skip creating the draft GitHub release - required: false - default: true - type: boolean push: tags: - 'v*' env: APP_NAME: tailwindcss-oxide - NODE_VERSION: 24 - PNPM_VERSION: '11.9.0' + NODE_VERSION: 20 OXIDE_LOCATION: ./crates/node permissions: contents: read -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: build: strategy: @@ -54,40 +43,44 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu strip: strip - build-flags: --use-napi-cross + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian - os: ubuntu-latest target: aarch64-unknown-linux-gnu - strip: aarch64-linux-gnu-strip - build-flags: --use-napi-cross + strip: llvm-strip + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - os: ubuntu-latest target: armv7-unknown-linux-gnueabihf - strip: arm-linux-gnueabihf-strip - build-flags: --use-napi-cross + strip: llvm-strip + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig - os: ubuntu-latest target: aarch64-unknown-linux-musl - strip-zig: true - build-flags: -x + strip: aarch64-linux-musl-strip + download: true + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine - os: ubuntu-latest target: x86_64-unknown-linux-musl strip: strip - build-flags: -x + download: true + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine name: Build ${{ matrix.target }} (oxide) runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} timeout-minutes: 15 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: ${{ env.PNPM_VERSION }} + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - package-manager-cache: false + cache: 'pnpm' - name: Install gcc-arm-linux-gnueabihf if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }} @@ -95,56 +88,62 @@ jobs: sudo apt-get update sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y - - name: Install binutils-aarch64-linux-gnu - if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }} + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v4 + with: + path: | + ./crates/node/*.node + ./crates/node/*.wasm + ./crates/node/index.d.ts + ./crates/node/index.js + ./crates/node/browser.js + ./crates/node/tailwindcss-oxide.wasi-browser.js + ./crates/node/tailwindcss-oxide.wasi.cjs + ./crates/node/wasi-worker-browser.mjs + ./crates/node/wasi-worker.mjs + key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} + + - name: Install Node.JS + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install Rust (Stable) + if: ${{ matrix.download }} run: | - sudo apt-get update - sudo apt-get install binutils-aarch64-linux-gnu -y - - - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 - if: ${{ contains(matrix.target, 'musl') }} - with: - version: 0.14.1 - use-cache: false - - - name: Install cargo-zigbuild - uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2 - if: ${{ contains(matrix.target, 'musl') }} - env: - GITHUB_TOKEN: ${{ github.token }} - with: - tool: cargo-zigbuild + rustup default stable - name: Setup rust target run: rustup target add ${{ matrix.target }} - name: Install dependencies - run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* + run: pnpm install --ignore-scripts --filter=!./playgrounds/* - name: Build release - run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }} + run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} env: RUST_TARGET: ${{ matrix.target }} JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }} - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 - if: ${{ matrix.strip || matrix.strip-zig }} - env: - STRIP_COMMAND: ${{ matrix.strip }} - STRIP_ZIG: ${{ matrix.strip-zig }} - run: | - if [ "$STRIP_ZIG" = "true" ]; then - for file in ${{ env.OXIDE_LOCATION }}/*.node; do - zig objcopy --strip-all "$file" "$file.stripped" - mv "$file.stripped" "$file" - done - exit 0 - fi - - eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node" + if: ${{ matrix.strip }} + run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node - name: Upload artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: ${{ env.OXIDE_LOCATION }}/*.node @@ -154,11 +153,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false + - uses: actions/checkout@v4 - name: Build FreeBSD - uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0 + uses: cross-platform-actions/action@v0.25.0 env: DEBUG: napi:* RUSTUP_HOME: /usr/local/rustup @@ -174,12 +171,12 @@ jobs: shell: bash run: | sudo pkg install -y -f curl node libnghttp2 npm - sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true + sudo npm install -g pnpm@9.6.0 --unsafe-perm=true curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init chmod +x rustup-init ./rustup-init -y --profile minimal source "$HOME/.cargo/env" - pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true + pnpm install --ignore-scripts --filter=!./playgrounds/* || true echo "~~~~ rustc --version ~~~~" rustc --version echo "~~~~ node -v ~~~~" @@ -190,7 +187,7 @@ jobs: strip -x ${{ env.OXIDE_LOCATION }}/*.node ls -la ${{ env.OXIDE_LOCATION }} - name: Upload artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@v4 with: name: bindings-x86_64-unknown-freebsd path: ${{ env.OXIDE_LOCATION }}/*.node @@ -201,17 +198,18 @@ jobs: name: Build and release Tailwind CSS permissions: - contents: write # Required for creating releases + contents: write # for softprops/action-gh-release to create GitHub release + # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions + id-token: write needs: - build - build-freebsd steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@v4 with: fetch-depth: 20 - persist-credentials: false - run: git fetch --tags -f @@ -220,25 +218,51 @@ jobs: run: | echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: ${{ env.PNPM_VERSION }} + - uses: pnpm/action-setup@v4 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' registry-url: 'https://registry.npmjs.org' - package-manager-cache: false + + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v4 + with: + path: | + ./crates/node/*.node + ./crates/node/*.wasm + ./crates/node/index.d.ts + ./crates/node/index.js + ./crates/node/browser.js + ./crates/node/tailwindcss-oxide.wasi-browser.js + ./crates/node/tailwindcss-oxide.wasi.cjs + ./crates/node/wasi-worker-browser.mjs + ./crates/node/wasi-worker.mjs + key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} - name: Setup WASM target run: rustup target add wasm32-wasip1-threads - name: Install dependencies - run: pnpm --filter=!./playgrounds/* install --frozen-lockfile + run: pnpm --filter=!./playgrounds/* install - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@v4 with: path: ${{ env.OXIDE_LOCATION }} @@ -277,31 +301,30 @@ jobs: echo "EOF" >> $GITHUB_ENV - name: Upload standalone artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@v4 with: name: tailwindcss-standalone path: packages/@tailwindcss-standalone/dist/ - name: Upload npm package tarballs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@v4 with: name: npm-package-tarballs path: dist/*.tgz - name: Prepare GitHub Release - if: ${{ !inputs.dry_run }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create "${TAG_NAME}" \ - --draft \ - --title "${TAG_NAME}" \ - --notes "${RELEASE_NOTES}" \ - packages/@tailwindcss-standalone/dist/sha256sums.txt \ - packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64 \ - packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl \ - packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64 \ - packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl \ - packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64 \ - packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64 \ - packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe + uses: softprops/action-gh-release@v2 + with: + draft: true + tag_name: ${{ env.TAG_NAME }} + body: | + ${{ env.RELEASE_NOTES }} + files: | + packages/@tailwindcss-standalone/dist/sha256sums.txt + packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64 + packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl + packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64 + packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl + packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64 + packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64 + packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe diff --git a/.github/workflows/release-insiders.yml b/.github/workflows/release-insiders.yml new file mode 100644 index 000000000..53478a227 --- /dev/null +++ b/.github/workflows/release-insiders.yml @@ -0,0 +1,318 @@ +name: Release Insiders + +on: + push: + branches: [main] + +permissions: + contents: read + +env: + APP_NAME: tailwindcss-oxide + NODE_VERSION: 20 + OXIDE_LOCATION: ./crates/node + RELEASE_CHANNEL: insiders + +jobs: + build: + strategy: + matrix: + include: + # Windows + - os: windows-latest + target: x86_64-pc-windows-msvc + - os: windows-latest + target: aarch64-pc-windows-msvc + # macOS + - os: macos-latest + target: x86_64-apple-darwin + strip: strip -x # Must use -x on macOS. This produces larger results on linux. + - os: macos-latest + target: aarch64-apple-darwin + page-size: 14 + strip: strip -x # Must use -x on macOS. This produces larger results on linux. + # Android + - os: ubuntu-latest + target: aarch64-linux-android + strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip + - os: ubuntu-latest + target: armv7-linux-androideabi + strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip + # Linux + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + strip: strip + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + strip: llvm-strip + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + - os: ubuntu-latest + target: armv7-unknown-linux-gnueabihf + strip: llvm-strip + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig + - os: ubuntu-latest + target: aarch64-unknown-linux-musl + strip: aarch64-linux-musl-strip + download: true + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + strip: strip + download: true + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + + name: Build ${{ matrix.target }} (oxide) + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + + - name: Install gcc-arm-linux-gnueabihf + if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }} + run: | + sudo apt-get update + sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y + + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v4 + with: + path: | + ./crates/node/*.node + ./crates/node/*.wasm + ./crates/node/index.d.ts + ./crates/node/index.js + ./crates/node/browser.js + ./crates/node/tailwindcss-oxide.wasi-browser.js + ./crates/node/tailwindcss-oxide.wasi.cjs + ./crates/node/wasi-worker-browser.mjs + ./crates/node/wasi-worker.mjs + key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} + + - name: Install Node.JS + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install Rust (Stable) + if: ${{ matrix.download }} + run: | + rustup default stable + + - name: Setup rust target + run: rustup target add ${{ matrix.target }} + + - name: Install dependencies + run: pnpm install --ignore-scripts --filter=!./playgrounds/* + + - name: Build release + run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} + env: + RUST_TARGET: ${{ matrix.target }} + JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }} + + - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 + if: ${{ matrix.strip }} + run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: bindings-${{ matrix.target }} + path: ${{ env.OXIDE_LOCATION }}/*.node + + build-freebsd: + name: Build x86_64-unknown-freebsd (OXIDE) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - name: Build FreeBSD + uses: cross-platform-actions/action@v0.25.0 + env: + DEBUG: napi:* + RUSTUP_HOME: /usr/local/rustup + CARGO_HOME: /usr/local/cargo + RUSTUP_IO_THREADS: 1 + RUST_TARGET: x86_64-unknown-freebsd + with: + operating_system: freebsd + version: '14.0' + memory: 13G + cpu_count: 3 + environment_variables: 'DEBUG RUSTUP_IO_THREADS' + shell: bash + run: | + sudo pkg install -y -f curl node libnghttp2 npm + sudo npm install -g pnpm@9.6.0 --unsafe-perm=true + curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init + chmod +x rustup-init + ./rustup-init -y --profile minimal + source "$HOME/.cargo/env" + echo "~~~~ rustc --version ~~~~" + rustc --version + echo "~~~~ node -v ~~~~" + node -v + echo "~~~~ pnpm --version ~~~~" + pnpm --version + pnpm install --ignore-scripts --filter=!./playgrounds/* || true + pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform + strip -x ${{ env.OXIDE_LOCATION }}/*.node + ls -la ${{ env.OXIDE_LOCATION }} + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: bindings-x86_64-unknown-freebsd + path: ${{ env.OXIDE_LOCATION }}/*.node + + release: + runs-on: macos-14 + timeout-minutes: 15 + name: Build and release Tailwind CSS insiders + + permissions: + contents: write # for softprops/action-gh-release to create GitHub release + # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions + id-token: write + + needs: + - build + - build-freebsd + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 20 + + - name: Resolve version + id: vars + run: | + echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' + + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v4 + with: + path: | + ./crates/node/*.node + ./crates/node/*.wasm + ./crates/node/index.d.ts + ./crates/node/index.js + ./crates/node/browser.js + ./crates/node/tailwindcss-oxide.wasi-browser.js + ./crates/node/tailwindcss-oxide.wasi.cjs + ./crates/node/wasi-worker-browser.mjs + ./crates/node/wasi-worker.mjs + key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} + + - name: Setup WASM target + run: rustup target add wasm32-wasip1-threads + + - name: Install dependencies + run: pnpm --filter=!./playgrounds/* install + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: ${{ env.OXIDE_LOCATION }} + + - name: Move artifacts + run: | + cd ${{ env.OXIDE_LOCATION }} + cp bindings-x86_64-pc-windows-msvc/* ./npm/win32-x64-msvc/ + cp bindings-aarch64-pc-windows-msvc/* ./npm/win32-arm64-msvc/ + cp bindings-x86_64-apple-darwin/* ./npm/darwin-x64/ + cp bindings-aarch64-apple-darwin/* ./npm/darwin-arm64/ + cp bindings-aarch64-linux-android/* ./npm/android-arm64/ + cp bindings-armv7-linux-androideabi/* ./npm/android-arm-eabi/ + cp bindings-aarch64-unknown-linux-gnu/* ./npm/linux-arm64-gnu/ + cp bindings-aarch64-unknown-linux-musl/* ./npm/linux-arm64-musl/ + cp bindings-armv7-unknown-linux-gnueabihf/* ./npm/linux-arm-gnueabihf/ + cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/ + cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/ + cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/ + + - name: 'Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}' + run: pnpm run version-packages 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }} + + - name: Build Tailwind CSS + run: pnpm run build + + - name: Run pre-publish optimizations scripts + run: node ./scripts/pre-publish-optimizations.mjs + + - name: Lock pre-release versions + run: node ./scripts/lock-pre-release-versions.mjs + + - name: Upload npm package tarballs + uses: actions/upload-artifact@v4 + with: + name: npm-package-tarballs + path: dist/*.tgz + + - name: Publish + run: | + pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks + # The wasm package needs a special npm config that isn't read when pnpm --recursive is used + pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks; popd; + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Trigger Tailwind Play update + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'tailwindlabs', + repo: 'upgrades', + ref: 'main', + workflow_id: 'upgrade-tailwindcss.yml' + }) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c957d3af..8b27773b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,34 +1,18 @@ name: Release on: - push: - branches: [main] release: types: [published] workflow_dispatch: - inputs: - channel: - description: Release channel to publish - required: true - default: insiders - type: choice - options: - - insiders - - release permissions: contents: read env: APP_NAME: tailwindcss-oxide - NODE_VERSION: 24 - PNPM_VERSION: '11.9.0' + NODE_VERSION: 20 OXIDE_LOCATION: ./crates/node -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true - jobs: build: strategy: @@ -58,40 +42,44 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu strip: strip - build-flags: --use-napi-cross + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian - os: ubuntu-latest target: aarch64-unknown-linux-gnu - strip: aarch64-linux-gnu-strip - build-flags: --use-napi-cross + strip: llvm-strip + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - os: ubuntu-latest target: armv7-unknown-linux-gnueabihf - strip: arm-linux-gnueabihf-strip - build-flags: --use-napi-cross + strip: llvm-strip + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig - os: ubuntu-latest target: aarch64-unknown-linux-musl - strip-zig: true - build-flags: -x + strip: aarch64-linux-musl-strip + download: true + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine - os: ubuntu-latest target: x86_64-unknown-linux-musl strip: strip - build-flags: -x + download: true + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine name: Build ${{ matrix.target }} (oxide) runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} timeout-minutes: 15 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: ${{ env.PNPM_VERSION }} + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - package-manager-cache: false + cache: 'pnpm' - name: Install gcc-arm-linux-gnueabihf if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }} @@ -99,56 +87,62 @@ jobs: sudo apt-get update sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y - - name: Install binutils-aarch64-linux-gnu - if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }} + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v4 + with: + path: | + ./crates/node/*.node + ./crates/node/*.wasm + ./crates/node/index.d.ts + ./crates/node/index.js + ./crates/node/browser.js + ./crates/node/tailwindcss-oxide.wasi-browser.js + ./crates/node/tailwindcss-oxide.wasi.cjs + ./crates/node/wasi-worker-browser.mjs + ./crates/node/wasi-worker.mjs + key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} + + - name: Install Node.JS + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install Rust (Stable) + if: ${{ matrix.download }} run: | - sudo apt-get update - sudo apt-get install binutils-aarch64-linux-gnu -y - - - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 - if: ${{ contains(matrix.target, 'musl') }} - with: - version: 0.14.1 - use-cache: false - - - name: Install cargo-zigbuild - uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2 - if: ${{ contains(matrix.target, 'musl') }} - env: - GITHUB_TOKEN: ${{ github.token }} - with: - tool: cargo-zigbuild + rustup default stable - name: Setup rust target run: rustup target add ${{ matrix.target }} - name: Install dependencies - run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* + run: pnpm install --ignore-scripts --filter=!./playgrounds/* - name: Build release - run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }} + run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} env: RUST_TARGET: ${{ matrix.target }} JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }} - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 - if: ${{ matrix.strip || matrix.strip-zig }} - env: - STRIP_COMMAND: ${{ matrix.strip }} - STRIP_ZIG: ${{ matrix.strip-zig }} - run: | - if [ "$STRIP_ZIG" = "true" ]; then - for file in ${{ env.OXIDE_LOCATION }}/*.node; do - zig objcopy --strip-all "$file" "$file.stripped" - mv "$file.stripped" "$file" - done - exit 0 - fi - - eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node" + if: ${{ matrix.strip }} + run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node - name: Upload artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: ${{ env.OXIDE_LOCATION }}/*.node @@ -158,11 +152,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false + - uses: actions/checkout@v4 - name: Build FreeBSD - uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0 + uses: cross-platform-actions/action@v0.25.0 env: DEBUG: napi:* RUSTUP_HOME: /usr/local/rustup @@ -178,7 +170,7 @@ jobs: shell: bash run: | sudo pkg install -y -f curl node libnghttp2 npm - sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true + sudo npm install -g pnpm@9.6.0 --unsafe-perm=true curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init chmod +x rustup-init ./rustup-init -y --profile minimal @@ -189,12 +181,12 @@ jobs: node -v echo "~~~~ pnpm --version ~~~~" pnpm --version - pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true + pnpm install --ignore-scripts --filter=!./playgrounds/* || true pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform strip -x ${{ env.OXIDE_LOCATION }}/*.node ls -la ${{ env.OXIDE_LOCATION }} - name: Upload artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@v4 with: name: bindings-x86_64-unknown-freebsd path: ${{ env.OXIDE_LOCATION }}/*.node @@ -202,10 +194,10 @@ jobs: release: runs-on: macos-14 timeout-minutes: 15 - name: Build and publish Tailwind CSS + name: Build and release Tailwind CSS permissions: - contents: read + contents: write # for softprops/action-gh-release to create GitHub release # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions id-token: write @@ -214,51 +206,55 @@ jobs: - build-freebsd steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@v4 with: fetch-depth: 20 - persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: ${{ env.PNPM_VERSION }} + - uses: pnpm/action-setup@v4 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' registry-url: 'https://registry.npmjs.org' - package-manager-cache: false - # npm trusted publishing validates the caller workflow filename, so all npm publishes live here. - # This workflow rebuilds the publish artifacts instead of depending on prepare-release.yml. - - name: Resolve release metadata - env: - INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }} - run: | - if [[ "${{ github.event_name }}" == "release" || "$INPUT_CHANNEL" == "release" ]]; then - release_channel=$(node ./scripts/release-channel.js) + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} - echo "RELEASE_KIND=release" >> $GITHUB_ENV - echo "RELEASE_CHANNEL=$release_channel" >> $GITHUB_ENV - echo "FEATURES_ENV=stable" >> $GITHUB_ENV - else - sha_short=$(git rev-parse --short HEAD) - - echo "RELEASE_KIND=insiders" >> $GITHUB_ENV - echo "RELEASE_CHANNEL=insiders" >> $GITHUB_ENV - echo "SHA_SHORT=$sha_short" >> $GITHUB_ENV - echo "INSIDERS_VERSION=0.0.0-insiders.$sha_short" >> $GITHUB_ENV - fi + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v4 + with: + path: | + ./crates/node/*.node + ./crates/node/*.wasm + ./crates/node/index.d.ts + ./crates/node/index.js + ./crates/node/browser.js + ./crates/node/tailwindcss-oxide.wasi-browser.js + ./crates/node/tailwindcss-oxide.wasi.cjs + ./crates/node/wasi-worker-browser.mjs + ./crates/node/wasi-worker.mjs + key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} - name: Setup WASM target run: rustup target add wasm32-wasip1-threads - name: Install dependencies - run: pnpm --filter=!./playgrounds/* install --frozen-lockfile + run: pnpm --filter=!./playgrounds/* install - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@v4 with: path: ${{ env.OXIDE_LOCATION }} @@ -278,19 +274,10 @@ jobs: cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/ cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/ - - name: 'Version based on commit: ${{ env.INSIDERS_VERSION }}' - if: env.RELEASE_KIND == 'insiders' - run: pnpm run version-packages ${INSIDERS_VERSION} - - name: Build Tailwind CSS - if: env.RELEASE_KIND == 'insiders' - run: pnpm run build - - - name: Build Tailwind CSS - if: env.RELEASE_KIND == 'release' run: pnpm run build env: - FEATURES_ENV: ${{ env.FEATURES_ENV }} + FEATURES_ENV: stable - name: Run pre-publish optimizations scripts run: node ./scripts/pre-publish-optimizations.mjs @@ -298,20 +285,22 @@ jobs: - name: Lock pre-release versions run: node ./scripts/lock-pre-release-versions.mjs - - name: Upload npm package tarballs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: npm-package-tarballs - path: dist/*.tgz + - name: Calculate environment variables + run: | + echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV + echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./packages/tailwindcss/package.json`).version);')" >> $GITHUB_ENV - name: Publish run: | - pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${RELEASE_CHANNEL} --no-git-checks + pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks # The wasm package needs a special npm config that isn't read when pnpm --recursive is used - pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${RELEASE_CHANNEL} --no-git-checks --config.node-linker=hoisted; popd; + pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks; popd; + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Trigger Tailwind Play update - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + if: env.RELEASE_CHANNEL == 'latest' + uses: actions/github-script@v7 with: github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }} script: | @@ -321,38 +310,3 @@ jobs: ref: 'main', workflow_id: 'upgrade-tailwindcss.yml' }) - - notify: - if: ${{ always() && (needs.build.result == 'failure' || needs.build-freebsd.result == 'failure' || needs.release.result == 'failure') }} - needs: - - build - - build-freebsd - - release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - - name: Resolve release label - id: release - env: - INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }} - RELEASE_TAG: ${{ github.event.release.tag_name || '' }} - run: | - if [[ "${{ github.event_name }}" == "release" ]]; then - tag_name="${RELEASE_TAG:-${GITHUB_REF_NAME}}" - echo "label=release ${tag_name}" >> $GITHUB_OUTPUT - elif [[ "$INPUT_CHANNEL" == "release" ]]; then - version=$(node -p "require('./packages/tailwindcss/package.json').version") - echo "label=release v${version}" >> $GITHUB_OUTPUT - else - sha_short=$(git rev-parse --short HEAD) - echo "label=insiders release 0.0.0-insiders.${sha_short}" >> $GITHUB_OUTPUT - fi - - - name: Notify Discord - uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2 - with: - webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }} - message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) for `${{ steps.release.outputs.label }}` has failed.' diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..ded82e2f6 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers = true diff --git a/.prettierignore b/.prettierignore index 4f50b932f..3de4530dd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,5 @@ pnpm-lock.yaml target/ crates/node/index.d.ts crates/node/index.js -crates/ignore/ .next .fingerprint diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e6f6f89..6f0181e87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,262 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added - -- Add `@tailwindcss/turbopack` package to run Tailwind CSS with Next.js ([20367](https://github.com/tailwindlabs/tailwindcss/pull/20367)) - -### Fixed - -- Ensure watch mode detects changes to symlinked `@source` files whose real paths aren't otherwise scanned ([#20356](https://github.com/tailwindlabs/tailwindcss/pull/20356)) -- Ensure custom variants using `@scope` wrap the generated utilities instead of nesting inside them ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369)) -- Fix flattening of `@scope` at-rules ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369)) -- Fix standalone declarations in `@scope`, wrap them in `:where(:scope)` ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369)) -- Always emit a space for empty fallback values in CSS variables (e.g. `var(--tw-blur,)` → `var(--tw-blur, )`) ([#20373](https://github.com/tailwindlabs/tailwindcss/pull/20373)) -- Canonicalization: convert arbitrary breakpoint and container query variants to named equivalents (e.g. `max-[64rem]` → `max-lg`) ([#20380](https://github.com/tailwindlabs/tailwindcss/pull/20380)) -- Prevent `@tailwindcss/vite` from crashing on every edit under Vite's experimental `bundledDev` mode ([#20379](https://github.com/tailwindlabs/tailwindcss/pull/20379)) -- Ensure `@tailwindcss/oxide` falls back to WASM on platforms without native bindings ([#20383](https://github.com/tailwindlabs/tailwindcss/pull/20383)) -- Detect classes in Ruby percent literals using angle brackets or custom delimiters (e.g. `%w`, `%w|flex|`), including in Slim and Haml templates ([#20387](https://github.com/tailwindlabs/tailwindcss/pull/20387)) -- Preserve whitespace in `--default(…)` values in custom functional utilities (e.g. `--default(box alphabetic)` no longer becomes `boxalphabetic`) ([#20392](https://github.com/tailwindlabs/tailwindcss/pull/20392)) -- Don't scan gitignored directories (e.g. `node_modules` and `.git`) when the project uses a safelist-style `.gitignore` (e.g. `/*` followed by `!/…` negations) ([#20397](https://github.com/tailwindlabs/tailwindcss/pull/20397)) -- Ensure root `theme('…')` namespace lookups in JavaScript plugins and config files return the full namespace object instead of the value of its `DEFAULT` key ([#20399](https://github.com/tailwindlabs/tailwindcss/pull/20399)) -- Skip ignored directories entirely when computing watch globs (`scanner.globs`), instead of walking their full contents on every rebuild ([#20408](https://github.com/tailwindlabs/tailwindcss/pull/20408)) -- Oxide: drop invalid UTF-8 candidates ([#20389](https://github.com/tailwindlabs/tailwindcss/pull/20389)) -- `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414)) -- Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417)) -- Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419)) -- Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420)) -- Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434)) -- Don't generate CSS for candidates containing an empty additional modifier (e.g. `bg-red-500/50/` and `group-hover/foo//bar:flex`) ([#20466](https://github.com/tailwindlabs/tailwindcss/pull/20466)) -- Sort `min-*`, `max-*`, and container query variants with decimal values numerically (e.g. `min-[40.25rem]` before `min-[40.5rem]`) ([#20512](https://github.com/tailwindlabs/tailwindcss/pull/20512)) -- Ensure CSS comments ending with `\*/` are closed correctly instead of swallowing the CSS that follows (e.g. `/* C:\temp\*/`) ([#20508](https://github.com/tailwindlabs/tailwindcss/pull/20508)) -- Improve style invalidation performance of `group-*` and `peer-*` variants ([#20513](https://github.com/tailwindlabs/tailwindcss/pull/20513)) - -## [4.3.3] - 2026-07-16 - -### Fixed - -- Support `--watch --poll[=ms]` in `@tailwindcss/cli` when filesystem events are unreliable or unavailable ([#20297](https://github.com/tailwindlabs/tailwindcss/pull/20297)) -- Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298)) -- Prevent Preflight from overriding Firefox's native `iframe:focus-visible` outline styles ([#20292](https://github.com/tailwindlabs/tailwindcss/pull/20292)) -- Ensure `theme('colors.foo')` in JS plugins resolves correctly when both `--color-foo` and `--color-foo-bar` exist ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299)) -- Ensure fractional opacity modifiers work with named shadow sizes like `shadow-sm/12.5`, `text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and `inset-shadow-sm/12.5` ([#20302](https://github.com/tailwindlabs/tailwindcss/pull/20302)) -- Parse selectors like `[data-foo]div` as two selectors instead of one ([#20303](https://github.com/tailwindlabs/tailwindcss/pull/20303)) -- Ensure `@tailwindcss/postcss` rebuilds when a preprocessor like Sass changes the input CSS without changing the input file on disk ([#20310](https://github.com/tailwindlabs/tailwindcss/pull/20310)) -- Ensure CSS nesting is handled even when Lightning CSS isn't run, such as in `@tailwindcss/browser` and Tailwind Play ([#20124](https://github.com/tailwindlabs/tailwindcss/pull/20124)) -- Prevent achromatic theme colors from shifting hue when mixed in polar color spaces like `oklch` ([#20314](https://github.com/tailwindlabs/tailwindcss/pull/20314)) -- Ensure `--spacing(0)` is optimized to `0px` instead of `0` so it remains a `` when used in `calc(…)` ([#20319](https://github.com/tailwindlabs/tailwindcss/pull/20319)) -- Load `@parcel/watcher` only when needed in `@tailwindcss/cli --watch` mode, so one-off builds and `--watch --poll` work when `@parcel/watcher` can't be loaded ([#20325](https://github.com/tailwindlabs/tailwindcss/pull/20325)) -- Use explicit platform fonts instead of `system-ui` and `ui-sans-serif` so CJK text respects the page's `lang` attribute on Windows ([#20318](https://github.com/tailwindlabs/tailwindcss/pull/20318)) -- Prevent `@tailwindcss/upgrade` from rewriting ignored files when run from a subdirectory ([#20329](https://github.com/tailwindlabs/tailwindcss/pull/20329)) -- Ensure earlier `@source` rules pointing to nested files are scanned when later `@source` rules point to files in parent folders ([#20335](https://github.com/tailwindlabs/tailwindcss/pull/20335)) -- Prevent `@tailwindcss/vite` from triggering full page reloads when scanned files are processed by Vite but haven't been loaded as modules yet ([#20336](https://github.com/tailwindlabs/tailwindcss/pull/20336)) - -## [4.3.2] - 2026-06-26 - -### Fixed - -- Support bare spacing values for `auto-rows-*` and `auto-cols-*` utilities (e.g. `auto-rows-12` and `auto-cols-16`) ([#20229](https://github.com/tailwindlabs/tailwindcss/pull/20229)) -- Prevent `@tailwindcss/cli` in `--watch` mode from crashing on Windows when `@source` points to a directory that doesn't exist ([#20242](https://github.com/tailwindlabs/tailwindcss/pull/20242)) -- Prevent `@tailwindcss/vite` from crashing in Deno v2.8.x when `context.parentURL` is not a valid URL ([#20245](https://github.com/tailwindlabs/tailwindcss/pull/20245)) -- Ensure `@tailwindcss/cli` in `--watch` mode rebuilds when the input CSS file changes in an ignored directory ([#20246](https://github.com/tailwindlabs/tailwindcss/pull/20246)) -- Allow `@variant` rules used in `addBase(…)` to use custom variants defined later ([#20247](https://github.com/tailwindlabs/tailwindcss/pull/20247)) -- Prevent `@tailwindcss/vite` from crashing during HMR when scanned files or directories are deleted ([#20259](https://github.com/tailwindlabs/tailwindcss/pull/20259)) -- Generate `font-size` instead of `color` declarations for `text-[--spacing(…)]` ([#20260](https://github.com/tailwindlabs/tailwindcss/pull/20260)) -- Prevent `@source` patterns from scanning unrelated sibling files and folders ([#20263](https://github.com/tailwindlabs/tailwindcss/pull/20263)) -- Extract class candidates adjacent to Template Toolkit delimiters like `%]…[%` in `.tt`, `.tt2`, and `.tx` files ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269)) -- Extract class candidates from conditional Maud syntax like `p.text-black[condition]` ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269)) -- Prevent `@position-try` rules from triggering unknown at-rule warnings when optimizing CSS ([#20277](https://github.com/tailwindlabs/tailwindcss/pull/20277)) -- Support class suggestions for named opacity modifiers from `--opacity` theme values ([#20287](https://github.com/tailwindlabs/tailwindcss/pull/20287)) -- Prevent type errors in `@tailwindcss/postcss` when used with newer PostCSS patch releases ([#20289](https://github.com/tailwindlabs/tailwindcss/pull/20289)) - -## [4.3.1] - 2026-06-12 - -### Added - -- Add `--silent` option to suppress output in `@tailwindcss/cli` ([#20100](https://github.com/tailwindlabs/tailwindcss/pull/20100)) - -### Fixed - -- Remove deprecation warnings by using `Module#registerHooks` instead of `Module#register` on Node 26+ ([#20028](https://github.com/tailwindlabs/tailwindcss/pull/20028)) -- Canonicalization: don't crash when plugin utilities throw for unsupported values ([#20052](https://github.com/tailwindlabs/tailwindcss/pull/20052)) -- Allow `@apply` to be used with CSS mixins ([#19427](https://github.com/tailwindlabs/tailwindcss/pull/19427)) -- Ensure `not-*` correctly negates `@container` queries, including `style(…)` queries ([#20059](https://github.com/tailwindlabs/tailwindcss/pull/20059)) -- Ensure `drop-shadow-*` color utilities work with custom shadow values containing `calc(…)` ([#20080](https://github.com/tailwindlabs/tailwindcss/pull/20080)) -- Fix 'Sourcemap is likely to be incorrect' warnings when using `@tailwindcss/vite` ([#20103](https://github.com/tailwindlabs/tailwindcss/pull/20103)) -- Ensure `@tailwindcss/webpack` can be installed in Rspack projects without requiring `webpack` as a peer dependency ([#20027](https://github.com/tailwindlabs/tailwindcss/pull/20027)) -- Canonicalization: don't suggest invalid `calc(…)` expressions (e.g. `px-[calc(1rem+0px)]` → `px-[calc(1rem+0)]`) ([#20127](https://github.com/tailwindlabs/tailwindcss/pull/20127)) -- Canonicalization: avoid suggesting large spacing-scale values for arbitrary lengths (e.g. `left-[99999px]` → `left-[99999px]`, not `left-24999.75`) ([#20130](https://github.com/tailwindlabs/tailwindcss/pull/20130)) -- Ensure `@tailwindcss/cli` in `--watch` mode recovers when a tracked dependency is deleted and restored ([#20137](https://github.com/tailwindlabs/tailwindcss/pull/20137)) -- Ensure standalone `@tailwindcss/cli` binaries are ignored when scanning for class candidates ([#20139](https://github.com/tailwindlabs/tailwindcss/pull/20139)) -- Ensure class candidates are extracted from Twig `addClass(…)` and `removeClass(…)` calls ([#20198](https://github.com/tailwindlabs/tailwindcss/pull/20198)) -- Don't crash in the Ruby or Vue preprocessors when scanning files containing invalid UTF-8 bytes ([#19588](https://github.com/tailwindlabs/tailwindcss/pull/19588)) -- Allow `@variant` to be used inside `addBase` ([#19480](https://github.com/tailwindlabs/tailwindcss/pull/19480)) -- Ensure `@source` globs with symlinks are preserved ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203)) -- Ensure later `@source` rules can re-include files excluded by earlier `@source not` rules ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203)) -- Upgrade: don't migrate empty class rules to invalid `@utility` rules ([#20205](https://github.com/tailwindlabs/tailwindcss/pull/20205)) -- Ensure transitions between `inset-shadow-none` and other inset shadows work correctly ([#20208](https://github.com/tailwindlabs/tailwindcss/pull/20208)) -- Ensure explicitly referenced `@source` directories are scanned even when ignored by git ([#20214](https://github.com/tailwindlabs/tailwindcss/pull/20214)) -- Ensure `@source` globs ending in `**/*` preserve dynamic path segments to avoid scanning too many files ([#20217](https://github.com/tailwindlabs/tailwindcss/pull/20217)) -- Canonicalization: don't fold `calc(…)` divisions when the result would require high precision (e.g. `w-[calc(100%/3.5)]` → `w-[calc(100%/3.5)]`, not `w-[28.571428571428573%]`) ([#20221](https://github.com/tailwindlabs/tailwindcss/pull/20221)) -- Serve ESM type declarations to ESM importers of `@tailwindcss/postcss` ([#20228](https://github.com/tailwindlabs/tailwindcss/pull/20228)) - -### Changed - -- Generate `0` instead of `calc(var(--spacing) * 0)` for spacing utilities like `m-0` and `left-0` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196)) -- Generate `var(--spacing)` instead of `calc(var(--spacing) * 1)` for spacing utilities like `m-1` and `left-1` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196)) - -## [4.3.0] - 2026-05-08 - -### Added - -- Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901)) -- Add `scrollbar-{auto,thin,none}` utilities for `scrollbar-width`, and `scrollbar-thumb-*` / `scrollbar-track-*` color utilities for `scrollbar-color` ([#19981](https://github.com/tailwindlabs/tailwindcss/pull/19981), [#20019](https://github.com/tailwindlabs/tailwindcss/pull/20019)) -- Add `scrollbar-gutter-*` utilities ([#20018](https://github.com/tailwindlabs/tailwindcss/pull/20018)) -- Add `zoom-*` utilities ([#20020](https://github.com/tailwindlabs/tailwindcss/pull/20020)) -- Add `tab-*` utilities ([#20022](https://github.com/tailwindlabs/tailwindcss/pull/20022)) -- Allow using `@variant` with stacked variants (e.g. `@variant hover:focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996)) -- Allow using `@variant` with compound variants (e.g. `@variant hover, focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996)) -- Support `--default(…)` in `--value(…)` and `--modifier(…)` for functional `@utility` definitions ([#19989](https://github.com/tailwindlabs/tailwindcss/pull/19989)) - -### Fixed - -- Ensure `@plugin` resolves package JavaScript entries instead of browser CSS entries when using `@tailwindcss/vite` ([#19949](https://github.com/tailwindlabs/tailwindcss/pull/19949)) -- Fix relative `@import` and `@plugin` paths resolving from the wrong directory when using `@tailwindcss/vite` ([#19965](https://github.com/tailwindlabs/tailwindcss/pull/19965)) -- Ensure CSS files containing `@variant` are processed by `@tailwindcss/vite` ([#19966](https://github.com/tailwindlabs/tailwindcss/pull/19966)) -- Resolve imports relative to `base` when `result.opts.from` is not provided when using `@tailwindcss/postcss` ([#19980](https://github.com/tailwindlabs/tailwindcss/pull/19980)) -- Canonicalization: preserve significant `_` whitespace in arbitrary values ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986)) -- Canonicalization: add parentheses when removing whitespace from arbitrary values would hurt readability (e.g. `w-[calc(100%---spacing(60))]` → `w-[calc(100%-(--spacing(60)))]`) ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986)) -- Canonicalization: preserve the original unit in arbitrary values instead of normalizing to base units (e.g. `-mt-[20in]` → `mt-[-20in]`, not `mt-[-1920px]`) ([#19988](https://github.com/tailwindlabs/tailwindcss/pull/19988)) -- Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#19991](https://github.com/tailwindlabs/tailwindcss/pull/19991)) -- Upgrade: don’t migrate inline `style` attributes (e.g. `style="flex-grow: 1"` → `style="flex-grow: 1"`, not `style="grow: 1"`) ([#19918](https://github.com/tailwindlabs/tailwindcss/pull/19918)) -- Allow multiple `@utility` definitions with the same name but different value types ([#19777](https://github.com/tailwindlabs/tailwindcss/pull/19777)) -- Export missing `PluginWithConfig` type from `tailwindcss/plugin` to fix errors when inferring plugin config types ([#19707](https://github.com/tailwindlabs/tailwindcss/pull/19707)) -- Ensure `start` and `end` legacy utilities without values do not generate CSS ([#20003](https://github.com/tailwindlabs/tailwindcss/pull/20003)) -- Ensure `--value(…)` is required in functional `@utility` definitions ([#20005](https://github.com/tailwindlabs/tailwindcss/pull/20005)) -- Canonicalization: preserve required whitespace around operators in negated arbitrary values (e.g. `-left-[(var(--a)+var(--b))]`) ([#20011](https://github.com/tailwindlabs/tailwindcss/pull/20011)) - -## [4.2.4] - 2026-04-21 - -### Fixed - -- Ensure imports in `@import` and `@plugin` still resolve correctly when using Vite aliases in `@tailwindcss/vite` ([#19947](https://github.com/tailwindlabs/tailwindcss/pull/19947)) - -## [4.2.3] - 2026-04-20 - -### Fixed - -- Canonicalization: improve canonicalization for `tracking-*` utilities by preferring non-negative utilities (e.g. `-tracking-tighter` → `tracking-wider`) ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827)) -- Fix crash due to invalid characters in candidate (exceeding valid unicode code point range) ([#19829](https://github.com/tailwindlabs/tailwindcss/pull/19829)) -- Ensure query params in imports are considered unique resources when using `@tailwindcss/webpack` ([#19723](https://github.com/tailwindlabs/tailwindcss/pull/19723)) -- Canonicalization: collapse arbitrary values into shorthand utilities (e.g. `px-[1.2rem] py-[1.2rem]` → `p-[1.2rem]`) ([#19837](https://github.com/tailwindlabs/tailwindcss/pull/19837)) -- Canonicalization: collapse `border-{t,b}-*` into `border-y-*`, `border-{l,r}-*` into `border-x-*`, and `border-{t,r,b,l}-*` into `border-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842)) -- Canonicalization: collapse `scroll-m{t,b}-*` into `scroll-my-*`, `scroll-m{l,r}-*` into `scroll-mx-*`, and `scroll-m{t,r,b,l}-*` into `scroll-m-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842)) -- Canonicalization: collapse `scroll-p{t,b}-*` into `scroll-py-*`, `scroll-p{l,r}-*` into `scroll-px-*`, and `scroll-p{t,r,b,l}-*` into `scroll-p-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842)) -- Canonicalization: collapse `overflow-{x,y}-*` into `overflow-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842)) -- Canonicalization: collapse `overscroll-{x,y}-*` into `overscroll-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842)) -- Read from `--placeholder-color` instead of `--background-color` for `placeholder-*` utilities ([#19843](https://github.com/tailwindlabs/tailwindcss/pull/19843)) -- Upgrade: ensure files are not emptied out when killing the upgrade process while it's running ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846)) -- Upgrade: use `config.content` when migrating from Tailwind CSS v3 to Tailwind CSS v4 ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846)) -- Upgrade: never migrate files that are ignored by git ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846)) -- Add `.env` and `.env.*` to default ignored content files ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846)) -- Canonicalization: migrate `overflow-ellipsis` into `text-ellipsis` ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849)) -- Canonicalization: migrate `start-full` → `inset-s-full`, `start-auto` → `inset-s-auto`, `start-px` → `inset-s-px`, and `start-` → `inset-s-` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849)) -- Canonicalization: migrate `end-full` → `inset-e-full`, `end-auto` → `inset-e-auto`, `end-px` → `inset-e-px`, and `end-` → `inset-e-` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849)) -- Canonicalization: move the `-` sign inside the arbitrary value `-left-[9rem]` → `left-[-9rem]` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858)) -- Canonicalization: move the `-` sign outside the arbitrary value `ml-[calc(-1*var(--width))]` → `-ml-(--width)` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858)) -- Improve performance when scanning JSONL / NDJSON files ([#19862](https://github.com/tailwindlabs/tailwindcss/pull/19862)) -- Support `NODE_PATH` environment variable in standalone CLI ([#19617](https://github.com/tailwindlabs/tailwindcss/pull/19617)) - -## [4.2.2] - 2026-03-18 - -### Fixed - -- Don't crash when candidates contain prototype properties like `row-constructor` ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725)) -- Canonicalize `calc(var(--spacing)*…)` expressions into `--spacing(…)` ([#19769](https://github.com/tailwindlabs/tailwindcss/pull/19769)) -- Fix crash in canonicalization step when handling utilities containing `@property` at-rules (e.g. `shadow-sm border`) ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727)) -- Skip full reload for server only modules scanned by client CSS when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745)) -- Add support for Vite 8 in `@tailwindcss/vite` ([#19790](https://github.com/tailwindlabs/tailwindcss/pull/19790)) -- Improve canonicalization for bare values exceeding default spacing scale suggestions (e.g. `w-1234 h-1234` → `size-1234`) ([#19809](https://github.com/tailwindlabs/tailwindcss/pull/19809)) -- Fix canonicalization resulting in empty list (e.g. `w-5 h-5 size-5` → `''` instead of `size-5`) ([#19812](https://github.com/tailwindlabs/tailwindcss/pull/19812)) -- Resolve tsconfig paths to allow for `@import '@/path/to/file';` when using `@tailwindcss/vite` ([#19803](https://github.com/tailwindlabs/tailwindcss/pull/19803)) - -## [4.2.1] - 2026-02-23 - -### Fixed - -- Allow trailing dash in functional utility names for backwards compatibility ([#19696](https://github.com/tailwindlabs/tailwindcss/pull/19696)) -- Properly detect classes containing `.` characters within curly braces in MDX files ([#19711](https://github.com/tailwindlabs/tailwindcss/pull/19711)) - -## [4.2.0] - 2026-02-18 - -### Added - -- Add mauve, olive, mist, and taupe color palettes to the default theme ([#19627](https://github.com/tailwindlabs/tailwindcss/pull/19627)) -- Add `@tailwindcss/webpack` package to run Tailwind CSS as a webpack plugin ([#19610](https://github.com/tailwindlabs/tailwindcss/pull/19610)) -- Add `pbs-*` and `pbe-*` utilities for `padding-block-start` and `padding-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601)) -- Add `mbs-*` and `mbe-*` utilities for `margin-block-start` and `margin-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601)) -- Add `scroll-pbs-*` and `scroll-pbe-*` utilities for `scroll-padding-block-start` and `scroll-padding-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601)) -- Add `scroll-mbs-*` and `scroll-mbe-*` utilities for `scroll-margin-block-start` and `scroll-margin-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601)) -- Add `border-bs-*` and `border-be-*` utilities for `border-block-start` and `border-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601)) -- Add `inline-*`, `min-inline-*`, `max-inline-*` utilities for `inline-size`, `min-inline-size`, and `max-inline-size` ([#19612](https://github.com/tailwindlabs/tailwindcss/pull/19612)) -- Add `block-*`, `min-block-*`, `max-block-*` utilities for `block-size`, `min-block-size`, and `max-block-size` ([#19612](https://github.com/tailwindlabs/tailwindcss/pull/19612)) -- Add `inset-s-*`, `inset-e-*`, `inset-bs-*`, `inset-be-*` utilities for `inset-inline-start`, `inset-inline-end`, `inset-block-start`, and `inset-block-end` ([#19613](https://github.com/tailwindlabs/tailwindcss/pull/19613)) -- Add `font-features-*` utility for `font-feature-settings` ([#19623](https://github.com/tailwindlabs/tailwindcss/pull/19623)) - -### Fixed - -- Prevent double `@supports` wrapper for `color-mix` values ([#19450](https://github.com/tailwindlabs/tailwindcss/pull/19450)) -- Allow whitespace around `@source inline()` argument ([#19461](https://github.com/tailwindlabs/tailwindcss/pull/19461)) -- Emit comment when source maps are saved to files when using `@tailwindcss/cli` ([#19447](https://github.com/tailwindlabs/tailwindcss/pull/19447)) -- Detect utilities containing capital letters followed by numbers ([#19465](https://github.com/tailwindlabs/tailwindcss/pull/19465)) -- Fix class extraction for Rails' strict locals ([#19525](https://github.com/tailwindlabs/tailwindcss/pull/19525)) -- Align `@utility` name validation with Oxide scanner rules ([#19524](https://github.com/tailwindlabs/tailwindcss/pull/19524)) -- Fix infinite loop when using `@variant` inside `@custom-variant` ([#19633](https://github.com/tailwindlabs/tailwindcss/pull/19633)) -- Allow multiples of `.25` in `aspect-*` fractions (e.g. `aspect-8.5/11`) ([#19688](https://github.com/tailwindlabs/tailwindcss/pull/19688)) -- Ensure changes to external files listed via `@source` trigger a full page reload when using `@tailwindcss/vite` ([#19670](https://github.com/tailwindlabs/tailwindcss/pull/19670)) -- Improve performance of Oxide scanner in bigger projects by reducing file system walks ([#19632](https://github.com/tailwindlabs/tailwindcss/pull/19632)) -- Ensure import aliases in Astro v5 work without crashing when using `@tailwindcss/vite` ([#19677](https://github.com/tailwindlabs/tailwindcss/issues/19677)) -- Allow escape characters in `@utility` names to improve support with formatters such as Biome ([#19626](https://github.com/tailwindlabs/tailwindcss/pull/19626)) -- Fix incorrect canonicalization results when canonicalizing multiple times ([#19675](https://github.com/tailwindlabs/tailwindcss/pull/19675)) -- Add `.jj` to default ignored content directories ([#19687](https://github.com/tailwindlabs/tailwindcss/pull/19687)) - -### Deprecated - -- Deprecate `start-*` and `end-*` utilities in favor of `inset-s-*` and `inset-e-*` utilities ([#19613](https://github.com/tailwindlabs/tailwindcss/pull/19613)) - -## [4.1.18] - 2025-12-11 - -### Fixed - -- Ensure validation of `source(…)` happens relative to the file it is in ([#19274](https://github.com/tailwindlabs/tailwindcss/pull/19274)) -- Include filename and line numbers in CSS parse errors ([#19282](https://github.com/tailwindlabs/tailwindcss/pull/19282)) -- Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243)) -- Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243)) -- Support environment API in `@tailwindcss/vite` ([#18970](https://github.com/tailwindlabs/tailwindcss/pull/18970)) -- Preserve case of theme keys from JS configs and plugins ([#19337](https://github.com/tailwindlabs/tailwindcss/pull/19337)) -- Write source maps correctly on the CLI when using `--watch` ([#19373](https://github.com/tailwindlabs/tailwindcss/pull/19373)) -- Handle special defaults (like `ringColor.DEFAULT`) in JS configs ([#19348](https://github.com/tailwindlabs/tailwindcss/pull/19348)) -- Improve backwards compatibility for `content` theme key from JS configs ([#19381](https://github.com/tailwindlabs/tailwindcss/pull/19381)) -- Upgrade: Handle `future` and `experimental` config keys ([#19344](https://github.com/tailwindlabs/tailwindcss/pull/19344)) -- Try to canonicalize any arbitrary utility to a bare value ([#19379](https://github.com/tailwindlabs/tailwindcss/pull/19379)) -- Validate candidates similarly to Oxide ([#19397](https://github.com/tailwindlabs/tailwindcss/pull/19397)) -- Canonicalization: combine `text-*` and `leading-*` classes ([#19396](https://github.com/tailwindlabs/tailwindcss/pull/19396)) -- Correctly handle duplicate CLI arguments ([#19416](https://github.com/tailwindlabs/tailwindcss/pull/19416)) -- Don’t emit color-mix fallback rules inside `@keyframes` ([#19419](https://github.com/tailwindlabs/tailwindcss/pull/19419)) -- CLI: Don't hang when output is `/dev/stdout` ([#19421](https://github.com/tailwindlabs/tailwindcss/pull/19421)) - -## [3.4.19] - 2025-12-10 - -### Fixed - -- Don’t break `sibling-*()` functions when used inside `calc(…)` ([#19335](https://github.com/tailwindlabs/tailwindcss/pull/19335)) - -## [4.1.17] - 2025-11-06 - ### Fixed - Substitute `@variant` inside legacy JS APIs ([#19263](https://github.com/tailwindlabs/tailwindcss/pull/19263)) -- Prevent occasional crash on Windows when loaded into a worker thread ([#19242](https://github.com/tailwindlabs/tailwindcss/pull/19242)) +- Handle utilities with `1` keys in a JS config ([#19254](https://github.com/tailwindlabs/tailwindcss/pull/19254)) + +### Added + +- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901)) ## [4.1.16] - 2025-10-23 @@ -313,16 +65,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgrade: Ensure first class inside `className` is migrated ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031)) - Upgrade: Migrate classes inside `*ClassName` and `*Class` attributes ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031)) -## [3.4.18] - 2025-10-01 - -### Fixed - -- Improve support for raw `supports-[…]` queries in arbitrary values ([#13605](https://github.com/tailwindlabs/tailwindcss/pull/13605)) -- Fix `require.cache` error when loaded through a TypeScript file in Node 22.18+ ([#18665](https://github.com/tailwindlabs/tailwindcss/pull/18665)) -- Support `import.meta.resolve(…)` in configs for new enough Node.js versions ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938)) -- Allow using newer versions of `postcss-load-config` for better ESM and TypeScript PostCSS config support with the CLI ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938)) -- Remove irrelevant utility rules when matching important classes ([#19030](https://github.com/tailwindlabs/tailwindcss/pull/19030)) - ## [4.1.13] - 2025-09-03 ### Changed @@ -1655,6 +1397,16 @@ For a deep-dive into everything that's new, [check out the announcement post](ht - First 4.0.0-alpha.1 release +## [3.4.18] - 2024-10-01 + +### Fixed + +- Improve support for raw `supports-[…]` queries in arbitrary values ([#13605](https://github.com/tailwindlabs/tailwindcss/pull/13605)) +- Fix `require.cache` error when loaded through a TypeScript file in Node 22.18+ ([#18665](https://github.com/tailwindlabs/tailwindcss/pull/18665)) +- Support `import.meta.resolve(…)` in configs for new enough Node.js versions ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938)) +- Allow using newer versions of `postcss-load-config` for better ESM and TypeScript PostCSS config support with the CLI ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938)) +- Remove irrelevant utility rules when matching important classes ([#19030](https://github.com/tailwindlabs/tailwindcss/pull/19030)) + ## [3.4.17] - 2024-12-17 ### Fixed @@ -4123,23 +3875,10 @@ No release notes - Everything! -[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.3...HEAD -[4.3.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.2...v4.3.3 -[4.3.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.1...v4.3.2 -[4.3.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.0...v4.3.1 -[4.3.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.4...v4.3.0 -[4.2.4]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.3...v4.2.4 -[4.2.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.2...v4.2.3 -[4.2.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.1...v4.2.2 -[4.2.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.0...v4.2.1 -[4.2.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.18...v4.2.0 -[4.1.18]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.17...v4.1.18 -[3.4.19]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.18...v3.4.19 -[4.1.17]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.16...v4.1.17 +[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.16...HEAD [4.1.16]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.15...v4.1.16 [4.1.15]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.14...v4.1.15 [4.1.14]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.13...v4.1.14 -[3.4.18]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v3.4.18 [4.1.13]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.12...v4.1.13 [4.1.12]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.11...v4.1.12 [4.1.11]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.10...v4.1.11 @@ -4197,6 +3936,7 @@ No release notes [4.0.0-alpha.24]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.0-alpha.23...v4.0.0-alpha.24 [4.0.0-alpha.23]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.0-alpha.22...v4.0.0-alpha.23 [4.0.0-alpha.22]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v4.0.0-alpha.22 +[3.4.18]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v3.4.18 [3.4.17]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.16...v3.4.17 [3.4.16]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.15...v3.4.16 [3.4.15]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.14...v3.4.15 diff --git a/Cargo.lock b/Cargo.lock index 73fd9e995..162cf37dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,7 +40,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", - "regex-automata 0.4.18", + "regex-automata 0.4.8", "serde", ] @@ -59,22 +59,11 @@ dependencies = [ "syn", ] -[[package]] -name = "console" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" -dependencies = [ - "encode_unicode", - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "convert_case" -version = "0.11.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" dependencies = [ "unicode-segmentation", ] @@ -115,9 +104,19 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "ctor" -version = "1.0.12" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" [[package]] name = "diff" @@ -125,6 +124,21 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "dtor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -137,12 +151,6 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "errno" version = "0.3.9" @@ -168,104 +176,16 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - [[package]] name = "globset" -version = "0.4.20" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.18", + "regex-automata 0.4.8", "regex-syntax 0.8.5", ] @@ -290,7 +210,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.18", + "regex-automata 0.4.8", "same-file", "walkdir", "winapi-util", @@ -298,7 +218,7 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.33" +version = "0.4.24" dependencies = [ "bstr", "crossbeam-channel", @@ -307,24 +227,12 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.18", + "regex-automata 0.4.8", "same-file", "walkdir", "winapi-util", ] -[[package]] -name = "insta" -version = "1.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" -dependencies = [ - "console", - "once_cell", - "similar", - "tempfile", -] - [[package]] name = "itertools" version = "0.11.0" @@ -348,12 +256,12 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libloading" -version = "0.9.0" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-link", + "windows-targets", ] [[package]] @@ -391,13 +299,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "napi" -version = "3.11.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de33522036981030a75c231829566bc63414e08101a6f5ff4ac6cef19c8e0941" +checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7" dependencies = [ "bitflags", "ctor", - "futures", "napi-build", "napi-sys", "nohash-hasher", @@ -406,15 +313,15 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.3.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" +checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14" [[package]] name = "napi-derive" -version = "3.6.0" +version = "3.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49c513341a61a16a10af6efcce46b30d0822ba2d4fb197d24d33dfc199c78d5" +checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0" dependencies = [ "convert_case", "ctor", @@ -426,9 +333,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "6.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd" +checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a" dependencies = [ "convert_case", "proc-macro2", @@ -439,9 +346,9 @@ dependencies = [ [[package]] name = "napi-sys" -version = "3.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fbf1fa9f1babfe396d74bbbf52b3643770243e8f5b0b46715d4caf7f0dfc9a" +checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d" dependencies = [ "libloading", ] @@ -474,9 +381,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.4" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "overload" @@ -511,18 +418,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" -version = "1.12.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -530,9 +437,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.13.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -546,7 +453,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.18", + "regex-automata 0.4.8", "regex-syntax 0.8.5", ] @@ -561,9 +468,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.18" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -631,18 +538,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - [[package]] name = "smallvec" version = "1.10.0" @@ -651,9 +546,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -681,8 +576,7 @@ dependencies = [ "dunce", "fast-glob", "globwalk", - "ignore 0.4.33", - "insta", + "ignore 0.4.24", "log", "pretty_assertions", "rayon", @@ -844,12 +738,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-sys" version = "0.52.0" @@ -868,15 +756,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-targets" version = "0.52.6" diff --git a/README.md b/README.md index 5f532607d..7d21bd883 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@

- Build Status + Build Status Total Downloads - Latest Release - License + Latest Release + License

--- @@ -29,8 +29,8 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/crates/ignore/Cargo.toml b/crates/ignore/Cargo.toml index d0af38ba9..bd0352d15 100644 --- a/crates/ignore/Cargo.toml +++ b/crates/ignore/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ignore" -version = "0.4.33" #:version +version = "0.4.24" #:version authors = ["Andrew Gallant "] description = """ A fast library for efficiently matching ignore files such as `.gitignore` @@ -12,10 +12,7 @@ repository = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore" readme = "README.md" keywords = ["glob", "ignore", "gitignore", "pattern", "file"] license = "Unlicense OR MIT" -# CHANGED: Use an explicit edition instead of `edition.workspace = true` since this crate is -# vendored into the Tailwind CSS workspace. edition = "2024" -rust-version = "1.88" [lib] name = "ignore" @@ -23,17 +20,15 @@ bench = false [dependencies] crossbeam-deque = "0.8.3" -# CHANGED: Use the published globset crate instead of a path dependency. -globset = "0.4.20" +globset = "0.4.17" log = "0.4.20" memchr = "2.6.3" same-file = "1.0.6" walkdir = "2.4.0" -# CHANGED: Added `dunce` to canonicalize paths without UNC prefixes on Windows. dunce = "1.0.5" [dependencies.regex-automata] -version = "0.4.18" +version = "0.4.0" default-features = false features = ["std", "perf", "syntax", "meta", "nfa", "hybrid", "dfa-onepass"] diff --git a/crates/ignore/examples/walk.rs b/crates/ignore/examples/walk.rs index c61d0515e..9c627dc3e 100644 --- a/crates/ignore/examples/walk.rs +++ b/crates/ignore/examples/walk.rs @@ -18,7 +18,9 @@ fn main() { let stdout_thread = std::thread::spawn(move || { let mut stdout = std::io::BufWriter::new(std::io::stdout()); for dent in rx { - stdout.write_all(&Vec::from_path_lossy(dent.path())).unwrap(); + stdout + .write_all(&Vec::from_path_lossy(dent.path())) + .unwrap(); stdout.write_all(b"\n").unwrap(); } }); diff --git a/crates/ignore/src/default_types.rs b/crates/ignore/src/default_types.rs index 6b5bba0f0..4e060b76a 100644 --- a/crates/ignore/src/default_types.rs +++ b/crates/ignore/src/default_types.rs @@ -47,7 +47,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[ (&["cml"], &["*.cml"]), (&["coffeescript"], &["*.coffee"]), (&["config"], &["*.cfg", "*.conf", "*.config", "*.ini"]), - (&["container"], &["*Containerfile*", "*Dockerfile*"]), (&["coq"], &["*.v"]), (&["cpp"], &[ "*.[ChH]", "*.cc", "*.[ch]pp", "*.[ch]xx", "*.hh", "*.inl", @@ -110,7 +109,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[ (&["hbs"], &["*.hbs"]), (&["hs"], &["*.hs", "*.lhs"]), (&["html"], &["*.htm", "*.html", "*.ejs"]), - (&["hurl"], &["*.hurl"]), (&["hy"], &["*.hy"]), (&["idris"], &["*.idr", "*.lidr"]), (&["janet"], &["*.janet"]), @@ -187,7 +185,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[ (&["mint"], &["*.mint"]), (&["mk"], &["mkfile"]), (&["ml"], &["*.ml"]), - (&["mojo"], &["*.mojo"]), (&["motoko"], &["*.mo"]), (&["msbuild"], &[ "*.csproj", "*.fsproj", "*.vcxproj", "*.proj", "*.props", "*.targets", @@ -209,12 +206,11 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[ "*.php", "*.php3", "*.php4", "*.php5", "*.php7", "*.php8", "*.pht", "*.phtml" ]), - (&["pkgbuild"], &["PKGBUILD"]), (&["po"], &["*.po"]), (&["pod"], &["*.pod"]), (&["postscript"], &["*.eps", "*.ps"]), (&["prolog"], &["*.pl", "*.pro", "*.prolog", "*.P"]), - (&["proto", "protobuf"], &["*.proto"]), + (&["protobuf"], &["*.proto"]), (&["ps"], &["*.cdxml", "*.ps1", "*.ps1xml", "*.psd1", "*.psm1"]), (&["puppet"], &["*.epp", "*.erb", "*.pp", "*.rb"]), (&["purs"], &["*.purs"]), @@ -235,7 +231,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[ (&["red"], &["*.r", "*.red", "*.reds"]), (&["rescript"], &["*.res", "*.resi"]), (&["robot"], &["*.robot"]), - (&["rocq"], &["*.v"]), (&["rst"], &["*.rst"]), (&["ruby"], &[ // Idiomatic files @@ -279,7 +274,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[ (&["spark"], &["*.spark"]), (&["spec"], &["*.spec"]), (&["sql"], &["*.sql", "*.psql"]), - (&["ssa"], &["*.ssa"]), (&["stylus"], &["*.styl"]), (&["sv"], &["*.v", "*.vg", "*.sv", "*.svh", "*.h"]), (&["svelte"], &["*.svelte", "*.svelte.ts"]), @@ -365,14 +359,4 @@ mod tests { previous_name = name; } } - - #[test] - fn default_types_aliases_are_sorted() { - for (aliases, _) in DEFAULT_TYPES.iter() { - assert!( - aliases.is_sorted(), - "this alias list is not sorted: {aliases:?}", - ); - } - } } diff --git a/crates/ignore/src/dir.rs b/crates/ignore/src/dir.rs index 6bee724c8..11b58f8ca 100644 --- a/crates/ignore/src/dir.rs +++ b/crates/ignore/src/dir.rs @@ -16,7 +16,7 @@ use std::{ collections::HashMap, ffi::{OsStr, OsString}, - fs::{self, File, FileType}, + fs::{File, FileType}, io::{self, BufRead}, path::{Path, PathBuf}, sync::{Arc, RwLock, Weak}, @@ -25,7 +25,7 @@ use std::{ use crate::{ gitignore::{self, Gitignore, GitignoreBuilder}, overrides::{self, Override}, - pathutil::{is_hidden_entry, strip_prefix}, + pathutil::{is_hidden, strip_prefix}, types::{self, Types}, walk::DirEntry, {Error, Match, PartialErrorBuilder}, @@ -91,25 +91,7 @@ struct IgnoreOptions { /// Ignore is a matcher useful for recursively walking one or more directories. #[derive(Clone, Debug)] -pub(crate) struct Ignore { - inner: Arc, - // Parent matchers are cached independently of the path being walked, but - // matching them still needs the canonicalized path originally passed to - // `add_parents`. For example, when walking `/tmp/project/src`, parent - // matchers use `/tmp/project/src` to rewrite `/tmp/project/src/foo.py` - // before matching it against ignore files from `/tmp/project` and its - // ancestors. - // - // For ripgrep itself, this means that `rg pat src tests` must rewrite - // `src/foo` relative to `.../src`, and not whatever root was prepared - // first. - // - // See: https://github.com/BurntSushi/ripgrep/pull/3420 - // See: https://github.com/BurntSushi/ripgrep/issues/3376 - // See: https://github.com/BurntSushi/ripgrep/issues/3419 - // See: https://github.com/BurntSushi/ripgrep/issues/3320 - absolute_base: Option>, -} +pub(crate) struct Ignore(Arc); #[derive(Clone, Debug)] struct IgnoreInner { @@ -130,9 +112,12 @@ struct IgnoreInner { /// /// If this is the root directory or there are otherwise no more /// directories to match, then `parent` is `None`. - parent: Option>, + parent: Option, /// Whether this is an absolute parent matcher, as added by add_parent. is_absolute_parent: bool, + /// The absolute base path of this matcher. Populated only if parent + /// directories are added. + absolute_base: Option>, /// The directory that gitignores should be interpreted relative to. /// /// Usually this is the directory containing the gitignore file. But in @@ -167,36 +152,34 @@ struct IgnoreInner { impl Ignore { /// Return the directory path of this matcher. - #[cfg(test)] pub(crate) fn path(&self) -> &Path { - &self.inner.dir + &self.0.dir } /// Return true if this matcher has no parent. pub(crate) fn is_root(&self) -> bool { - self.inner.parent.is_none() + self.0.parent.is_none() + } + + /// Returns true if this matcher was added via the `add_parents` method. + pub(crate) fn is_absolute_parent(&self) -> bool { + self.0.is_absolute_parent } /// Return this matcher's parent, if one exists. pub(crate) fn parent(&self) -> Option { - self.inner.parent.as_ref().map(|parent| Ignore { - inner: parent.clone(), - absolute_base: self.absolute_base.clone(), - }) + self.0.parent.clone() } /// Create a new `Ignore` matcher with the parent directories of `dir`. /// /// Note that this can only be called on an `Ignore` matcher with no /// parents (i.e., `is_root` returns `true`). This will panic otherwise. - pub(crate) fn add_parents>( - &self, - path: P, - ) -> (Ignore, Option) { - if !self.inner.opts.parents - && !self.inner.opts.git_ignore - && !self.inner.opts.git_exclude - && !self.inner.opts.git_global + pub(crate) fn add_parents>(&self, path: P) -> (Ignore, Option) { + if !self.0.opts.parents + && !self.0.opts.git_ignore + && !self.0.opts.git_exclude + && !self.0.opts.git_global { // If we never need info from parent directories, then don't do // anything. @@ -226,34 +209,25 @@ impl Ignore { let mut errs = PartialErrorBuilder::default(); let mut ig = self.clone(); for parent in parents.into_iter().rev() { - let mut compiled = self.inner.compiled.write().unwrap(); + let mut compiled = self.0.compiled.write().unwrap(); if let Some(weak) = compiled.get(parent.as_os_str()) { if let Some(prebuilt) = weak.upgrade() { - ig = Ignore { - inner: prebuilt, - absolute_base: Some(absolute_base.clone()), - }; + ig = Ignore(prebuilt); continue; } } let (mut igtmp, err) = ig.add_child_path(parent); errs.maybe_push(err); igtmp.is_absolute_parent = true; - igtmp.has_git = - if self.inner.opts.require_git && self.inner.opts.git_ignore { - parent.join(".git").exists() || parent.join(".jj").exists() - } else { - false - }; - let ig_arc = Arc::new(igtmp); - ig = Ignore { - inner: ig_arc.clone(), - absolute_base: Some(absolute_base.clone()), + igtmp.absolute_base = Some(absolute_base.clone()); + igtmp.has_git = if self.0.opts.require_git && self.0.opts.git_ignore { + parent.join(".git").exists() || parent.join(".jj").exists() + } else { + false }; - compiled.insert( - parent.as_os_str().to_os_string(), - Arc::downgrade(&ig_arc), - ); + let ig_arc = Arc::new(igtmp); + ig = Ignore(ig_arc.clone()); + compiled.insert(parent.as_os_str().to_os_string(), Arc::downgrade(&ig_arc)); } (ig, errs.into_error_option()) } @@ -266,161 +240,60 @@ impl Ignore { /// returned if it exists. /// /// Note that all I/O errors are completely ignored. - pub(crate) fn add_child>( - &self, - dir: P, - ) -> (Ignore, Option) { + pub(crate) fn add_child>(&self, dir: P) -> (Ignore, Option) { let (ig, err) = self.add_child_path(dir.as_ref()); - ( - Ignore { - inner: Arc::new(ig), - absolute_base: self.absolute_base.clone(), - }, - err, - ) - } - - /// Like add_child, but uses successful read_dir entries to reduce - /// probing when discovering ignore files. - pub(crate) fn add_child_with_entries>( - &self, - dir: P, - entries: &[fs::DirEntry], - ) -> (Ignore, Option) { - let files = self.collect_ignore_files(entries); - let (ig, err) = self.add_child_path_with_found_ignore_files( - dir.as_ref(), - Some(&files), - ); - ( - Ignore { - inner: Arc::new(ig), - absolute_base: self.absolute_base.clone(), - }, - err, - ) + (Ignore(Arc::new(ig)), err) } /// Like add_child, but takes a full path and returns an IgnoreInner. fn add_child_path(&self, dir: &Path) -> (IgnoreInner, Option) { - self.add_child_path_with_found_ignore_files(dir, None) - } - - fn collect_ignore_files( - &self, - entries: &[fs::DirEntry], - ) -> IgnoreFilesFound { - let custom_ignore_filenames = &self.inner.custom_ignore_filenames; - let mut files = IgnoreFilesFound { - has_ignore: false, - has_git_ignore: false, - has_git_dir: false, - has_jj_dir: false, - custom_ignore_files: vec![false; custom_ignore_filenames.len()], - }; - for entry in entries { - let file_name = entry.file_name(); - if file_name == OsStr::new(".ignore") { - files.has_ignore = true; - } else if file_name == OsStr::new(".gitignore") { - files.has_git_ignore = true; - } else if file_name == OsStr::new(".git") { - files.has_git_dir = true; - } else if file_name == OsStr::new(".jj") { - files.has_jj_dir = true; - } - for (i, name) in custom_ignore_filenames.iter().enumerate() { - if file_name == name.as_os_str() { - files.custom_ignore_files[i] = true; - } - } - } - files - } - - fn add_child_path_with_found_ignore_files( - &self, - dir: &Path, - ignore_files_list: Option<&IgnoreFilesFound>, - ) -> (IgnoreInner, Option) { - let check_vcs_dir = self.inner.opts.require_git - && (self.inner.opts.git_ignore || self.inner.opts.git_exclude); - let git_type = if check_vcs_dir - && ignore_files_list.is_none_or(|i| i.has_git_dir) - { - dir.join(".git").metadata().ok().map(|md| md.file_type()) - } else { - None - }; - let has_jj = check_vcs_dir - && ignore_files_list.is_none_or(|i| i.has_jj_dir) - && dir.join(".jj").exists(); - let has_git = check_vcs_dir && (git_type.is_some() || has_jj); + let git_type = + if self.0.opts.require_git && (self.0.opts.git_ignore || self.0.opts.git_exclude) { + dir.join(".git").metadata().ok().map(|md| md.file_type()) + } else { + None + }; + let has_git = git_type.is_some() || dir.join(".jj").exists(); let mut errs = PartialErrorBuilder::default(); - let custom_ig_matcher = if self - .inner - .custom_ignore_filenames - .is_empty() - { + let custom_ig_matcher = if self.0.custom_ignore_filenames.is_empty() { Gitignore::empty() } else { - let custom_ignore_names: Vec<&OsString> = match ignore_files_list { - None => self.inner.custom_ignore_filenames.iter().collect(), - Some(m) => self - .inner - .custom_ignore_filenames - .iter() - .zip(m.custom_ignore_files.iter()) - .filter(|&(_, &matched)| matched) - .map(|(name, _)| name) - .collect(), - }; - if custom_ignore_names.is_empty() { - Gitignore::empty() - } else { - let (m, err) = create_gitignore( - &dir, - &dir, - &custom_ignore_names, - self.inner.opts.ignore_case_insensitive, - ); - errs.maybe_push(err); - m - } + let (m, err) = create_gitignore( + &dir, + &dir, + &self.0.custom_ignore_filenames, + self.0.opts.ignore_case_insensitive, + ); + errs.maybe_push(err); + m }; - let ig_matcher = if !self.inner.opts.ignore - || !ignore_files_list.is_none_or(|i| i.has_ignore) - { + let ig_matcher = if !self.0.opts.ignore { Gitignore::empty() } else { let (m, err) = create_gitignore( &dir, &dir, &[".ignore"], - self.inner.opts.ignore_case_insensitive, + self.0.opts.ignore_case_insensitive, ); errs.maybe_push(err); m }; - let gi_matcher = if !self.inner.opts.git_ignore - || !ignore_files_list.is_none_or(|i| i.has_git_ignore) - { + let gi_matcher = if !self.0.opts.git_ignore { Gitignore::empty() } else { let (m, err) = create_gitignore( &dir, &dir, &[".gitignore"], - self.inner.opts.ignore_case_insensitive, + self.0.opts.ignore_case_insensitive, ); errs.maybe_push(err); m }; - let gi_exclude_matcher = if !self.inner.opts.git_exclude - || !ignore_files_list.is_none_or(|i| i.has_git_dir) - { + let gi_exclude_matcher = if !self.0.opts.git_exclude { Gitignore::empty() } else { match resolve_git_commondir(dir, git_type) { @@ -429,7 +302,7 @@ impl Ignore { &dir, &git_dir, &["info/exclude"], - self.inner.opts.ignore_case_insensitive, + self.0.opts.ignore_case_insensitive, ); errs.maybe_push(err); m @@ -441,38 +314,32 @@ impl Ignore { } }; let ig = IgnoreInner { - compiled: self.inner.compiled.clone(), + compiled: self.0.compiled.clone(), dir: dir.to_path_buf(), - overrides: self.inner.overrides.clone(), - types: self.inner.types.clone(), - parent: Some(self.inner.clone()), + overrides: self.0.overrides.clone(), + types: self.0.types.clone(), + parent: Some(self.clone()), is_absolute_parent: false, - global_gitignores_relative_to: self - .inner - .global_gitignores_relative_to - .clone(), - explicit_ignores: self.inner.explicit_ignores.clone(), - custom_ignore_filenames: self - .inner - .custom_ignore_filenames - .clone(), + absolute_base: self.0.absolute_base.clone(), + global_gitignores_relative_to: self.0.global_gitignores_relative_to.clone(), + explicit_ignores: self.0.explicit_ignores.clone(), + custom_ignore_filenames: self.0.custom_ignore_filenames.clone(), custom_ignore_matcher: custom_ig_matcher, ignore_matcher: ig_matcher, - git_global_matcher: self.inner.git_global_matcher.clone(), + git_global_matcher: self.0.git_global_matcher.clone(), git_ignore_matcher: gi_matcher, git_exclude_matcher: gi_exclude_matcher, has_git, - opts: self.inner.opts, + opts: self.0.opts, }; (ig, errs.into_error_option()) } /// Returns true if at least one type of ignore rule should be matched. fn has_any_ignore_rules(&self) -> bool { - let opts = self.inner.opts; - let has_custom_ignore_files = - !self.inner.custom_ignore_filenames.is_empty(); - let has_explicit_ignores = !self.inner.explicit_ignores.is_empty(); + let opts = self.0.opts; + let has_custom_ignore_files = !self.0.custom_ignore_filenames.is_empty(); + let has_explicit_ignores = !self.0.explicit_ignores.is_empty(); opts.ignore || opts.git_global @@ -483,12 +350,9 @@ impl Ignore { } /// Like `matched`, but works with a directory entry instead. - pub(crate) fn matched_dir_entry<'a>( - &'a self, - dent: &DirEntry, - ) -> Match> { + pub(crate) fn matched_dir_entry<'a>(&'a self, dent: &DirEntry) -> Match> { let m = self.matched(dent.path(), dent.is_dir()); - if m.is_none() && self.inner.opts.hidden && is_hidden_entry(dent) { + if m.is_none() && self.0.opts.hidden && is_hidden(dent) { return Match::Ignore(IgnoreMatch::hidden()); } m @@ -498,11 +362,7 @@ impl Ignore { /// ignored or not. /// /// The match contains information about its origin. - pub(crate) fn matched<'a, P: AsRef>( - &'a self, - path: P, - is_dir: bool, - ) -> Match> { + fn matched<'a, P: AsRef>(&'a self, path: P, is_dir: bool) -> Match> { // We need to be careful with our path. If it has a leading ./, then // strip it because it causes nothing but trouble. let mut path = path.as_ref(); @@ -513,9 +373,9 @@ impl Ignore { // regardless of whether it's whitelist/ignore, then we quit and // return that result immediately. Overrides have the highest // precedence. - if !self.inner.overrides.is_empty() { + if !self.0.overrides.is_empty() { let mat = self - .inner + .0 .overrides .matched(path, is_dir) .map(IgnoreMatch::overrides); @@ -532,9 +392,8 @@ impl Ignore { whitelisted = mat; } } - if !self.inner.types.is_empty() { - let mat = - self.inner.types.matched(path, is_dir).map(IgnoreMatch::types); + if !self.0.types.is_empty() { + let mat = self.0.types.matched(path, is_dir).map(IgnoreMatch::types); if mat.is_ignore() { return mat; } else if mat.is_whitelist() { @@ -546,143 +405,98 @@ impl Ignore { /// Performs matching only on the ignore files for this directory and /// all parent directories. - pub(crate) fn matched_ignore<'a>( - &'a self, - path: &Path, - is_dir: bool, - ) -> Match> { - let ( - mut m_custom_ignore, - mut m_ignore, - mut m_gi, - mut m_gi_exclude, - mut m_explicit, - ) = (Match::None, Match::None, Match::None, Match::None, Match::None); - let any_git = !self.inner.opts.require_git - || self.parents().any(|ig| ig.inner.has_git); + fn matched_ignore<'a>(&'a self, path: &Path, is_dir: bool) -> Match> { + let (mut m_custom_ignore, mut m_ignore, mut m_gi, mut m_gi_exclude, mut m_explicit) = ( + Match::None, + Match::None, + Match::None, + Match::None, + Match::None, + ); + let any_git = !self.0.opts.require_git || self.parents().any(|ig| ig.0.has_git); let mut saw_git = false; - for ig in self.parents().take_while(|ig| !ig.inner.is_absolute_parent) - { + for ig in self.parents().take_while(|ig| !ig.0.is_absolute_parent) { if m_custom_ignore.is_none() { - m_custom_ignore = ig - .inner - .custom_ignore_matcher - .matched(path, is_dir) - .map(IgnoreMatch::gitignore); + m_custom_ignore = + ig.0.custom_ignore_matcher + .matched(path, is_dir) + .map(IgnoreMatch::gitignore); } if m_ignore.is_none() { - m_ignore = ig - .inner - .ignore_matcher - .matched(path, is_dir) - .map(IgnoreMatch::gitignore); + m_ignore = + ig.0.ignore_matcher + .matched(path, is_dir) + .map(IgnoreMatch::gitignore); } if any_git && !saw_git && m_gi.is_none() { - m_gi = ig - .inner - .git_ignore_matcher - .matched(path, is_dir) - .map(IgnoreMatch::gitignore); + m_gi = + ig.0.git_ignore_matcher + .matched(path, is_dir) + .map(IgnoreMatch::gitignore); } if any_git && !saw_git && m_gi_exclude.is_none() { - m_gi_exclude = ig - .inner - .git_exclude_matcher - .matched(path, is_dir) - .map(IgnoreMatch::gitignore); + m_gi_exclude = + ig.0.git_exclude_matcher + .matched(path, is_dir) + .map(IgnoreMatch::gitignore); } - saw_git = saw_git || ig.inner.has_git; + saw_git = saw_git || ig.0.has_git; } - if self.inner.opts.parents { - if let Some(abs_parent_path) = self.absolute_base() { - // What we want to do here is take the absolute base path of - // this directory and join it with the path we're searching. - // The main issue we want to avoid is accidentally duplicating - // directory components, so we try to strip any common prefix - // off of `path`. Overall, this seems a little ham-fisted, but - // it does fix a nasty bug. It should do fine until we overhaul - // this crate. - let path = abs_parent_path.join( - self.parents() - .take_while(|ig| !ig.inner.is_absolute_parent) - .last() - .map_or(path, |ig| { - // This is a weird special case when ripgrep users - // search with just a `.`, as some tools do - // automatically (like consult). In this case, if - // we don't bail out now, the code below will strip - // a leading `.` from `path`, which might mangle - // a hidden file name! - if ig.inner.dir.as_path() == Path::new(".") { - return path; - } - let without_dot_slash = strip_if_is_prefix( - "./", - ig.inner.dir.as_path(), - ); - let relative_base = - strip_if_is_prefix(without_dot_slash, path); - strip_if_is_prefix("/", relative_base) - }), - ); - - for ig in self - .parents() - .skip_while(|ig| !ig.inner.is_absolute_parent) - { + if self.0.opts.parents { + if let Some(_) = self.absolute_base() { + // CHANGED: We removed a code path that rewrote the `path` to be relative to + // `self.absolute_base()` because it assumed that the every path is inside the base + // which is not the case for us as we use `WalkBuilder#add` to add roots outside of the + // base. + for ig in self.parents().skip_while(|ig| !ig.0.is_absolute_parent) { if m_custom_ignore.is_none() { - m_custom_ignore = ig - .inner - .custom_ignore_matcher - .matched(&path, is_dir) - .map(IgnoreMatch::gitignore); + m_custom_ignore = + ig.0.custom_ignore_matcher + .matched(&path, is_dir) + .map(IgnoreMatch::gitignore); } if m_ignore.is_none() { - m_ignore = ig - .inner - .ignore_matcher - .matched(&path, is_dir) - .map(IgnoreMatch::gitignore); + m_ignore = + ig.0.ignore_matcher + .matched(&path, is_dir) + .map(IgnoreMatch::gitignore); } if any_git && !saw_git && m_gi.is_none() { - m_gi = ig - .inner - .git_ignore_matcher - .matched(&path, is_dir) - .map(IgnoreMatch::gitignore); + m_gi = + ig.0.git_ignore_matcher + .matched(&path, is_dir) + .map(IgnoreMatch::gitignore); } if any_git && !saw_git && m_gi_exclude.is_none() { - m_gi_exclude = ig - .inner - .git_exclude_matcher - .matched(&path, is_dir) - .map(IgnoreMatch::gitignore); + m_gi_exclude = + ig.0.git_exclude_matcher + .matched(&path, is_dir) + .map(IgnoreMatch::gitignore); } - saw_git = saw_git || ig.inner.has_git; + saw_git = saw_git || ig.0.has_git; } } } - for gi in self.inner.explicit_ignores.iter().rev() { - if !m_explicit.is_none() { - break; - } - // CHANGED: We need to make sure that the explicit gitignore rules - // apply to the path + for gi in self.0.explicit_ignores.iter().rev() { + // CHANGED: We need to make sure that the explicit gitignore rules apply to the path // - // path = Is the current file/folder we are traversing - // gi.path() = Is the path of the custom gitignore file + // path = Is the current file/folder we are traversing + // gi.path() = Is the path of the custom gitignore file + // + // E.g.: If we have a custom rule for `/src/utils` with `**/*`, and we are looking at + // just `/src`, then the `**/*` rules do not apply to this folder, so we can + // ignore the current custom gitignore file. // - // E.g.: If we have a custom rule for `/src/utils` with `**/*`, and - // we are looking at just `/src`, then the `**/*` rules do - // not apply to this folder, so we can ignore the current - // custom gitignore file. if !path.starts_with(gi.path()) { continue; } + if !m_explicit.is_none() { + break; + } m_explicit = gi.matched(&path, is_dir).map(IgnoreMatch::gitignore); } let m_global = if any_git { - self.inner + self.0 .git_global_matcher .matched(&path, is_dir) .map(IgnoreMatch::gitignore) @@ -690,90 +504,59 @@ impl Ignore { Match::None }; - // CHANGED: We added logic to configure an order in which the ignore - // files are respected. Explicitly added ignores (via - // `WalkBuilder::add_gitignore`) take precedence over all ignore files - // found on disk, and the first source with a definitive answer wins. + // CHANGED: We added logic to configure an order in which the ignore files are respected and + // allowed a whitelist in a later file to overrule a block on an earlier file. let order = [ // Manually added ignores - m_explicit, + &m_explicit, // .custom-ignore - m_custom_ignore, + &m_custom_ignore, // .ignore - m_ignore, + &m_ignore, // .gitignore - m_gi, + &m_gi, // .git/info/exclude - m_gi_exclude, + &m_gi_exclude, // Global gitignore - m_global, + &m_global, ]; + for check in order.into_iter() { - if !check.is_none() { - return check; + if check.is_none() { + continue; } + + return check.clone(); } - Match::None + + m_explicit } /// Returns an iterator over parent ignore matchers, including this one. pub(crate) fn parents(&self) -> Parents<'_> { - Parents(Some(IgnoreRef { inner: &self.inner })) + Parents(Some(self)) } /// Returns the first absolute path of the first absolute parent, if /// one exists. fn absolute_base(&self) -> Option<&Path> { - self.absolute_base.as_ref().map(|p| &***p) - } -} - -/// State for tracking what kinds of files ripgrep is interested in for a -/// given directory. -/// -/// This is computed over the entire set of files in a directory instead of -/// trying to stat each file individually. If a file is present, it's only then -/// that we stat it for more information, instead of relying on the stat to -/// determine its existence. -#[derive(Debug)] -struct IgnoreFilesFound { - has_ignore: bool, - has_git_ignore: bool, - has_git_dir: bool, - has_jj_dir: bool, - custom_ignore_files: Vec, -} - -#[derive(Clone, Copy)] -pub(crate) struct IgnoreRef<'a> { - inner: &'a IgnoreInner, -} - -impl IgnoreRef<'_> { - pub(crate) fn path(&self) -> &Path { - &self.inner.dir - } - - pub(crate) fn is_absolute_parent(&self) -> bool { - self.inner.is_absolute_parent + self.0.absolute_base.as_ref().map(|p| &***p) } } /// An iterator over all parents of an ignore matcher, including itself. -pub(crate) struct Parents<'a>(Option>); +/// +/// The lifetime `'a` refers to the lifetime of the initial `Ignore` matcher. +pub(crate) struct Parents<'a>(Option<&'a Ignore>); impl<'a> Iterator for Parents<'a> { - type Item = IgnoreRef<'a>; + type Item = &'a Ignore; - fn next(&mut self) -> Option> { + fn next(&mut self) -> Option<&'a Ignore> { match self.0.take() { None => None, Some(ig) => { - self.0 = ig - .inner - .parent - .as_deref() - .map(|inner| IgnoreRef { inner }); + self.0 = ig.0.parent.as_ref(); Some(ig) } } @@ -862,42 +645,33 @@ impl IgnoreBuilder { } gi } else { - log::debug!( - "ignoring global gitignore file because CWD is not known" - ); + log::debug!("ignoring global gitignore file because CWD is not known"); Gitignore::empty() }; - Ignore { - inner: Arc::new(IgnoreInner { - compiled: Arc::new(RwLock::new(HashMap::new())), - dir: self.dir.clone(), - overrides: self.overrides.clone(), - types: self.types.clone(), - parent: None, - is_absolute_parent: true, - global_gitignores_relative_to, - explicit_ignores: Arc::new(self.explicit_ignores.clone()), - custom_ignore_filenames: Arc::new( - self.custom_ignore_filenames.clone(), - ), - custom_ignore_matcher: Gitignore::empty(), - ignore_matcher: Gitignore::empty(), - git_global_matcher: Arc::new(git_global_matcher), - git_ignore_matcher: Gitignore::empty(), - git_exclude_matcher: Gitignore::empty(), - has_git: false, - opts: self.opts, - }), + Ignore(Arc::new(IgnoreInner { + compiled: Arc::new(RwLock::new(HashMap::new())), + dir: self.dir.clone(), + overrides: self.overrides.clone(), + types: self.types.clone(), + parent: None, + is_absolute_parent: true, absolute_base: None, - } + global_gitignores_relative_to, + explicit_ignores: Arc::new(self.explicit_ignores.clone()), + custom_ignore_filenames: Arc::new(self.custom_ignore_filenames.clone()), + custom_ignore_matcher: Gitignore::empty(), + ignore_matcher: Gitignore::empty(), + git_global_matcher: Arc::new(git_global_matcher), + git_ignore_matcher: Gitignore::empty(), + git_exclude_matcher: Gitignore::empty(), + has_git: false, + opts: self.opts, + })) } /// Set the current directory used for matching global gitignores. - pub(crate) fn current_dir( - &mut self, - cwd: impl Into, - ) -> &mut IgnoreBuilder { + pub(crate) fn current_dir(&mut self, cwd: impl Into) -> &mut IgnoreBuilder { self.global_gitignores_relative_to = Some(cwd.into()); self } @@ -907,10 +681,7 @@ impl IgnoreBuilder { /// By default, no override matcher is used. /// /// This overrides any previous setting. - pub(crate) fn overrides( - &mut self, - overrides: Override, - ) -> &mut IgnoreBuilder { + pub(crate) fn overrides(&mut self, overrides: Override) -> &mut IgnoreBuilder { self.overrides = Arc::new(overrides); self } @@ -941,7 +712,8 @@ impl IgnoreBuilder { &mut self, file_name: S, ) -> &mut IgnoreBuilder { - self.custom_ignore_filenames.push(file_name.as_ref().to_os_string()); + self.custom_ignore_filenames + .push(file_name.as_ref().to_os_string()); self } @@ -953,11 +725,6 @@ impl IgnoreBuilder { self } - /// Whether ignoring hidden files is enabled or not. - pub(crate) fn is_hidden(&self) -> bool { - self.opts.hidden - } - /// Enables reading `.ignore` files. /// /// `.ignore` files have the same semantics as `gitignore` files and are @@ -1028,10 +795,7 @@ impl IgnoreBuilder { /// Process ignore files case insensitively /// /// This is disabled by default. - pub(crate) fn ignore_case_insensitive( - &mut self, - yes: bool, - ) -> &mut IgnoreBuilder { + pub(crate) fn ignore_case_insensitive(&mut self, yes: bool) -> &mut IgnoreBuilder { self.opts.ignore_case_insensitive = yes; self } @@ -1090,10 +854,7 @@ pub(crate) fn create_gitignore>( /// them when multiple repositories are searched. /// /// Some I/O errors are ignored. -fn resolve_git_commondir( - dir: &Path, - git_type: Option, -) -> Result> { +fn resolve_git_commondir(dir: &Path, git_type: Option) -> Result> { let git_dir_path = || dir.join(".git"); let git_dir = git_dir_path(); if !git_type.map_or(false, |ft| ft.is_file()) { @@ -1138,20 +899,15 @@ fn resolve_git_commondir( /// Strips `prefix` from `path` if it's a prefix, otherwise returns `path` /// unchanged. -fn strip_if_is_prefix<'a, P: AsRef + ?Sized>( - prefix: &'a P, - path: &'a Path, -) -> &'a Path { +fn strip_if_is_prefix<'a, P: AsRef + ?Sized>(prefix: &'a P, path: &'a Path) -> &'a Path { strip_prefix(prefix, path).map_or(path, |p| p) } #[cfg(test)] mod tests { - use std::{io::Write, path::Path, sync::Arc}; + use std::{io::Write, path::Path}; - use crate::{ - Error, dir::IgnoreBuilder, gitignore::Gitignore, tests::TempDir, - }; + use crate::{Error, dir::IgnoreBuilder, gitignore::Gitignore, tests::TempDir}; fn wfile>(path: P, contents: &str) { let mut file = std::fs::File::create(path).unwrap(); @@ -1180,11 +936,11 @@ mod tests { let (gi, err) = Gitignore::new(td.path().join("not-an-ignore")); assert!(err.is_none()); - let (ig, err) = - IgnoreBuilder::new().add_ignore(gi).build().add_child(td.path()); + let (ig, err) = IgnoreBuilder::new() + .add_ignore(gi) + .build() + .add_child(td.path()); assert!(err.is_none()); - // CHANGED: Explicit ignores only apply to paths inside the directory - // of the ignore file, so we have to match against full paths. assert!(ig.matched(td.path().join("foo"), false).is_ignore()); assert!(ig.matched(td.path().join("bar"), false).is_whitelist()); assert!(ig.matched(td.path().join("baz"), false).is_none()); @@ -1455,152 +1211,15 @@ mod tests { let (ig2, err) = ig1.add_child("src"); assert!(err.is_none()); - assert!(ig1.matched("llvm", true).is_none()); - assert!(ig2.matched("llvm", true).is_none()); + // CHANGED: These test cases do not make sense for us as we never call the Ignore with + // relative paths. + assert!(ig1.matched("llvm", true).is_ignore()); + assert!(ig2.matched("llvm", true).is_ignore()); assert!(ig2.matched("src/llvm", true).is_none()); assert!(ig2.matched("foo", false).is_ignore()); assert!(ig2.matched("src/foo", false).is_ignore()); } - #[test] - fn absolute_parent_matchers_are_cached_across_roots() { - let td = tmpdir(); - mkdirp(td.path().join(".git")); - mkdirp(td.path().join("src/build")); - mkdirp(td.path().join("tests/build")); - wfile(td.path().join(".gitignore"), "tests/**/build/\n"); - - let ig0 = IgnoreBuilder::new().build(); - let (src_parents, err) = ig0.add_parents(td.path().join("src")); - assert!(err.is_none()); - let (src, err) = src_parents.add_child(td.path().join("src")); - assert!(err.is_none()); - let (tests_parents, err) = ig0.add_parents(td.path().join("tests")); - assert!(err.is_none()); - let (tests, err) = tests_parents.add_child(td.path().join("tests")); - assert!(err.is_none()); - - assert!(Arc::ptr_eq(&src_parents.inner, &tests_parents.inner)); - assert!(src.matched("build", true).is_none()); - assert!(tests.matched("build", true).is_ignore()); - } - - /// Parent matchers are shared across search roots, but path rewriting for - /// absolute parents must use each root's own base path. Otherwise a rule - /// like `src/invalid` is matched against the wrong absolute path when - /// `src` is searched before a sibling root (e.g. `tests`). - /// - /// Paths passed to `matched` use the same relative layout as `Walk` when - /// roots are given as relative directory names. - /// - /// Regression for: https://github.com/BurntSushi/ripgrep/issues/3376 - /// and https://github.com/BurntSushi/ripgrep/issues/3419 - #[test] - fn multi_root_gitignore_order_independent() { - let td = tmpdir(); - let cwd = std::env::current_dir().unwrap(); - // Use paths relative to CWD like the CLI walk does for `rg pat src tests`. - let root = td.path().strip_prefix(&cwd).unwrap_or(td.path()); - let src_root = root.join("src"); - let tests_root = root.join("tests"); - - mkdirp(td.path().join(".git")); - mkdirp(td.path().join("src")); - mkdirp(td.path().join("tests")); - wfile(td.path().join(".gitignore"), "src/invalid\n"); - wfile(td.path().join("src/invalid"), "x"); - wfile(td.path().join("src/valid"), "x"); - wfile(td.path().join("tests/valid"), "x"); - - let ig0 = IgnoreBuilder::new().build(); - - // Historically buggy order: search `src` first, then `tests`. - let (src_parents, err) = ig0.add_parents(&src_root); - assert!(err.is_none()); - let (src, err) = src_parents.add_child(&src_root); - assert!(err.is_none()); - let (tests_parents, err) = ig0.add_parents(&tests_root); - assert!(err.is_none()); - let (tests, err) = tests_parents.add_child(&tests_root); - assert!(err.is_none()); - - assert!(Arc::ptr_eq(&src_parents.inner, &tests_parents.inner)); - // Each root must carry its own absolute_base even though inners are shared. - assert_ne!( - src.absolute_base.as_ref().unwrap().as_path(), - tests.absolute_base.as_ref().unwrap().as_path() - ); - assert!( - src.matched(src_root.join("invalid"), false).is_ignore(), - "parent .gitignore must apply for the src root even when \ - another root was prepared in the same process" - ); - assert!(src.matched(src_root.join("valid"), false).is_none()); - assert!(tests.matched(tests_root.join("valid"), false).is_none()); - - // Reverse order should behave the same way. - let ig0 = IgnoreBuilder::new().build(); - let (tests_parents, err) = ig0.add_parents(&tests_root); - assert!(err.is_none()); - let (tests, err) = tests_parents.add_child(&tests_root); - assert!(err.is_none()); - let (src_parents, err) = ig0.add_parents(&src_root); - assert!(err.is_none()); - let (src, err) = src_parents.add_child(&src_root); - assert!(err.is_none()); - - assert!(src.matched(src_root.join("invalid"), false).is_ignore()); - assert!(src.matched(src_root.join("valid"), false).is_none()); - assert!(tests.matched(tests_root.join("valid"), false).is_none()); - } - - /// Same multi-root / order issue for non-git ignore files (e.g. `.rgignore` - /// via custom ignore names). - /// - /// Regression for: https://github.com/BurntSushi/ripgrep/issues/3320 - #[test] - fn multi_root_custom_ignore_order_independent() { - let td = tmpdir(); - let cwd = std::env::current_dir().unwrap(); - let root = td.path().strip_prefix(&cwd).unwrap_or(td.path()); - let alpha_root = root.join("alpha"); - let beta_root = root.join("beta"); - - mkdirp(td.path().join("alpha")); - mkdirp(td.path().join("beta")); - wfile(td.path().join(".rgignore"), "beta/**/*.svg\n"); - wfile(td.path().join("alpha/a.txt"), "x"); - wfile(td.path().join("beta/x.svg"), "x"); - - let ig0 = IgnoreBuilder::new() - .add_custom_ignore_filename(".rgignore") - .ignore(false) - .git_ignore(false) - .git_global(false) - .git_exclude(false) - .build(); - - let (alpha_parents, err) = ig0.add_parents(&alpha_root); - assert!(err.is_none()); - let (alpha, err) = alpha_parents.add_child(&alpha_root); - assert!(err.is_none()); - let (beta_parents, err) = ig0.add_parents(&beta_root); - assert!(err.is_none()); - let (beta, err) = beta_parents.add_child(&beta_root); - assert!(err.is_none()); - - assert_ne!( - alpha.absolute_base.as_ref().unwrap().as_path(), - beta.absolute_base.as_ref().unwrap().as_path() - ); - assert!(alpha.matched(alpha_root.join("a.txt"), false).is_none()); - assert!( - beta.matched(beta_root.join("x.svg"), false).is_ignore(), - "parent .rgignore must apply for the beta root regardless of \ - which root was set up first" - ); - } - #[test] fn git_info_exclude_in_linked_worktree() { let td = tmpdir(); @@ -1608,14 +1227,16 @@ mod tests { mkdirp(git_dir.join("info")); wfile(git_dir.join("info/exclude"), "ignore_me"); mkdirp(git_dir.join("worktrees/linked-worktree")); - let commondir_path = - || git_dir.join("worktrees/linked-worktree/commondir"); + let commondir_path = || git_dir.join("worktrees/linked-worktree/commondir"); mkdirp(td.path().join("linked-worktree")); let worktree_git_dir_abs = format!( "gitdir: {}", git_dir.join("worktrees/linked-worktree").to_str().unwrap(), ); - wfile(td.path().join("linked-worktree/.git"), &worktree_git_dir_abs); + wfile( + td.path().join("linked-worktree/.git"), + &worktree_git_dir_abs, + ); // relative commondir wfile(commondir_path(), "../.."); diff --git a/crates/ignore/src/gitignore.rs b/crates/ignore/src/gitignore.rs index 8824139b5..f822d8390 100644 --- a/crates/ignore/src/gitignore.rs +++ b/crates/ignore/src/gitignore.rs @@ -102,9 +102,7 @@ impl Gitignore { /// /// Note that I/O errors are ignored. For more granular control over /// errors, use `GitignoreBuilder`. - pub fn new>( - gitignore_path: P, - ) -> (Gitignore, Option) { + pub fn new>(gitignore_path: P) -> (Gitignore, Option) { let path = gitignore_path.as_ref(); let parent = path.parent().unwrap_or(Path::new("/")); let mut builder = GitignoreBuilder::new(parent); @@ -125,17 +123,6 @@ impl Gitignore { /// The global config file path is specified by git's `core.excludesFile` /// config option. /// - /// # Behavior - /// - /// This routine does its best to discover any global git exclude files. - /// This will try to parse out the `excludesFile` config option in your - /// global git configuration, if necessary. - /// - /// The specific things this routine tries (which are subject to change - /// based on how git behaves) are: - /// - /// - /// /// Git's config file location is `$HOME/.gitconfig`. If `$HOME/.gitconfig` /// does not exist or does not specify `core.excludesFile`, then /// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not @@ -158,8 +145,7 @@ impl Gitignore { num_ignores: 0, num_whitelists: 0, matches: None, - // CHANGED: Add a flag to have Gitignore rules that apply only to - // files. + // CHANGED: Add a flag to have Gitignore rules that apply only to files. only_on_files: false, } } @@ -204,11 +190,7 @@ impl Gitignore { /// determined by a common suffix of the directory containing this /// gitignore) is stripped. If there is no common suffix/prefix overlap, /// then `path` is assumed to be relative to this matcher. - pub fn matched>( - &self, - path: P, - is_dir: bool, - ) -> Match<&Glob> { + pub fn matched>(&self, path: P, is_dir: bool) -> Match<&Glob> { if self.is_empty() { return Match::None; } @@ -261,16 +243,11 @@ impl Gitignore { } /// Like matched, but takes a path that has already been stripped. - fn matched_stripped>( - &self, - path: P, - is_dir: bool, - ) -> Match<&Glob> { + fn matched_stripped>(&self, path: P, is_dir: bool) -> Match<&Glob> { if self.is_empty() { return Match::None; } - // CHANGED: Rules marked as only_on_files can not match against - // directories. + // CHANGED: Rules marked as only_on_files can not match against directories. if self.only_on_files && is_dir { return Match::None; } @@ -293,10 +270,7 @@ impl Gitignore { /// Strips the given path such that it's suitable for matching with this /// gitignore matcher. - fn strip<'a, P: 'a + AsRef + ?Sized>( - &'a self, - path: &'a P, - ) -> &'a Path { + fn strip<'a, P: 'a + AsRef + ?Sized>(&'a self, path: &'a P) -> &'a Path { let mut path = path.as_ref(); // A leading ./ is completely superfluous. We also strip it from // our gitignore root path, so we need to strip it from our candidate @@ -352,8 +326,7 @@ impl GitignoreBuilder { globs: vec![], case_insensitive: false, allow_unclosed_class: true, - // CHANGED: Add a flag to have Gitignore rules that apply only to - // files. + // CHANGED: Add a flag to have Gitignore rules that apply only to files. only_on_files: false, } } @@ -364,21 +337,18 @@ impl GitignoreBuilder { pub fn build(&self) -> Result { let nignore = self.globs.iter().filter(|g| !g.is_whitelist()).count(); let nwhite = self.globs.iter().filter(|g| g.is_whitelist()).count(); - let set = self - .builder - .build() - .map_err(|err| Error::Glob { glob: None, err: err.to_string() })?; + let set = self.builder.build().map_err(|err| Error::Glob { + glob: None, + err: err.to_string(), + })?; Ok(Gitignore { set, root: self.root.clone(), globs: self.globs.clone(), num_ignores: nignore as u64, num_whitelists: nwhite as u64, - matches: Some(Arc::new( - Pool::with_available_parallelism_capacity(|| vec![]), - )), - // CHANGED: Add a flag to have Gitignore rules that apply only to - // files. + matches: Some(Arc::new(Pool::new(|| vec![]))), + // CHANGED: Add a flag to have Gitignore rules that apply only to files. only_on_files: self.only_on_files, }) } @@ -441,8 +411,11 @@ impl GitignoreBuilder { // Match Git's handling of .gitignore files that begin with the Unicode BOM const UTF8_BOM: &str = "\u{feff}"; - let line = - if i == 0 { line.trim_start_matches(UTF8_BOM) } else { &line }; + let line = if i == 0 { + line.trim_start_matches(UTF8_BOM) + } else { + &line + }; if let Err(err) = self.add_line(Some(path.to_path_buf()), &line) { errs.push(err.tagged(path, lineno)); @@ -564,10 +537,7 @@ impl GitignoreBuilder { /// affected. /// /// This is disabled by default. - pub fn case_insensitive( - &mut self, - yes: bool, - ) -> Result<&mut GitignoreBuilder, Error> { + pub fn case_insensitive(&mut self, yes: bool) -> Result<&mut GitignoreBuilder, Error> { // TODO: This should not return a `Result`. Fix this in the next semver // release. self.case_insensitive = yes; @@ -586,10 +556,7 @@ impl GitignoreBuilder { /// modes since the glob parser becomes more permissive. You might want to /// enable this when compatibility (e.g., with POSIX glob implementations) /// is more important than good error messages. - pub fn allow_unclosed_class( - &mut self, - yes: bool, - ) -> &mut GitignoreBuilder { + pub fn allow_unclosed_class(&mut self, yes: bool) -> &mut GitignoreBuilder { self.allow_unclosed_class = yes; self } @@ -609,56 +576,32 @@ impl GitignoreBuilder { /// /// Note that the file path returned may not exist. pub fn gitconfig_excludes_path() -> Option { - // When GIT_CONFIG_GLOBAL is set, it replaces both $HOME/.gitconfig and - // $XDG_CONFIG_HOME/git/config (per git 2.32+). Otherwise, git supports - // $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config simultaneously, where - // $HOME/.gitconfig takes precedent. - gitconfig_global_env_contents() - .and_then(|x| parse_excludes_file(&x)) - .or_else(|| { - gitconfig_home_contents().and_then(|x| parse_excludes_file(&x)) - }) - .or_else(|| { - gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x)) - }) - // System-level config has the lowest priority for core.excludesFile. - // GIT_CONFIG_SYSTEM overrides the default /etc/gitconfig path. - .or_else(|| { - gitconfig_system_contents().and_then(|x| parse_excludes_file(&x)) - }) - .or_else(excludes_file_default) -} - -/// Returns the file contents of git's global config file from the path -/// specified by the `GIT_CONFIG_GLOBAL` environment variable. -fn gitconfig_global_env_contents() -> Option> { - let path = std::env::var_os("GIT_CONFIG_GLOBAL").map(PathBuf::from)?; - if path.as_os_str().is_empty() { - return None; + // git supports $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config. Notably, + // both can be active at the same time, where $HOME/.gitconfig takes + // precedent. So if $HOME/.gitconfig defines a `core.excludesFile`, then + // we're done. + match gitconfig_home_contents().and_then(|x| parse_excludes_file(&x)) { + Some(path) => return Some(path), + None => {} } - let mut file = BufReader::new(File::open(path).ok()?); - let mut contents = vec![]; - file.read_to_end(&mut contents).ok().map(|_| contents) -} - -/// Returns the file contents of git's system-level config file. -/// -/// Checks `GIT_CONFIG_SYSTEM` first, then falls back to `/etc/gitconfig`. -fn gitconfig_system_contents() -> Option> { - let path = std::env::var_os("GIT_CONFIG_SYSTEM") - .map(PathBuf::from) - .filter(|x| !x.as_os_str().is_empty()) - .unwrap_or_else(|| PathBuf::from("/etc/gitconfig")); - let mut file = BufReader::new(File::open(path).ok()?); - let mut contents = vec![]; - file.read_to_end(&mut contents).ok().map(|_| contents) + match gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x)) { + Some(path) => return Some(path), + None => {} + } + excludes_file_default() } /// Returns the file contents of git's global config file, if one exists, in /// the user's home directory. fn gitconfig_home_contents() -> Option> { - let home = home_dir()?; - let mut file = BufReader::new(File::open(home.join(".gitconfig")).ok()?); + let home = match home_dir() { + None => return None, + Some(home) => home, + }; + let mut file = match File::open(home.join(".gitconfig")) { + Err(_) => return None, + Ok(file) => BufReader::new(file), + }; let mut contents = vec![]; file.read_to_end(&mut contents).ok().map(|_| contents) } @@ -667,11 +610,19 @@ fn gitconfig_home_contents() -> Option> { /// the user's XDG_CONFIG_HOME directory. fn gitconfig_xdg_contents() -> Option> { let path = std::env::var_os("XDG_CONFIG_HOME") - .map(PathBuf::from) - .filter(|x| !x.as_os_str().is_empty()) + .and_then(|x| { + if x.is_empty() { + None + } else { + Some(PathBuf::from(x)) + } + }) .or_else(|| home_dir().map(|p| p.join(".config"))) - .map(|x| x.join("git/config"))?; - let mut file = BufReader::new(File::open(path).ok()?); + .map(|x| x.join("git/config")); + let mut file = match path.and_then(|p| File::open(p).ok()) { + None => return None, + Some(file) => BufReader::new(file), + }; let mut contents = vec![]; file.read_to_end(&mut contents).ok().map(|_| contents) } @@ -681,8 +632,13 @@ fn gitconfig_xdg_contents() -> Option> { /// Specifically, this respects XDG_CONFIG_HOME. fn excludes_file_default() -> Option { std::env::var_os("XDG_CONFIG_HOME") - .map(PathBuf::from) - .filter(|x| !x.as_os_str().is_empty()) + .and_then(|x| { + if x.is_empty() { + None + } else { + Some(PathBuf::from(x)) + } + }) .or_else(|| home_dir().map(|p| p.join(".config"))) .map(|x| x.join("git/ignore")) } @@ -711,7 +667,9 @@ fn parse_excludes_file(data: &[u8]) -> Option { re.captures(data, &mut caps); let span = caps.get_group(1)?; let candidate = &data[span]; - std::str::from_utf8(candidate).ok().map(|s| PathBuf::from(expand_tilde(s))) + std::str::from_utf8(candidate) + .ok() + .map(|s| PathBuf::from(expand_tilde(s))) } /// Expands ~ in file paths to the value of $HOME. @@ -873,10 +831,7 @@ mod tests { fn parse_excludes_file4() { let data = bytes("[core]\nexcludesFile = \"~/foo/bar\""); let got = super::parse_excludes_file(&data); - assert_eq!( - path_string(got.unwrap()), - super::expand_tilde("~/foo/bar") - ); + assert_eq!(path_string(got.unwrap()), super::expand_tilde("~/foo/bar")); } #[test] diff --git a/crates/ignore/src/incremental.rs b/crates/ignore/src/incremental.rs deleted file mode 100644 index 5030b0d00..000000000 --- a/crates/ignore/src/incremental.rs +++ /dev/null @@ -1,1286 +0,0 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - sync::OnceLock, -}; - -use crate::{ - Error, Match, PartialErrorBuilder, dir::Ignore, pathutil::is_hidden_path, -}; - -/// A cached matcher for checking paths against hierarchical ignore files. -/// -/// An `IncrementalIgnore` is built from a [`crate::WalkBuilder`]. Unlike a -/// recursive walk, it can check individual paths while still respecting the -/// ignore files in every relevant parent directory. Matchers for directories -/// are compiled on first use and then retained for later queries. -/// Each matcher corresponds to exactly one root configured on the builder, -/// and paths passed to it are interpreted relative to that root. -/// A matcher for the special `-` root representing standard input is inert -/// and always returns a non-match. -/// -/// The matcher checks path-based filters in the same precedence order as -/// a traversal. This includes glob overrides, `.ignore`, `.gitignore`, -/// `.git/info/exclude`, global and explicitly added ignore files, custom -/// ignore file names and file type selections. It does not apply filters that -/// require a directory entry or other traversal state, such as custom entry -/// predicates. Hidden-file detection, minimum and maximum depth limits and the -/// maximum file size are applied. -/// -/// A matcher is a snapshot at directory granularity. Once the ignore files in -/// a directory have been loaded, edits to those files are not observed. Build -/// a new matcher to reload them. -/// -/// # Warning -/// -/// The incremental path checking here necessarily needs to do a lot more work -/// per path matched. Callers should _not_ use this to run directory traversal. -/// This is intended to avoid the work of re-traversing an entire directory -/// tree when only a few changes are detected. (For example, in response to -/// file additions or deletions.) -/// -/// # Example -/// -/// ```rust,no_run -/// use ignore::WalkBuilder; -/// -/// let mut builder = WalkBuilder::new("."); -/// builder.add_custom_ignore_filename(".rgignore"); -/// let mut matchers = builder.build_matchers(); -/// let matcher = &mut matchers[0]; -/// -/// if matcher.matched("src/generated.rs", false).is_ignore() { -/// println!("ignored"); -/// } -/// ``` -#[derive(Clone, Debug)] -pub struct IncrementalIgnore { - /// The root exactly as it was given to `WalkBuilder`. - root: PathBuf, - /// The normalized root used only by the opt-in normalization routine. - normalized_root: OnceLock>, - /// The matcher for the configured root directory, loaded on first use. - ignore: RootIgnore, - /// Directory paths relative to `root`, excluding the root itself. - dirs: HashMap, - /// Options for additional filtering beyond gitignore. - options: IncrementalIgnoreOptions, -} - -/// The options for a matcher, mostly meant to duplicate as much as we can from -/// `WalkParallel`. -#[derive(Clone, Debug)] -pub(crate) struct IncrementalIgnoreOptions { - pub(crate) min_depth: Option, - pub(crate) max_depth: Option, - pub(crate) max_filesize: Option, - pub(crate) hidden: bool, - pub(crate) follow_links: bool, -} - -#[derive(Clone, Debug)] -enum RootIgnore { - Unloaded(Ignore), - Loaded(Ignore), - NotDirectory, - Stdin, -} - -/// Cached traversal state for a directory relative to the configured root. -/// -/// The presence of an entry means that the directory and every ancestor -/// between it and the root have already been checked. -#[derive(Clone, Debug)] -enum CachedDir { - /// The directory may be descended into. The matcher includes the ignore - /// rules loaded through this directory and is therefore the matcher to use - /// for its children. - Allowed(Ignore), - /// The directory may not be descended into because it was ignored by a - /// path rule or hidden-file filtering. Every descendant is consequently - /// ignored, and ignore files inside this directory are not loaded. - Ignored, -} - -impl IncrementalIgnore { - pub(crate) fn new( - root: PathBuf, - ignore: Ignore, - options: IncrementalIgnoreOptions, - ) -> IncrementalIgnore { - // File traversal special cases `-` to search stdin, so we recognize - // it here for completeness too. In particular, we really want - // `WalkBuilder::build_matchers` to return a matcher for every root, - // even when it's a simple file (handled automatically) or when it's - // stdin (necessarily special cased). - // - // If callers need to search a file or directory named `-`, then they - // can use `./-`. As is the case for file traversal too. - let ignore = if root == Path::new("-") { - RootIgnore::Stdin - } else { - RootIgnore::Unloaded(ignore) - }; - IncrementalIgnore { - root, - normalized_root: OnceLock::new(), - ignore, - dirs: HashMap::new(), - options, - } - } - - /// Return the root that paths matched by this matcher are relative to. - pub fn root(&self) -> &Path { - &self.root - } - - /// Normalize `path` and return it relative to this matcher's root. - /// - /// This returns `None` when `path` cannot be made absolute or - /// when it is known to be outside this matcher's root. Unlike - /// [`IncrementalIgnore::matched`], this performs absolute path conversion, - /// lexical normalization and allocation. It is intended as an opt-in - /// convenience for callers that do not already have root-relative paths. - /// - /// Note that `.` is interpreted relative to the process level current - /// working directory. It is _not_ interpreted relative to the root of - /// this matcher. - /// - /// Note also that this may reject paths that only differ in casing. For - /// example, if the root path for this matcher is `/FOO` but the provided - /// path is `/foo/bar`, then this may return `None`. Callers must ensure - /// casing is consistent between the path provided and the root path for - /// this matcher. - pub fn normalize>(&self, path: P) -> Option { - if matches!(self.ignore, RootIgnore::Stdin) { - return None; - } - let path = normalize_absolute(path.as_ref())?; - let root = self - .normalized_root - .get_or_init(|| normalize_absolute(&self.root)) - .as_ref()?; - path.strip_prefix(root).ok().map(Path::to_path_buf) - } - - /// Match a root-relative path against ignore files in its directory and - /// all relevant parent directories. - /// - /// `is_dir` should be true when `path` should be matched as a directory. - /// - /// For the return value, use [`IncrementalMatch::is_ignore`], - /// [`IncrementalMatch::is_whitelist`] or [`IncrementalMatch::is_none`] to - /// inspect it. - /// - /// Matchers for previously unseen directories are loaded and cached during - /// this call. Errors encountered while loading ignore files are logged. To - /// receive those errors, use [`IncrementalIgnore::matched_with_errors`]. - /// - /// `path` must be relative to this matcher's root and must not contain a - /// parent directory (`..`) component. Behavior is unspecified when these - /// preconditions are violated. Callers with an absolute path or with a - /// path containing `.` or `..` may use [`IncrementalIgnore::normalize`] - /// to get a path satisfying these preconditions. In all cases, a relative - /// path is *assumed* to be relative to the root of this ignore matcher. - /// - /// In general, it is intended that callers doing recursive directory - /// traversal on the root of this matcher may provide relative paths to - /// this routine *without* calling [`IncrementalIgnore::normalize`]. - /// - /// The empty path represents the explicitly configured root and also - /// returns non-match, consistent with recursive traversal where a root is - /// always treated as being at depth zero. - pub fn matched>( - &mut self, - path: P, - is_dir: bool, - ) -> IncrementalMatch { - let (matched, err) = self.matched_with_errors(path, is_dir); - if let Some(err) = err { - log::debug!("error while loading ignore files: {err}"); - } - matched - } - - /// Match a root-relative path and return errors encountered while loading - /// ignore files. - /// - /// This is equivalent to [`IncrementalIgnore::matched`], except that - /// it returns any errors from newly loaded ignore files. Loading can - /// partially succeed, so valid rules are always applied to the returned - /// match even when an error is present. - pub fn matched_with_errors>( - &mut self, - path: P, - is_dir: bool, - ) -> (IncrementalMatch, Option) { - let mut errs = PartialErrorBuilder::default(); - let matched = - self.matched_with_errors_impl(path.as_ref(), is_dir, &mut errs); - (matched, errs.into_error_option()) - } - - fn matched_with_errors_impl( - &mut self, - relative: &Path, - is_dir: bool, - errs: &mut PartialErrorBuilder, - ) -> IncrementalMatch { - // We short-circuit here when our matcher corresponds to `Stdin` in - // order to always return a non-match. This is somewhat redundant with - // `root_ignore()` which does this too, but we do it here so that it - // always happens, e.g., before depth filtering. - if relative.is_absolute() || matches!(self.ignore, RootIgnore::Stdin) { - return IncrementalMatch::none(is_dir); - } - - let (mut satisfies_min, mut edge_max) = (true, false); - if self.options.min_depth.is_some() || self.options.max_depth.is_some() - { - let depth = relative - .components() - .filter_map(|component| match component { - std::path::Component::CurDir => None, - component => Some(component), - }) - .count(); - satisfies_min = - self.options.min_depth.is_none_or(|min| depth >= min); - let satisfies_max = - self.options.max_depth.is_none_or(|max| depth <= max); - edge_max = self.options.max_depth.is_some_and(|max| depth == max); - // When we have a file that isn't past our min depth, we can give - // up right away. - if !is_dir && !satisfies_min { - return IncrementalMatch::ignore().not_within_depth(); - } - // Same for *anything* that exceeds our max depth. - if !satisfies_max { - return IncrementalMatch::ignore().not_within_depth(); - } - } - - // When the path is invalid in some way, we bail out early to avoid - // potentially doing a stat call below. - let (mut mat, valid) = self - .matched_with_errors_ignore(relative, is_dir, errs) - .map(|mat| (mat, true)) - .unwrap_or_else(|| (IncrementalMatch::none(is_dir), false)); - - if !is_dir - && !mat.is_ignore() - && valid - && let Some(max_filesize) = self.options.max_filesize - { - let path = self.root().join(relative); - let result = if self.options.follow_links { - path.metadata() - } else { - path.symlink_metadata() - }; - match result { - Ok(md) if md.len() > max_filesize => { - return IncrementalMatch::ignore(); - } - Ok(_) => {} - Err(err) => { - // Record the error but otherwise fall through - let err = Error::from(err); - errs.push(err.with_path(path)); - } - } - } - - // We still need to tag our `mat` if it doesn't pass the depth filter, - // or if it's a directory on the edge of a depth filter. This can only - // happen when the path is reported as a directory. Otherwise regular - // files are always handled above. - if is_dir { - if !satisfies_min { - mat = mat.not_within_depth(); - } - if edge_max { - mat = mat.no_descent(); - } - } - mat - } - - fn matched_with_errors_ignore( - &mut self, - relative: &Path, - is_dir: bool, - errs: &mut PartialErrorBuilder, - ) -> Option { - let mut components = relative - .components() - .filter_map(|component| match component { - std::path::Component::CurDir => None, - component => Some(component), - }) - .peekable(); - components.peek()?; - - // If the exact parent is cached, then all of its ancestors have - // already been checked. An allowed cache entry is the matcher to use - // for children of that directory, while an ignored cache entry is - // terminal for every descendant. In the usual case, this avoids both - // walking every component and allocating a relative directory path. - // - // Only try the fast path when the final component is a normal path - // component. Invalid paths are handled by the component walk below. - let has_normal_final_component = - relative.components().next_back().is_some_and(|component| { - matches!(component, std::path::Component::Normal(_)) - }); - if has_normal_final_component && let Some(parent) = relative.parent() { - match self.dirs.get(parent) { - Some(CachedDir::Allowed(ignore)) => { - return Some(self.match_path(ignore, relative, is_dir)); - } - Some(CachedDir::Ignored) => { - return Some(IncrementalMatch::ignore()); - } - None => {} - } - } - - let mut ignore = self.root_ignore(errs)?; - let mut dir = PathBuf::new(); - while let Some(component) = components.next() { - match component { - std::path::Component::ParentDir - | std::path::Component::RootDir - | std::path::Component::Prefix(_) => { - return None; - } - std::path::Component::CurDir => continue, - std::path::Component::Normal(_) => {} - } - if components.peek().is_none() { - break; - } - dir.push(component.as_os_str()); - match self.dirs.get(&dir) { - Some(CachedDir::Allowed(cached)) => { - ignore = cached.clone(); - continue; - } - Some(CachedDir::Ignored) => { - return Some(IncrementalMatch::ignore()); - } - None => {} - } - - let path = self.root.join(&dir); - let mat = ignore.matched(&path, true); - let is_hidden = - self.options.hidden && mat.is_none() && is_hidden_path(&path); - if mat.is_ignore() || is_hidden { - self.dirs.insert(dir.clone(), CachedDir::Ignored); - return Some(IncrementalMatch::ignore()); - } - let (child, err) = ignore.add_child(&path); - errs.maybe_push(err); - self.dirs.insert(dir.clone(), CachedDir::Allowed(child.clone())); - ignore = child; - } - - Some(self.match_path(&ignore, relative, is_dir)) - } - - fn match_path( - &self, - ignore: &Ignore, - relative: &Path, - is_dir: bool, - ) -> IncrementalMatch { - let path = self.root.join(relative); - let mut mat = IncrementalMatch::from_match( - ignore.matched(&path, is_dir).map(|_| ()), - is_dir, - ); - // Whether a file is hidden or not has low precedence in filtering. We - // only check it if we haven't matched anything above. This permits - // callers to whitelist hidden files or directories. - if self.options.hidden && mat.is_none() && is_hidden_path(&path) { - mat = IncrementalMatch::ignore(); - } - mat - } - - fn root_ignore( - &mut self, - errs: &mut PartialErrorBuilder, - ) -> Option { - let ignore = match self.ignore { - RootIgnore::Unloaded(ref ignore) => ignore.clone(), - RootIgnore::Loaded(ref ignore) => return Some(ignore.clone()), - RootIgnore::NotDirectory | RootIgnore::Stdin => return None, - }; - if !self.root.is_dir() { - self.ignore = RootIgnore::NotDirectory; - return None; - } - - let (parents, err) = ignore.add_parents(&self.root); - errs.maybe_push(err); - let (root, err) = parents.add_child(&self.root); - errs.maybe_push(err); - self.ignore = RootIgnore::Loaded(root.clone()); - Some(root) - } -} - -/// The result of an incremental match. -/// -/// This is similar to [`Match`] in that it reports whether a file path should -/// be ignored, whitelisted or didn't match anything at all. It also has extra -/// data, such as whether a directory matched but should not be descended into. -/// -/// Generally speaking, callers that only care about specific files can stick -/// to the `is_none()`, `is_ignore()` or `is_whitelist()` predicates. Directory -/// entries are more complicated because we sometimes want to yield directories -/// to descend into, but not actually visit (e.g., for the minimum depth -/// filter). Or, we may want to yield a directory to visit but not descend into -/// (e.g., for the maximum depth filter). -#[derive(Clone, Debug)] -pub struct IncrementalMatch { - mat: Match<()>, - should_descend: bool, - is_within_depth: bool, -} - -impl IncrementalMatch { - fn none(is_dir: bool) -> IncrementalMatch { - IncrementalMatch { - mat: Match::None, - should_descend: is_dir, - is_within_depth: true, - } - } - - fn ignore() -> IncrementalMatch { - IncrementalMatch { - mat: Match::Ignore(()), - should_descend: false, - is_within_depth: true, - } - } - - fn from_match(mat: Match<()>, is_dir: bool) -> IncrementalMatch { - let should_descend = is_dir && !mat.is_ignore(); - IncrementalMatch { mat, should_descend, is_within_depth: true } - } - - fn no_descent(self) -> IncrementalMatch { - IncrementalMatch { should_descend: false, ..self } - } - - fn not_within_depth(self) -> IncrementalMatch { - IncrementalMatch { is_within_depth: false, ..self } - } - - /// Returns true if the match result didn't match anything. - pub fn is_none(&self) -> bool { - self.mat.is_none() - } - - /// Returns true if the match result implies the path should be ignored. - pub fn is_ignore(&self) -> bool { - self.mat.is_ignore() - } - - /// Returns true if the match result implies the path should be - /// whitelisted. - pub fn is_whitelist(&self) -> bool { - self.mat.is_whitelist() - } - - /// Returns true only when this match corresponds to a directory *and* - /// whether the caller should look inside this directory for additional - /// results. - /// - /// It is possible for a match to report `false` for - /// [`IncrementalMatch::is_ignore`] _and_ `false` for - /// [`IncrementalMatch::should_descend`]. This can occur, for example, when - /// a maximum depth setting allows a directory through, but where none of - /// its children entries should be visited. - /// - /// This is always `false` for a file path that does *not* correspond to a - /// directory. - pub fn should_descend(&self) -> bool { - self.should_descend - } - - /// Returns true only when this result corresponds to an entry that is - /// within the depth filter. - /// - /// This is `false` when a path corresponds to a directory and is less than - /// the minimum depth. In this case, callers should continue looking inside - /// that directory. - /// - /// This is always `true` for a match corresponding to a file path that - /// isn't a directory. - pub fn is_within_depth(&self) -> bool { - self.is_within_depth - } - - /// Inverts the match so that `Ignore` becomes `Whitelist` and - /// `Whitelist` becomes `Ignore`. A non-match remains the same. - pub fn invert(self) -> IncrementalMatch { - IncrementalMatch { mat: self.mat.invert(), ..self } - } -} - -/// Return a lexically normalized absolute representation of `path`. -/// -/// This collapses `.` and `..`, but intentionally does not canonicalize or -/// resolve symlinks. Ignore rules apply to the lexical path, and resolving a -/// symlink below a root could move the result outside of that root. -fn normalize_absolute(path: &Path) -> Option { - let absolute = std::path::absolute(path).ok()?; - let mut normalized = PathBuf::new(); - for component in absolute.components() { - match component { - std::path::Component::CurDir => {} - std::path::Component::ParentDir => { - normalized.pop(); - } - _ => normalized.push(component.as_os_str()), - } - } - Some(normalized) -} - -#[cfg(test)] -mod tests { - use std::{ - fs::{self, File}, - io::Write, - path::{Path, PathBuf}, - }; - - use crate::{ - IncrementalIgnore, IncrementalMatch, WalkBuilder, - overrides::OverrideBuilder, tests::TempDir, types::TypesBuilder, - }; - - use super::CachedDir; - - fn wfile>(path: P, contents: &str) { - let mut file = File::create(path).unwrap(); - file.write_all(contents.as_bytes()).unwrap(); - } - - fn mkdirp>(path: P) { - fs::create_dir_all(path).unwrap(); - } - - fn tmpdir() -> TempDir { - TempDir::new().unwrap() - } - - fn builder>(path: P) -> WalkBuilder { - let mut builder = WalkBuilder::new(path); - builder.git_global(false); - builder - } - - fn one_matcher(builder: &WalkBuilder) -> IncrementalIgnore { - let mut matchers = builder.build_matchers(); - assert_eq!(matchers.len(), 1); - matchers.pop().unwrap() - } - - fn builders, P: AsRef>( - paths: I, - ) -> WalkBuilder { - let mut builder = WalkBuilder::from_iter(paths); - builder.git_global(false); - builder - } - - fn matchers(builder: &WalkBuilder) -> Vec { - builder.build_matchers() - } - - fn matchedf>( - matcher: &mut IncrementalIgnore, - path: P, - ) -> IncrementalMatch { - let (matched, err) = matcher.matched_with_errors(path, false); - assert!(err.is_none(), "unexpected matcher error: {err:?}"); - matched - } - - fn matchedd>( - matcher: &mut IncrementalIgnore, - path: P, - ) -> IncrementalMatch { - let (matched, err) = matcher.matched_with_errors(path, true); - assert!(err.is_none(), "unexpected matcher error: {err:?}"); - matched - } - - // Test that multiple parent ignore files, when nested, are respected. - #[test] - fn nested_parent_gitignores() { - let td = tmpdir(); - let root = td.path().join("project/work"); - mkdirp(td.path().join(".git")); - mkdirp(root.join("src")); - wfile(td.path().join(".gitignore"), "*.tmp\n"); - wfile(td.path().join("project/.gitignore"), "!keep.tmp\nnested.log\n"); - - let mut m = one_matcher(&builder(&root)); - assert_eq!(m.root(), root); - assert!(matchedf(&mut m, "src/drop.tmp").is_ignore()); - assert!(matchedf(&mut m, "src/keep.tmp").is_whitelist()); - assert!(matchedf(&mut m, "src/nested.log").is_ignore()); - assert!(matchedf(&mut m, "src/ok.rs").is_none()); - } - - // Test that an anchored rule in a child directory is matched relative to - // that directory, not relative to the configured root. - #[test] - fn anchored_child_rule_uses_child_root() { - let td = tmpdir(); - let root = td.path().join("root"); - mkdirp(root.join("a")); - wfile(root.join("a/.ignore"), "/foo\n"); - - let mut m = one_matcher(&builder(&root)); - assert!(matchedf(&mut m, "a/foo").is_ignore()); - assert!(matchedf(&mut m, "a/b/foo").is_none()); - } - - // Test that a leading `./` impacts how the rules are matched. - #[cfg(not(windows))] - #[test] - fn leading_dot_slash_impacts_matching() { - let td = tmpdir(); - let root = td.path().join("root"); - mkdirp(root.join("a")); - wfile(root.join(".ignore"), "/foo\n"); - wfile(root.join("a/.ignore"), "/foo\n"); - - let mut m = one_matcher(&builder(&root)); - assert!(matchedf(&mut m, "foo").is_ignore()); - assert!(matchedf(&mut m, "./foo").is_none()); - assert!(matchedf(&mut m, "a/allowed").is_none()); - assert!(matchedf(&mut m, "a/foo").is_ignore()); - assert!(matchedf(&mut m, "./a/foo").is_none()); - } - - // Test that custom ignore files are respected. - #[test] - fn parent_ignore_and_custom_ignore() { - let td = tmpdir(); - let root = td.path().join("project/work"); - mkdirp(root.join("src")); - wfile(td.path().join(".ignore"), "*.cache\n"); - wfile(td.path().join(".rgignore"), "*.svg\n"); - wfile(td.path().join("project/.ignore"), "!keep.cache\n"); - wfile(td.path().join("project/.rgignore"), "!keep.svg\n"); - - let mut builder = builder(&root); - builder.add_custom_ignore_filename(".rgignore"); - let mut m = one_matcher(&builder); - assert!(matchedf(&mut m, "src/drop.cache").is_ignore()); - assert!(matchedf(&mut m, "src/keep.cache").is_whitelist()); - assert!(matchedf(&mut m, "src/drop.svg").is_ignore()); - assert!(matchedf(&mut m, "src/keep.svg").is_whitelist()); - } - - // Test that glob overrides take precedence over ignore files. - #[test] - fn glob_overrides_are_applied() { - let td = tmpdir(); - wfile(td.path().join(".ignore"), "keep.rs\n!drop.rs\n"); - - let mut overrides = OverrideBuilder::new(td.path()); - overrides.add("keep.rs").unwrap(); - overrides.add("!drop.rs").unwrap(); - let mut b = builder(td.path()); - b.overrides(overrides.build().unwrap()); - let mut m = one_matcher(&b); - - assert!(matchedf(&mut m, "keep.rs").is_whitelist()); - assert!(matchedf(&mut m, "drop.rs").is_ignore()); - } - - // Test that an override-ignored directory prevents matching rules below - // it, just as it prevents a traversal from descending into the directory. - #[test] - fn glob_overrides_apply_to_ancestors() { - let td = tmpdir(); - mkdirp(td.path().join("blocked")); - wfile(td.path().join("blocked/.ignore"), "!keep.rs\n"); - - let mut overrides = OverrideBuilder::new(td.path()); - overrides.add("!blocked/").unwrap(); - let mut b = builder(td.path()); - b.overrides(overrides.build().unwrap()); - let mut m = one_matcher(&b); - - assert!(matchedf(&mut m, "blocked/keep.rs").is_ignore()); - } - - // Test that file type selections are applied to files, but not - // directories. - #[test] - fn file_types_are_applied() { - let td = tmpdir(); - mkdirp(td.path().join("src")); - - let mut types = TypesBuilder::new(); - types.add("rust", "*.rs").unwrap(); - types.select("rust"); - let mut b = builder(td.path()); - b.types(types.build().unwrap()); - let mut m = one_matcher(&b); - - assert!(matchedd(&mut m, "src").is_none()); - assert!(matchedf(&mut m, "src/lib.rs").is_whitelist()); - assert!(matchedf(&mut m, "README.md").is_ignore()); - } - - #[test] - fn directly_ignored_directory_is_not_descended() { - let td = tmpdir(); - mkdirp(td.path().join(".git")); - mkdirp(td.path().join("blocked")); - wfile(td.path().join(".gitignore"), "blocked/\n"); - - let mut m = one_matcher(&builder(td.path())); - let dir = matchedd(&mut m, "blocked"); - assert!(dir.is_ignore()); - assert!(!dir.should_descend()); - } - - // Test that when a directory is ignored, anything below it always ignored - // even when there are explicit whitelist rules. This matches directory - // traversal semantics. - #[test] - fn ignored_ancestor_wins() { - let td = tmpdir(); - mkdirp(td.path().join(".git")); - mkdirp(td.path().join("blocked")); - mkdirp(td.path().join("open")); - // This is the key line: since the entire `blocked` directory is - // ignored, a proper file traversal won't ever descend into it. So - // `blocked/keep.rs` should be ignored even if there are ignore rules - // "beneath" it that whitelist it. - wfile(td.path().join(".gitignore"), "blocked/\n!blocked/keep.rs\n"); - wfile(td.path().join("blocked/.gitignore"), "!keep.rs\n"); - - let mut m = one_matcher(&builder(td.path())); - assert!(matchedf(&mut m, "blocked/keep.rs").is_ignore()); - assert!(matchedf(&mut m, "blocked/other.rs").is_ignore()); - assert!(matchedf(&mut m, "open/keep.rs").is_none()); - } - - // Test that we respect git boundaries. And that we don't respect git - // boundaries when not configured to do so. - #[test] - fn respects_git_repository_boundaries() { - let td = tmpdir(); - let root = td.path().join("repo/src"); - mkdirp(td.path().join("repo/.git")); - mkdirp(&root); - wfile(td.path().join(".gitignore"), "outside-rule\n"); - wfile(td.path().join(".ignore"), "tool-rule\n"); - wfile(td.path().join("repo/.gitignore"), "inside-rule\n"); - - let mut m = one_matcher(&builder(&root)); - assert!(matchedf(&mut m, "inside-rule").is_ignore()); - assert!(matchedf(&mut m, "outside-rule").is_none()); - assert!(matchedf(&mut m, "tool-rule").is_ignore()); - - let mut no_git_required = builder(&root); - no_git_required.require_git(false); - let mut m = one_matcher(&no_git_required); - assert!(matchedf(&mut m, "outside-rule").is_ignore()); - } - - // Test that when a gitignore matcher in a parent directory is created, we - // reuse that matcher from memory even if it's changed on disk. - #[test] - fn compiled_matchers_are_reused() { - let td = tmpdir(); - mkdirp(td.path().join("a")); - wfile(td.path().join("a/.ignore"), "*.tmp\n"); - - let mut m = one_matcher(&builder(td.path())); - assert!(matchedf(&mut m, "a/first.tmp").is_ignore()); - - // Below demonstrates that this new ignore file contents - // aren't actually picked up because it was already loaded. - wfile(td.path().join("a/.ignore"), "!*.tmp\n*.rs\n"); - assert!(matchedf(&mut m, "a/first.tmp").is_ignore()); - assert!(matchedf(&mut m, "a/second.tmp").is_ignore()); - assert!(matchedf(&mut m, "a/keep.rs").is_none()); - // To get the new ignore file, we need to rebuild the matcher. - let mut m = one_matcher(&builder(td.path())); - assert!(matchedf(&mut m, "a/first.tmp").is_whitelist()); - assert!(matchedf(&mut m, "a/second.tmp").is_whitelist()); - assert!(matchedf(&mut m, "a/keep.rs").is_ignore()); - } - - #[test] - fn cached_allowed_parent_matches_children() { - let td = tmpdir(); - mkdirp(td.path().join("a/b")); - wfile(td.path().join("a/b/.ignore"), "ignored\n"); - - let mut m = one_matcher(&builder(td.path())); - assert!(matchedf(&mut m, "a/b/allowed").is_none()); - assert!(matches!( - m.dirs.get(Path::new("a/b")), - Some(CachedDir::Allowed(_)) - )); - assert!(matchedf(&mut m, "a/b/ignored").is_ignore()); - } - - #[test] - fn cached_ignored_parent_matches_children() { - let td = tmpdir(); - mkdirp(td.path().join("blocked")); - wfile(td.path().join(".ignore"), "blocked/\n"); - - let mut m = one_matcher(&builder(td.path())); - assert!(matchedf(&mut m, "blocked/first").is_ignore()); - assert!(matches!( - m.dirs.get(Path::new("blocked")), - Some(CachedDir::Ignored) - )); - assert!(matchedf(&mut m, "blocked/second").is_ignore()); - } - - #[test] - fn cached_parent_matcher_is_used_for_directory() { - let td = tmpdir(); - mkdirp(td.path().join("a/b")); - wfile(td.path().join("a/b/.ignore"), "**\n"); - - let mut m = one_matcher(&builder(td.path())); - assert!(matchedf(&mut m, "a/b/file").is_ignore()); - assert!(matches!( - m.dirs.get(Path::new("a/b")), - Some(CachedDir::Allowed(_)) - )); - let dir = matchedd(&mut m, "a/b"); - assert!(dir.is_none()); - assert!(dir.should_descend()); - } - - // Like `compiled_matchers_are_reused`, but with multiple roots. - #[test] - fn compiled_multi_matchers_are_reused() { - let td = tmpdir(); - mkdirp(td.path().join("a/b/c")); - - wfile(td.path().join("a/.ignore"), "*.tmp\n"); - let mut mats = matchers(&builders([ - td.path().join("a/b"), - td.path().join("a/b/c"), - ])); - assert!(matchedf(&mut mats[0], "first.tmp").is_ignore()); - assert!(matchedf(&mut mats[0], "c/first.tmp").is_ignore()); - - // Even though we haven't used the second matcher, it should still - // reuse the `a/.ignore` above. If it didn't, then `a/b/c/first.tmp` - // below would be whitelisted. - wfile(td.path().join("a/.ignore"), "!*.tmp\n"); - assert!(matchedf(&mut mats[1], "first.tmp").is_ignore()); - } - - #[test] - fn compiled_multi_child_matchers_are_not_reused() { - let td = tmpdir(); - mkdirp(td.path().join("a/b/c/d/e")); - - wfile(td.path().join("a/b/c/d/e/.ignore"), "*.tmp\n"); - let mut mats = matchers(&builders([ - td.path().join("a/b"), - td.path().join("a/b/c"), - ])); - // Some sanity checking first. - assert!(matchedf(&mut mats[0], "c/first.tmp").is_none()); - assert!(matchedf(&mut mats[0], "c/d/first.tmp").is_none()); - assert!(matchedf(&mut mats[0], "c/d/e/first.tmp").is_ignore()); - - // Now write a new ignore file at the same location as above - // and check that the other matcher still uses the "stale" data. - wfile(td.path().join("a/b/c/d/e/.ignore"), "!*.tmp\n"); - assert!(matchedf(&mut mats[1], "first.tmp").is_none()); - assert!(matchedf(&mut mats[1], "d/first.tmp").is_none()); - // This is the punch line: because we didn't load `mats[1]` before - // changing the ignore file, it loads it here and thus this path gets - // whitelisted. - assert!(matchedf(&mut mats[1], "d/e/first.tmp").is_whitelist()); - // ... but `mats[0]` still uses the stale gitignore matcher cached in - // memory! - assert!(matchedf(&mut mats[0], "c/d/e/first.tmp").is_ignore()); - - // If we rebuilder the matcher... then we force reloading and they're - // now consistent with one another. - let mut mats = matchers(&builders([ - td.path().join("a/b"), - td.path().join("a/b/c"), - ])); - assert!(matchedf(&mut mats[0], "c/d/e/first.tmp").is_whitelist()); - assert!(matchedf(&mut mats[1], "d/e/first.tmp").is_whitelist()); - } - - // Tests that even when there is an error with a glob pattern, we still - // respect other glob patterns that are valid. - #[test] - fn partial_errors_keep_valid_rules() { - let td = tmpdir(); - let root = td.path().join("work"); - mkdirp(&root); - wfile(td.path().join(".ignore"), "{bad\n*.tmp\n"); - - let mut builder = builder(&root); - builder.git_ignore(false).git_exclude(false); - let mut m = one_matcher(&builder); - let (matched, err) = m.matched_with_errors("drop.tmp", false); - assert!(err.is_some()); - assert!(matched.is_ignore()); - } - - // Tests that parent rules are not respected if the matcher is configured - // not to do so. - #[test] - fn parent_loading_can_be_disabled() { - let td = tmpdir(); - let root = td.path().join("work"); - mkdirp(&root); - wfile(td.path().join(".ignore"), "parent-rule\n"); - wfile(root.join(".ignore"), "root-rule\n"); - - let mut b = builder(&root); - b.parents(false); - let mut m = one_matcher(&b); - assert!(matchedf(&mut m, "parent-rule").is_none()); - assert!(matchedf(&mut m, "root-rule").is_ignore()); - - // Sanity check that without `parents(false)`, the parent rule is - // respected. - let mut m = one_matcher(&builder(&root)); - assert!(matchedf(&mut m, "parent-rule").is_ignore()); - assert!(matchedf(&mut m, "root-rule").is_ignore()); - } - - // Tests that we can normalize a file path that isn't already in "normal" - // relative form, and then use that to match on ignore files. - #[test] - fn paths_are_relative_to_the_root() { - let td = tmpdir(); - let root = td.path().join("root"); - let outside = td.path().join("outside"); - mkdirp(&root); - mkdirp(&outside); - wfile(root.join(".ignore"), "file\n"); - - let mut m = one_matcher(&builder(&root)); - assert!(matchedd(&mut m, "").is_none()); - assert!(matchedd(&mut m, ".").is_none()); - assert!(matchedf(&mut m, "file").is_ignore()); - // Doesn't work because it isn't relative to the root. It's absolute. - assert!(matchedf(&mut m, root.join("file")).is_none()); - // Also doesn't work because while it's relative, it contains `..`. - assert!(matchedf(&mut m, "dir/../file").is_none()); - - let norm = m.normalize(root.join("dir/../file")).unwrap(); - assert_eq!(norm, Path::new("file")); - assert!(matchedf(&mut m, "file").is_ignore()); - - // Doesn't normalize because its an absolute path outside of our root. - assert_eq!(m.normalize(outside.join("file")), None); - } - - // Tests that two matchers in two different directories correctly interpret - // the same parent gitignore file. - #[test] - fn multiple_roots_keep_their_own_context() { - let td = tmpdir(); - let root_a = td.path().join("a"); - let root_b = td.path().join("b"); - mkdirp(td.path().join(".git")); - mkdirp(&root_a); - mkdirp(&root_b); - wfile(td.path().join(".gitignore"), "/a/*.tmp\n/b/*.log\n"); - - let mut builder = - builders([root_a.as_path(), Path::new("-"), root_b.as_path()]); - builder.git_global(false); - let mut ms = matchers(&builder); - assert_eq!(ms.len(), 3); - assert_eq!(ms[0].root(), root_a); - assert_eq!(ms[1].root(), Path::new("-")); - assert_eq!(ms[2].root(), root_b); - assert!(matchedf(&mut ms[0], "drop.tmp").is_ignore()); - assert!(matchedf(&mut ms[0], "keep.log").is_none()); - assert!(matchedf(&mut ms[1], "anything").is_none()); - assert_eq!(ms[1].normalize("anything"), None); - assert!(matchedf(&mut ms[2], "keep.tmp").is_none()); - assert!(matchedf(&mut ms[2], "drop.log").is_ignore()); - } - - // Tests that only the exact `-` root represents standard input. - #[test] - fn dot_dash_root_is_not_stdin() { - let m = one_matcher(&builder("./-")); - assert_eq!(m.root(), Path::new("./-")); - assert_eq!(m.normalize("./-/file"), Some(PathBuf::from("file"))); - } - - #[test] - fn stdin_is_inert_with_depth_limits() { - let mut b = builder("-"); - b.min_depth(Some(2)); - let mut m = one_matcher(&b); - let mat = matchedf(&mut m, "file"); - assert!(mat.is_none()); - assert!(mat.is_within_depth()); - - let mut b = builder("-"); - b.max_depth(Some(0)); - let mut m = one_matcher(&b); - let mat = matchedd(&mut m, "dir"); - assert!(mat.is_none()); - assert!(mat.is_within_depth()); - assert!(mat.should_descend()); - } - - // Tests that ignore matching works lexically, and doesn't accidentally - // resolve symbolic links. - #[cfg(unix)] - #[test] - fn symlink_path_stays_under_lexical_root() { - use std::os::unix::fs::symlink; - - let td = tmpdir(); - let root = td.path().join("root"); - let outside = td.path().join("outside"); - mkdirp(&root); - mkdirp(&outside); - wfile(root.join(".ignore"), "link\n"); - wfile(outside.join("target"), ""); - symlink(outside.join("target"), root.join("link")).unwrap(); - - let mut m = one_matcher(&builder(&root)); - assert!(matchedf(&mut m, "link").is_ignore()); - } - - #[test] - fn depth_limits() { - let td = tmpdir(); - mkdirp(td.path().join("a/b/c/d")); - - let mut b = builder(td.path()); - b.min_depth(Some(2)).max_depth(Some(3)); - let mut m = one_matcher(&b); - - let dir = matchedd(&mut m, "a"); - assert!(!dir.is_within_depth()); - assert!(dir.should_descend()); - assert!(matchedf(&mut m, "file").is_ignore()); - - let dir = matchedd(&mut m, "a/b"); - assert!(dir.is_within_depth()); - assert!(dir.should_descend()); - assert!(!matchedf(&mut m, "a/file").is_ignore()); - - let dir = matchedd(&mut m, "a/b/c"); - assert!(dir.is_within_depth()); - assert!(!dir.should_descend()); - assert!(!matchedf(&mut m, "a/b/file").is_ignore()); - assert!(matchedf(&mut m, "a/b/c/file").is_ignore()); - - let dir = matchedd(&mut m, "a/b/c/d"); - assert!(dir.is_ignore()); - assert!(!dir.is_within_depth()); - assert!(!dir.should_descend()); - assert!(matchedf(&mut m, "a/b/c/d/file").is_ignore()); - } - - #[test] - fn depth_limits_apply_to_root() { - let td = tmpdir(); - - let mut b = builder(td.path()); - b.min_depth(Some(1)); - let mut m = one_matcher(&b); - for path in ["", "."] { - let root = matchedd(&mut m, path); - assert!(root.is_none()); - assert!(!root.is_within_depth()); - assert!(root.should_descend()); - } - - let mut b = builder(td.path()); - b.max_depth(Some(0)); - let mut m = one_matcher(&b); - for path in ["", "."] { - let root = matchedd(&mut m, path); - assert!(root.is_none()); - assert!(root.is_within_depth()); - assert!(!root.should_descend()); - } - } - - #[test] - fn max_filesize() { - let td = tmpdir(); - mkdirp(td.path().join("dir")); - wfile(td.path().join("empty"), ""); - wfile(td.path().join("at-limit"), "12345"); - wfile(td.path().join("over-limit"), "123456"); - - let mut b = builder(td.path()); - b.max_filesize(Some(5)); - let mut m = one_matcher(&b); - - assert!(matchedf(&mut m, "empty").is_none()); - assert!(matchedf(&mut m, "at-limit").is_none()); - assert!(matchedf(&mut m, "over-limit").is_ignore()); - - let dir = matchedd(&mut m, "dir"); - assert!(dir.is_none()); - assert!(dir.should_descend()); - } - - #[test] - fn max_filesize_does_not_stat_ignored_file() { - let td = tmpdir(); - wfile(td.path().join(".ignore"), "ignored\n"); - - let mut b = builder(td.path()); - b.max_filesize(Some(0)); - let mut m = one_matcher(&b); - let (mat, err) = m.matched_with_errors("ignored", false); - assert!(mat.is_ignore()); - assert!(err.is_none(), "ignored missing file was statted: {err:?}"); - } - - #[cfg(unix)] - #[test] - fn max_filesize_respects_follow_links() { - use std::os::unix::fs::symlink; - - let td = tmpdir(); - wfile( - td.path().join("target"), - "target contents are much longer than the size limit", - ); - symlink("target", td.path().join("link")).unwrap(); - - let mut b = builder(td.path()); - b.max_filesize(Some(10)); - let mut m = one_matcher(&b); - assert!(matchedf(&mut m, "link").is_none()); - - b.follow_links(true); - let mut m = one_matcher(&b); - assert!(matchedf(&mut m, "link").is_ignore()); - } - - #[test] - fn hidden_files_and_directories() { - let td = tmpdir(); - mkdirp(td.path().join(".hidden-dir")); - mkdirp(td.path().join("visible-dir")); - wfile(td.path().join(".hidden-file"), ""); - wfile(td.path().join("visible-file"), ""); - - let mut m = one_matcher(&builder(td.path())); - assert!(matchedf(&mut m, ".hidden-file").is_ignore()); - let dir = matchedd(&mut m, ".hidden-dir"); - assert!(dir.is_ignore()); - assert!(!dir.should_descend()); - assert!(matchedf(&mut m, "visible-file").is_none()); - let dir = matchedd(&mut m, "visible-dir"); - assert!(dir.is_none()); - assert!(dir.should_descend()); - - let mut b = builder(td.path()); - b.hidden(false); - let mut m = one_matcher(&b); - assert!(matchedf(&mut m, ".hidden-file").is_none()); - let dir = matchedd(&mut m, ".hidden-dir"); - assert!(dir.is_none()); - assert!(dir.should_descend()); - } - - #[test] - fn gitignore_whitelist_overrides_hidden_filter() { - let td = tmpdir(); - mkdirp(td.path().join(".git")); - mkdirp(td.path().join(".hidden-dir")); - wfile(td.path().join(".hidden-file"), ""); - wfile(td.path().join(".gitignore"), "!.hidden-file\n!.hidden-dir/\n"); - - let mut m = one_matcher(&builder(td.path())); - assert!(matchedf(&mut m, ".hidden-file").is_whitelist()); - let dir = matchedd(&mut m, ".hidden-dir"); - assert!(dir.is_whitelist()); - assert!(dir.should_descend()); - } - - #[test] - fn descendants_of_hidden_directories_are_ignored() { - let td = tmpdir(); - mkdirp(td.path().join(".git")); - mkdirp(td.path().join(".hidden/nested")); - mkdirp(td.path().join("visible/.hidden")); - wfile(td.path().join(".hidden/file"), ""); - wfile(td.path().join(".hidden/nested/file"), ""); - wfile(td.path().join("visible/.hidden/file"), ""); - wfile(td.path().join(".hidden/.gitignore"), "!file\n"); - - let mut m = one_matcher(&builder(td.path())); - assert!(matchedf(&mut m, ".hidden/file").is_ignore()); - assert!(matchedf(&mut m, ".hidden/nested/file").is_ignore()); - assert!(matchedf(&mut m, "visible/.hidden/file").is_ignore()); - } - - #[test] - fn whitelisted_hidden_directory_allows_descendants() { - let td = tmpdir(); - mkdirp(td.path().join(".git")); - mkdirp(td.path().join(".hidden")); - wfile(td.path().join(".hidden/file"), ""); - wfile(td.path().join(".gitignore"), "!.hidden/\n"); - - let mut m = one_matcher(&builder(td.path())); - let dir = matchedd(&mut m, ".hidden"); - assert!(dir.is_whitelist()); - assert!(dir.should_descend()); - assert!(matchedf(&mut m, ".hidden/file").is_none()); - } - - #[test] - fn hidden_directories_outside_depth_limits() { - let td = tmpdir(); - mkdirp(td.path().join("a/.hidden")); - - let mut b = builder(td.path()); - b.min_depth(Some(3)); - let mut m = one_matcher(&b); - let dir = matchedd(&mut m, "a/.hidden"); - assert!(dir.is_ignore()); - assert!(!dir.is_within_depth()); - - let mut b = builder(td.path()); - b.max_depth(Some(1)); - let mut m = one_matcher(&b); - let dir = matchedd(&mut m, "a/.hidden"); - assert!(dir.is_ignore()); - assert!(!dir.is_within_depth()); - } -} diff --git a/crates/ignore/src/lib.rs b/crates/ignore/src/lib.rs index b9c9a3ece..609004c4e 100644 --- a/crates/ignore/src/lib.rs +++ b/crates/ignore/src/lib.rs @@ -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>(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 = - errs.iter().map(|err| err.to_string()).collect(); + let msgs: Vec = 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 = - std::result::Result>; + pub(crate) type Result = std::result::Result>; 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)) diff --git a/crates/ignore/src/overrides.rs b/crates/ignore/src/overrides.rs index 005cae8f2..afb9f16ce 100644 --- a/crates/ignore/src/overrides.rs +++ b/crates/ignore/src/overrides.rs @@ -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>( - &'a self, - path: P, - is_dir: bool, - ) -> Match> { + pub fn matched<'a, P: AsRef>(&'a self, path: P, is_dir: bool) -> Match> { 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()); diff --git a/crates/ignore/src/pathutil.rs b/crates/ignore/src/pathutil.rs index 5de8c106c..0ceb5a356 100644 --- a/crates/ignore/src/pathutil.rs +++ b/crates/ignore/src/pathutil.rs @@ -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 + ?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 + ?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>(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>(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 + ?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 + ?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 + ?Sized>(path: &'a P) -> Option<&'a OsStr> { + path.as_ref().file_name() } diff --git a/crates/ignore/src/types.rs b/crates/ignore/src/types.rs index 313cf5c0e..aa23999c0 100644 --- a/crates/ignore/src/types.rs +++ b/crates/ignore/src/types.rs @@ -204,12 +204,8 @@ impl Selection { fn map U>(self, f: F) -> Selection { 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>( - &'a self, - path: P, - is_dir: bool, - ) -> Match> { + pub fn matched<'a, P: AsRef>(&'a self, path: P, is_dir: bool) -> Match> { // 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"); diff --git a/crates/ignore/src/walk.rs b/crates/ignore/src/walk.rs index 2dff78852..ebf20947a 100644 --- a/crates/ignore/src/walk.rs +++ b/crates/ignore/src/walk.rs @@ -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) -> DirEntry { - DirEntry { dent: DirEntryInner::Walkdir(dent), err } + DirEntry { + dent: DirEntryInner::Walkdir(dent), + err, + } } fn new_raw(dent: DirEntryRaw, err: Option) -> DirEntry { - DirEntry { dent: DirEntryInner::Raw(dent), err } + DirEntry { + dent: DirEntryInner::Raw(dent), + err, + } } } @@ -180,11 +187,9 @@ impl DirEntryInner { )); Err(err.with_path("")) } - 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 { + fn from_entry(depth: usize, ent: &fs::DirEntry) -> Result { 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 { 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 { - 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 { + 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 { + fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result { 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 { + fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result { 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>>, + global_gitignores_relative_to: OnceLock>>, } #[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>(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, I: IntoIterator>( - 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::>() .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 { - 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) -> &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) -> &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>(&mut self, path: P) -> Option { - // 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, - ) -> &mut WalkBuilder { + pub fn current_dir(&mut self, cwd: impl Into) -> &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, ig_root: Ignore, ig: Ignore, - max_depth: Option, max_filesize: Option, skip: Option>, filter: Option, @@ -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, I: IntoIterator>( - paths: I, - ) -> Walk { - WalkBuilder::from_iter(paths).build() - } - fn skip_entry(&self, ent: &DirEntry) -> Result { 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 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; } -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 { (**self).build() } @@ -1395,17 +1273,14 @@ struct FnBuilder { builder: F, } -impl<'s, F: FnMut() -> FnVisitor<'s>> ParallelVisitorBuilder<'s> - for FnBuilder -{ +impl<'s, F: FnMut() -> FnVisitor<'s>> ParallelVisitorBuilder<'s> for FnBuilder { fn build(&mut self) -> Box { let visitor = (self.builder)(); Box::new(FnVisitorImp { visitor }) } } -type FnVisitor<'s> = - Box) -> WalkState + Send + 's>; +type FnVisitor<'s> = Box) -> 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, } -#[derive(Default)] -struct ReadDirResult { - entries: Vec, - errors: Vec, -} - 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 { + fn read_dir(&mut self) -> Result { 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> = - std::iter::repeat_with(Deque::new_lifo).take(threads).collect(); - let stealers = Arc::<[Stealer]>::from( - deques.iter().map(Deque::stealer).collect::>(), - ); + let deques: Vec> = std::iter::repeat_with(Deque::new_lifo) + .take(threads) + .collect(); + let stealers = + Arc::<[Stealer]>::from(deques.iter().map(Deque::stealer).collect::>()); let stacks: Vec = 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, - fs_dent: fs::DirEntry, + result: Result, ) -> 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, -) -> bool { +fn skip_filesize(max_filesize: u64, path: &Path, ent: &Option) -> 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 { 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 { - 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 { @@ -2259,10 +2089,7 @@ mod tests { paths } - fn walk_collect_parallel( - prefix: &Path, - builder: &WalkBuilder, - ) -> Vec { + fn walk_collect_parallel(prefix: &Path, builder: &WalkBuilder) -> Vec { 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) - }); - } } diff --git a/crates/ignore/tests/gitignore_matched_path_or_any_parents_tests.rs b/crates/ignore/tests/gitignore_matched_path_or_any_parents_tests.rs index ecb7b47e3..b7b7c6f95 100644 --- a/crates/ignore/tests/gitignore_matched_path_or_any_parents_tests.rs +++ b/crates/ignore/tests/gitignore_matched_path_or_any_parents_tests.rs @@ -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()); } diff --git a/crates/node/.gitignore b/crates/node/.gitignore index c3f2ec696..d2200a62e 100644 --- a/crates/node/.gitignore +++ b/crates/node/.gitignore @@ -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 diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index a3df7b813..d4ddcdfd1 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -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" diff --git a/crates/node/npm/android-arm-eabi/package.json b/crates/node/npm/android-arm-eabi/package.json index d46ceb237..c941aad39 100644 --- a/crates/node/npm/android-arm-eabi/package.json +++ b/crates/node/npm/android-arm-eabi/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-android-arm-eabi", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,6 +22,6 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" } } diff --git a/crates/node/npm/android-arm64/package.json b/crates/node/npm/android-arm64/package.json index e6e9d577d..98368c21c 100644 --- a/crates/node/npm/android-arm64/package.json +++ b/crates/node/npm/android-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-android-arm64", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,6 +22,6 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" } } diff --git a/crates/node/npm/darwin-arm64/package.json b/crates/node/npm/darwin-arm64/package.json index 1cb737c2e..11d4c137f 100644 --- a/crates/node/npm/darwin-arm64/package.json +++ b/crates/node/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-darwin-arm64", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,6 +22,6 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" } } diff --git a/crates/node/npm/darwin-x64/package.json b/crates/node/npm/darwin-x64/package.json index ffd75e54b..d3a460f5a 100644 --- a/crates/node/npm/darwin-x64/package.json +++ b/crates/node/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-darwin-x64", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,6 +22,6 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" } } diff --git a/crates/node/npm/freebsd-x64/package.json b/crates/node/npm/freebsd-x64/package.json index e248f39d1..4a90b1ab2 100644 --- a/crates/node/npm/freebsd-x64/package.json +++ b/crates/node/npm/freebsd-x64/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-freebsd-x64", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,6 +22,6 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" } } diff --git a/crates/node/npm/linux-arm-gnueabihf/package.json b/crates/node/npm/linux-arm-gnueabihf/package.json index 50d2aef73..f1dde6638 100644 --- a/crates/node/npm/linux-arm-gnueabihf/package.json +++ b/crates/node/npm/linux-arm-gnueabihf/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-linux-arm-gnueabihf", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,6 +22,6 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" } } diff --git a/crates/node/npm/linux-arm64-gnu/package.json b/crates/node/npm/linux-arm64-gnu/package.json index 5875213f8..c94a234fc 100644 --- a/crates/node/npm/linux-arm64-gnu/package.json +++ b/crates/node/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-linux-arm64-gnu", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,7 +22,7 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" }, "libc": [ "glibc" diff --git a/crates/node/npm/linux-arm64-musl/package.json b/crates/node/npm/linux-arm64-musl/package.json index 9da37f3fd..f08112f9f 100644 --- a/crates/node/npm/linux-arm64-musl/package.json +++ b/crates/node/npm/linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-linux-arm64-musl", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,7 +22,7 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" }, "libc": [ "musl" diff --git a/crates/node/npm/linux-x64-gnu/package.json b/crates/node/npm/linux-x64-gnu/package.json index d409743fa..6c08ff6c6 100644 --- a/crates/node/npm/linux-x64-gnu/package.json +++ b/crates/node/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-linux-x64-gnu", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,7 +22,7 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" }, "libc": [ "glibc" diff --git a/crates/node/npm/linux-x64-musl/package.json b/crates/node/npm/linux-x64-musl/package.json index 5b404fd70..73a5b3868 100644 --- a/crates/node/npm/linux-x64-musl/package.json +++ b/crates/node/npm/linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-linux-x64-musl", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,7 +22,7 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" }, "libc": [ "musl" diff --git a/crates/node/npm/wasm32-wasi/.npmrc b/crates/node/npm/wasm32-wasi/.npmrc new file mode 100644 index 000000000..d67f37488 --- /dev/null +++ b/crates/node/npm/wasm32-wasi/.npmrc @@ -0,0 +1 @@ +node-linker=hoisted diff --git a/crates/node/npm/wasm32-wasi/README.md b/crates/node/npm/wasm32-wasi/README.md index 2a5368149..9ec3a7b3f 100644 --- a/crates/node/npm/wasm32-wasi/README.md +++ b/crates/node/npm/wasm32-wasi/README.md @@ -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` diff --git a/crates/node/npm/wasm32-wasi/package.json b/crates/node/npm/wasm32-wasi/package.json index cee9fb05a..b57e14604 100644 --- a/crates/node/npm/wasm32-wasi/package.json +++ b/crates/node/npm/wasm32-wasi/package.json @@ -1,6 +1,9 @@ { "name": "@tailwindcss/oxide-wasm32-wasi", - "version": "4.3.3", + "version": "4.1.16", + "cpu": [ + "wasm32" + ], "main": "tailwindcss-oxide.wasi.cjs", "files": [ "tailwindcss-oxide.wasm32-wasi.wasm", @@ -11,7 +14,7 @@ ], "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + "node": ">=14.0.0" }, "publishConfig": { "provenance": true, @@ -24,12 +27,12 @@ }, "browser": "tailwindcss-oxide.wasi-browser.js", "dependencies": { - "@napi-rs/wasm-runtime": "^1.2.2", - "@emnapi/core": "^1.11.3", - "@emnapi/runtime": "^1.11.3", - "@tybys/wasm-util": "^0.10.3", - "@emnapi/wasi-threads": "^1.2.3", - "tslib": "^2.8.1" + "@napi-rs/wasm-runtime": "^1.0.7", + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@tybys/wasm-util": "^0.10.1", + "@emnapi/wasi-threads": "^1.1.0", + "tslib": "^2.4.0" }, "bundledDependencies": [ "@napi-rs/wasm-runtime", diff --git a/crates/node/npm/wasm32-wasi/pnpm-lock.yaml b/crates/node/npm/wasm32-wasi/pnpm-lock.yaml new file mode 100644 index 000000000..5cd70ea37 --- /dev/null +++ b/crates/node/npm/wasm32-wasi/pnpm-lock.yaml @@ -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: {} diff --git a/crates/node/npm/win32-arm64-msvc/package.json b/crates/node/npm/win32-arm64-msvc/package.json index 991727a32..b39945151 100644 --- a/crates/node/npm/win32-arm64-msvc/package.json +++ b/crates/node/npm/win32-arm64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-win32-arm64-msvc", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,6 +22,6 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" } } diff --git a/crates/node/npm/win32-x64-msvc/package.json b/crates/node/npm/win32-x64-msvc/package.json index 82d75c300..8f437816e 100644 --- a/crates/node/npm/win32-x64-msvc/package.json +++ b/crates/node/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide-win32-x64-msvc", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -22,6 +22,6 @@ }, "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 10" } } diff --git a/crates/node/package.json b/crates/node/package.json index be09427fb..b05745eee 100644 --- a/crates/node/package.json +++ b/crates/node/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/oxide", - "version": "4.3.3", + "version": "4.1.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss.git", @@ -33,14 +33,12 @@ }, "license": "MIT", "devDependencies": { - "@emnapi/core": "1.11.3", - "@emnapi/runtime": "1.11.3", - "@napi-rs/cli": "3.7.4", - "@napi-rs/wasm-runtime": "^1.2.2", - "emnapi": "1.11.3" + "@napi-rs/cli": "^3.4.1", + "@napi-rs/wasm-runtime": "^1.0.7", + "emnapi": "1.6.0" }, "engines": { - "node": ">= 20" + "node": ">= 10" }, "files": [ "index.js", diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index fb32a818c..11ff25b0a 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -165,11 +165,6 @@ impl Scanner { self.scanner.get_files() } - #[napi(getter)] - pub fn scanned_files(&self) -> Vec { - self.scanner.get_scanned_files() - } - #[napi(getter)] pub fn globs(&mut self) -> Vec { self diff --git a/crates/oxide/Cargo.toml b/crates/oxide/Cargo.toml index f8a5f6137..0965320c9 100644 --- a/crates/oxide/Cargo.toml +++ b/crates/oxide/Cargo.toml @@ -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" diff --git a/crates/oxide/src/cursor.rs b/crates/oxide/src/cursor.rs index 71cdccdf6..818a0ef1b 100644 --- a/crates/oxide/src/cursor.rs +++ b/crates/oxide/src/cursor.rs @@ -1,49 +1,44 @@ use std::{ascii::escape_default, fmt::Display}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct Cursor<'a> { // The input we're scanning pub input: &'a [u8], // The location of the cursor in the input pub pos: usize, + + /// Is the cursor at the start of the input + pub at_start: bool, + + /// Is the cursor at the end of the input + pub at_end: bool, + + /// The previously consumed character + /// If `at_start` is true, this will be NUL + pub prev: u8, + + /// The current character + pub curr: u8, + + /// The upcoming character (if any) + /// If `at_end` is true, this will be NUL + pub next: u8, } impl<'a> Cursor<'a> { - #[inline(always)] pub fn new(input: &'a [u8]) -> Self { - Self { input, pos: 0 } - } - - /// The current byte at `pos`, or 0x00 if past the end. - #[inline(always)] - pub fn curr(&self) -> u8 { - if self.pos < self.input.len() { - unsafe { *self.input.get_unchecked(self.pos) } - } else { - 0x00 - } - } - - /// The next byte at `pos + 1`, or 0x00 if past the end. - #[inline(always)] - pub fn next(&self) -> u8 { - let next_pos = self.pos + 1; - if next_pos < self.input.len() { - unsafe { *self.input.get_unchecked(next_pos) } - } else { - 0x00 - } - } - - /// The previous byte at `pos - 1`, or 0x00 if at the start. - #[inline(always)] - pub fn prev(&self) -> u8 { - if self.pos > 0 { - unsafe { *self.input.get_unchecked(self.pos - 1) } - } else { - 0x00 - } + let mut cursor = Self { + input, + pos: 0, + at_start: true, + at_end: false, + prev: 0x00, + curr: 0x00, + next: 0x00, + }; + cursor.move_to(0); + cursor } pub fn advance_by(&mut self, amount: usize) { @@ -53,15 +48,38 @@ impl<'a> Cursor<'a> { #[inline(always)] pub fn advance(&mut self) { self.pos += 1; + + self.prev = self.curr; + self.curr = self.next; + self.next = *self + .input + .get(self.pos.saturating_add(1)) + .unwrap_or(&0x00u8); } #[inline(always)] pub fn advance_twice(&mut self) { self.pos += 2; + + self.prev = self.next; + self.curr = *self.input.get(self.pos).unwrap_or(&0x00u8); + self.next = *self + .input + .get(self.pos.saturating_add(1)) + .unwrap_or(&0x00u8); } pub fn move_to(&mut self, pos: usize) { - self.pos = pos.min(self.input.len()); + let len = self.input.len(); + let pos = pos.clamp(0, len); + + self.pos = pos; + self.at_start = pos == 0; + self.at_end = pos + 1 >= len; + + self.prev = *self.input.get(pos.wrapping_sub(1)).unwrap_or(&0x00u8); + self.curr = *self.input.get(pos).unwrap_or(&0x00u8); + self.next = *self.input.get(pos.saturating_add(1)).unwrap_or(&0x00u8); } } @@ -72,9 +90,9 @@ impl Display for Cursor<'_> { let pos = format!("{: >len_count$}", self.pos, len_count = len.len()); write!(f, "{}/{} ", pos, len)?; - if self.pos == 0 { + if self.at_start { write!(f, "S ")?; - } else if self.pos + 1 >= self.input.len() { + } else if self.at_end { write!(f, "E ")?; } else { write!(f, "M ")?; @@ -91,9 +109,9 @@ impl Display for Cursor<'_> { write!( f, "[{} {} {}]", - to_str(self.prev()), - to_str(self.curr()), - to_str(self.next()) + to_str(self.prev), + to_str(self.curr), + to_str(self.next) ) } } @@ -107,28 +125,36 @@ mod test { fn test_cursor() { let mut cursor = Cursor::new(b"hello world"); assert_eq!(cursor.pos, 0); - assert_eq!(cursor.prev(), 0x00); - assert_eq!(cursor.curr(), b'h'); - assert_eq!(cursor.next(), b'e'); + assert!(cursor.at_start); + assert!(!cursor.at_end); + assert_eq!(cursor.prev, 0x00); + assert_eq!(cursor.curr, b'h'); + assert_eq!(cursor.next, b'e'); cursor.advance_by(1); assert_eq!(cursor.pos, 1); - assert_eq!(cursor.prev(), b'h'); - assert_eq!(cursor.curr(), b'e'); - assert_eq!(cursor.next(), b'l'); + assert!(!cursor.at_start); + assert!(!cursor.at_end); + assert_eq!(cursor.prev, b'h'); + assert_eq!(cursor.curr, b'e'); + assert_eq!(cursor.next, b'l'); // Advancing too far should stop at the end cursor.advance_by(10); assert_eq!(cursor.pos, 11); - assert_eq!(cursor.prev(), b'd'); - assert_eq!(cursor.curr(), 0x00); - assert_eq!(cursor.next(), 0x00); + assert!(!cursor.at_start); + assert!(cursor.at_end); + assert_eq!(cursor.prev, b'd'); + assert_eq!(cursor.curr, 0x00); + assert_eq!(cursor.next, 0x00); // Can't advance past the end cursor.advance_by(1); assert_eq!(cursor.pos, 11); - assert_eq!(cursor.prev(), b'd'); - assert_eq!(cursor.curr(), 0x00); - assert_eq!(cursor.next(), 0x00); + assert!(!cursor.at_start); + assert!(cursor.at_end); + assert_eq!(cursor.prev, b'd'); + assert_eq!(cursor.curr, 0x00); + assert_eq!(cursor.next, 0x00); } } diff --git a/crates/oxide/src/extractor/arbitrary_property_machine.rs b/crates/oxide/src/extractor/arbitrary_property_machine.rs index a43253339..8ed3f1763 100644 --- a/crates/oxide/src/extractor/arbitrary_property_machine.rs +++ b/crates/oxide/src/extractor/arbitrary_property_machine.rs @@ -74,7 +74,7 @@ impl Machine for ArbitraryPropertyMachine { #[inline] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { - match cursor.curr().into() { + match cursor.curr.into() { // Start of an arbitrary property Class::OpenBracket => { self.start_pos = cursor.pos; @@ -97,8 +97,8 @@ impl Machine for ArbitraryPropertyMachine { let len = cursor.input.len(); while cursor.pos < len { - match cursor.curr().into() { - Class::Dash => match cursor.next().into() { + match cursor.curr.into() { + Class::Dash => match cursor.next.into() { // Start of a CSS variable // // E.g.: `[--my-color:red]` @@ -137,7 +137,7 @@ impl ArbitraryPropertyMachine { fn parse_property_variable(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { match self.css_variable_machine.next(cursor) { MachineState::Idle => self.restart(), - MachineState::Done(_) => match cursor.next().into() { + MachineState::Done(_) => match cursor.next.into() { // End of the CSS variable, must be followed by a `:` // // E.g.: `[--my-color:red]` @@ -165,8 +165,8 @@ impl Machine for ArbitraryPropertyMachine { let len = cursor.input.len(); let start_of_value_pos = cursor.pos; while cursor.pos < len { - match cursor.curr().into() { - Class::Escape => match cursor.next().into() { + match cursor.curr.into() { + Class::Escape => match cursor.next.into() { // An escaped whitespace character is not allowed // // E.g.: `[color:var(--my-\ color)]` @@ -181,7 +181,7 @@ impl Machine for ArbitraryPropertyMachine { }, Class::OpenParen | Class::OpenBracket | Class::OpenCurly => { - if !self.bracket_stack.push(cursor.curr()) { + if !self.bracket_stack.push(cursor.curr) { return self.restart(); } cursor.advance(); @@ -190,7 +190,7 @@ impl Machine for ArbitraryPropertyMachine { Class::CloseParen | Class::CloseBracket | Class::CloseCurly if !self.bracket_stack.is_empty() => { - if !self.bracket_stack.pop(cursor.curr()) { + if !self.bracket_stack.pop(cursor.curr) { return self.restart(); } cursor.advance(); @@ -227,16 +227,10 @@ impl Machine for ArbitraryPropertyMachine { Class::Slash if start_of_value_pos == cursor.pos => return self.restart(), // String interpolation-like syntax is not allowed. E.g.: `[${x}]` - Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => { + Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => { return self.restart() } - // An `!` at the top-level is invalid. We don't allow things to end with - // `!important` either as we have dedicated syntax for this. - Class::Exclamation if self.bracket_stack.is_empty() => { - return self.restart(); - } - // Everything else is valid _ => cursor.advance(), }; @@ -299,9 +293,6 @@ enum Class { #[bytes(b'/')] Slash, - #[bytes(b'!')] - Exclamation, - #[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')] Whitespace, @@ -378,9 +369,6 @@ mod tests { "[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]", vec!["[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]"], ), - // A property containing `!` at the top-level is invalid - ("[color:red!]", vec![]), - ("[color:red!important]", vec![]), ] { for wrapper in [ // No wrapper diff --git a/crates/oxide/src/extractor/arbitrary_value_machine.rs b/crates/oxide/src/extractor/arbitrary_value_machine.rs index cbef62c42..f5a612df1 100644 --- a/crates/oxide/src/extractor/arbitrary_value_machine.rs +++ b/crates/oxide/src/extractor/arbitrary_value_machine.rs @@ -32,7 +32,7 @@ impl Machine for ArbitraryValueMachine { #[inline] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { // An arbitrary value must start with an open bracket - if Class::OpenBracket != cursor.curr().into() { + if Class::OpenBracket != cursor.curr.into() { return MachineState::Idle; } @@ -42,8 +42,8 @@ impl Machine for ArbitraryValueMachine { let len = cursor.input.len(); while cursor.pos < len { - match cursor.curr().into() { - Class::Escape => match cursor.next().into() { + match cursor.curr.into() { + Class::Escape => match cursor.next.into() { // An escaped whitespace character is not allowed // // E.g.: `[color:var(--my-\ color)]` @@ -61,7 +61,7 @@ impl Machine for ArbitraryValueMachine { }, Class::OpenParen | Class::OpenBracket | Class::OpenCurly => { - if !self.bracket_stack.push(cursor.curr()) { + if !self.bracket_stack.push(cursor.curr) { return self.restart(); } cursor.advance(); @@ -70,7 +70,7 @@ impl Machine for ArbitraryValueMachine { Class::CloseParen | Class::CloseBracket | Class::CloseCurly if !self.bracket_stack.is_empty() => { - if !self.bracket_stack.pop(cursor.curr()) { + if !self.bracket_stack.pop(cursor.curr) { return self.restart(); } cursor.advance(); @@ -96,7 +96,7 @@ impl Machine for ArbitraryValueMachine { Class::Whitespace => return self.restart(), // String interpolation-like syntax is not allowed. E.g.: `[${x}]` - Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => { + Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => { return self.restart() } diff --git a/crates/oxide/src/extractor/arbitrary_variable_machine.rs b/crates/oxide/src/extractor/arbitrary_variable_machine.rs index e8d475557..6990e110b 100644 --- a/crates/oxide/src/extractor/arbitrary_variable_machine.rs +++ b/crates/oxide/src/extractor/arbitrary_variable_machine.rs @@ -80,13 +80,13 @@ impl Machine for ArbitraryVariableMachine { #[inline(always)] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { - match cursor.curr().into() { + match cursor.curr.into() { // Arbitrary variables start with `(` followed by a CSS variable // // E.g.: `(--my-variable)` // ^^ // - Class::OpenParen => match cursor.next().into() { + Class::OpenParen => match cursor.next.into() { Class::Dash => { self.start_pos = cursor.pos; cursor.advance(); @@ -117,7 +117,7 @@ impl Machine for ArbitraryVariableMachine { fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { match self.css_variable_machine.next(cursor) { MachineState::Idle => self.restart(), - MachineState::Done(_) => match cursor.next().into() { + MachineState::Done(_) => match cursor.next.into() { // A CSS variable followed by a `,` means that there is a fallback // // E.g.: `(--my-color,red)` @@ -134,7 +134,7 @@ impl Machine for ArbitraryVariableMachine { _ => { cursor.advance(); - match cursor.curr().into() { + match cursor.curr.into() { // End of an arbitrary variable, must be followed by `)` Class::CloseParen => self.done(self.start_pos, cursor), @@ -156,7 +156,7 @@ impl Machine for ArbitraryVariableMachine { let len = cursor.input.len(); while cursor.pos < len { - match cursor.curr().into() { + match cursor.curr.into() { // Valid data type characters // // E.g.: `(length:--my-length)` @@ -169,7 +169,7 @@ impl Machine for ArbitraryVariableMachine { // // E.g.: `(length:--my-length)` // ^ - Class::Colon => match cursor.next().into() { + Class::Colon => match cursor.next.into() { Class::Dash => { cursor.advance(); return self.transition::().next(cursor); @@ -196,8 +196,8 @@ impl Machine for ArbitraryVariableMachine { fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { let len = cursor.input.len(); while cursor.pos < len { - match cursor.curr().into() { - Class::Escape => match cursor.next().into() { + match cursor.curr.into() { + Class::Escape => match cursor.next.into() { // An escaped whitespace character is not allowed // // E.g.: `(--my-\ color)` @@ -212,7 +212,7 @@ impl Machine for ArbitraryVariableMachine { }, Class::OpenParen | Class::OpenBracket | Class::OpenCurly => { - if !self.bracket_stack.push(cursor.curr()) { + if !self.bracket_stack.push(cursor.curr) { return self.restart(); } cursor.advance(); @@ -221,7 +221,7 @@ impl Machine for ArbitraryVariableMachine { Class::CloseParen | Class::CloseBracket | Class::CloseCurly if !self.bracket_stack.is_empty() => { - if !self.bracket_stack.pop(cursor.curr()) { + if !self.bracket_stack.pop(cursor.curr) { return self.restart(); } cursor.advance(); @@ -253,7 +253,7 @@ impl Machine for ArbitraryVariableMachine { Class::Whitespace => return self.restart(), // String interpolation-like syntax is not allowed. E.g.: `[${x}]` - Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => { + Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => { return self.restart() } diff --git a/crates/oxide/src/extractor/bracket_stack.rs b/crates/oxide/src/extractor/bracket_stack.rs index 2b8d3a13f..f34e56ed3 100644 --- a/crates/oxide/src/extractor/bracket_stack.rs +++ b/crates/oxide/src/extractor/bracket_stack.rs @@ -25,7 +25,6 @@ impl BracketStack { b'(' => b')', b'[' => b']', b'{' => b'}', - b'<' => b'>', _ => std::hint::unreachable_unchecked(), }; } diff --git a/crates/oxide/src/extractor/candidate_machine.rs b/crates/oxide/src/extractor/candidate_machine.rs index a755cf02a..c82cc3707 100644 --- a/crates/oxide/src/extractor/candidate_machine.rs +++ b/crates/oxide/src/extractor/candidate_machine.rs @@ -32,14 +32,14 @@ impl Machine for CandidateMachine { while cursor.pos < len { // Skip ahead for known characters that will never be part of a candidate. No need to // run any sub-machines. - if cursor.curr().is_ascii_whitespace() { + if cursor.curr.is_ascii_whitespace() { self.reset(); cursor.advance(); continue; } // Candidates don't start with these characters, so we can skip ahead. - if matches!(cursor.curr(), b':' | b'"' | b'\'' | b'`') { + if matches!(cursor.curr, b':' | b'"' | b'\'' | b'`') { self.reset(); cursor.advance(); continue; @@ -56,7 +56,7 @@ impl Machine for CandidateMachine { // E.g.: `Some Class` // ^ ^ Invalid, we can jump ahead to the next boundary // - if matches!(cursor.curr(), b'<' | b'A'..=b'Z') { + if matches!(cursor.curr, b'<' | b'A'..=b'Z') { if let Some(offset) = cursor.input[cursor.pos..] .iter() .position(|&c| is_valid_before_boundary(&c)) diff --git a/crates/oxide/src/extractor/css_variable_machine.rs b/crates/oxide/src/extractor/css_variable_machine.rs index 7beb827ee..4e19c7111 100644 --- a/crates/oxide/src/extractor/css_variable_machine.rs +++ b/crates/oxide/src/extractor/css_variable_machine.rs @@ -20,7 +20,7 @@ impl Machine for CssVariableMachine { #[inline] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { // CSS Variables must start with `--` - if Class::Dash != cursor.curr().into() || Class::Dash != cursor.next().into() { + if Class::Dash != cursor.curr.into() || Class::Dash != cursor.next.into() { return MachineState::Idle; } @@ -30,11 +30,11 @@ impl Machine for CssVariableMachine { cursor.advance_twice(); while cursor.pos < len { - match cursor.curr().into() { + match cursor.curr.into() { // https://drafts.csswg.org/css-syntax-3/#ident-token-diagram // Class::AllowedCharacter | Class::Dash => { - match cursor.next().into() { + match cursor.next.into() { // Valid character followed by a valid character or an escape character // // E.g.: `--my-variable` @@ -58,7 +58,7 @@ impl Machine for CssVariableMachine { } } - Class::Escape => match cursor.next().into() { + Class::Escape => match cursor.next.into() { // An escaped whitespace character is not allowed // // In CSS it is allowed, but in the context of a class it's not because then we diff --git a/crates/oxide/src/extractor/mod.rs b/crates/oxide/src/extractor/mod.rs index a3d806a27..add0de4ac 100644 --- a/crates/oxide/src/extractor/mod.rs +++ b/crates/oxide/src/extractor/mod.rs @@ -86,7 +86,7 @@ impl<'a> Extractor<'a> { { let cursor = &mut self.cursor.clone(); while cursor.pos < len { - if cursor.curr().is_ascii_whitespace() { + if cursor.curr.is_ascii_whitespace() { cursor.advance(); continue; } @@ -104,7 +104,7 @@ impl<'a> Extractor<'a> { let cursor = &mut self.cursor.clone(); while cursor.pos < len { - if cursor.curr().is_ascii_whitespace() { + if cursor.curr.is_ascii_whitespace() { cursor.advance(); continue; } @@ -147,7 +147,7 @@ impl<'a> Extractor<'a> { let cursor = &mut self.cursor.clone(); while cursor.pos < len { - if cursor.curr().is_ascii_whitespace() { + if cursor.curr.is_ascii_whitespace() { cursor.advance(); continue; } @@ -238,7 +238,7 @@ mod tests { use std::hint::black_box; fn pre_process_input(input: &str, extension: &str) -> String { - let input = crate::scanner::pre_process_input(input.as_bytes().to_vec(), extension); + let input = crate::scanner::pre_process_input(input.as_bytes(), extension); String::from_utf8(input).unwrap() } @@ -908,18 +908,6 @@ mod tests { ); } - // https://github.com/tailwindlabs/tailwindcss/issues/20233 - #[test] - fn test_template_toolkit_syntax() { - assert_extract_candidates_contains( - &pre_process_input( - r#"
"#, - "tx", - ), - vec!["bg-white/40", "bg-white/10"], - ); - } - // https://github.com/tailwindlabs/tailwindcss/issues/17050 #[test] fn test_haml_syntax() { diff --git a/crates/oxide/src/extractor/modifier_machine.rs b/crates/oxide/src/extractor/modifier_machine.rs index 811bc9ded..22668a828 100644 --- a/crates/oxide/src/extractor/modifier_machine.rs +++ b/crates/oxide/src/extractor/modifier_machine.rs @@ -31,14 +31,14 @@ impl Machine for ModifierMachine { #[inline] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { // A modifier must start with a `/`, everything else is not a valid start of a modifier - if Class::Slash != cursor.curr().into() { + if Class::Slash != cursor.curr.into() { return MachineState::Idle; } let start_pos = cursor.pos; cursor.advance(); - match cursor.curr().into() { + match cursor.curr.into() { // Start of an arbitrary value: // // ``` @@ -70,9 +70,9 @@ impl Machine for ModifierMachine { Class::ValidStart => { let len = cursor.input.len(); while cursor.pos < len { - match cursor.curr().into() { + match cursor.curr.into() { Class::ValidStart | Class::ValidInside => { - match cursor.next().into() { + match cursor.next.into() { // Only valid characters are allowed, if followed by another valid character Class::ValidStart | Class::ValidInside => cursor.advance(), diff --git a/crates/oxide/src/extractor/named_utility_machine.rs b/crates/oxide/src/extractor/named_utility_machine.rs index 24dc027b2..7c847a2e2 100644 --- a/crates/oxide/src/extractor/named_utility_machine.rs +++ b/crates/oxide/src/extractor/named_utility_machine.rs @@ -52,8 +52,8 @@ impl Machine for NamedUtilityMachine { #[inline] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { - match cursor.curr().into() { - Class::AlphaLower => match cursor.next().into() { + match cursor.curr.into() { + Class::AlphaLower => match cursor.next.into() { // Valid single character utility in between quotes // // E.g.: `
` @@ -90,7 +90,7 @@ impl Machine for NamedUtilityMachine { // // E.g.: `-mx-2.5` // ^^ - Class::Dash => match cursor.next().into() { + Class::Dash => match cursor.next.into() { Class::AlphaLower => { self.start_pos = cursor.pos; cursor.advance(); @@ -118,7 +118,7 @@ impl Machine for NamedUtilityMachine { let len = cursor.input.len(); while cursor.pos < len { - match cursor.curr().into() { + match cursor.curr.into() { // Followed by a boundary character, we are at the end of the utility. // // E.g.: `'flex'` @@ -132,14 +132,14 @@ impl Machine for NamedUtilityMachine { // E.g.: `:div="{ flex: true }"` (JavaScript object syntax) // ^ Class::AlphaLower | Class::AlphaUpper => { - if is_valid_after_boundary(&cursor.next()) || { + if is_valid_after_boundary(&cursor.next) || { // Or any of these characters // // - `:`, because of JS object keys // - `/`, because of modifiers // - `!`, because of important matches!( - cursor.next().into(), + cursor.next.into(), Class::Colon | Class::Slash | Class::Exclamation ) } { @@ -150,7 +150,7 @@ impl Machine for NamedUtilityMachine { cursor.advance() } - Class::Dash => match cursor.next().into() { + Class::Dash => match cursor.next.into() { // Start of an arbitrary value // // E.g.: `bg-[#0088cc]` @@ -196,7 +196,7 @@ impl Machine for NamedUtilityMachine { _ => return self.restart(), }, - Class::Underscore => match cursor.next().into() { + Class::Underscore => match cursor.next.into() { // Valid characters _if_ followed by another valid character. These characters are // only valid inside of the utility but not at the end of the utility. // @@ -225,14 +225,14 @@ impl Machine for NamedUtilityMachine { // ^ // E.g.: `:div="{ flex: true }"` (JavaScript object syntax) // ^ - _ if is_valid_after_boundary(&cursor.next()) || { + _ if is_valid_after_boundary(&cursor.next) || { // Or any of these characters // // - `:`, because of JS object keys // - `/`, because of modifiers // - `!`, because of important matches!( - cursor.next().into(), + cursor.next.into(), Class::Colon | Class::Slash | Class::Exclamation ) } => @@ -249,11 +249,11 @@ impl Machine for NamedUtilityMachine { // E.g.: `px-2.5` // ^^^ Class::Dot => { - if !matches!(cursor.prev().into(), Class::Number) { + if !matches!(cursor.prev.into(), Class::Number) { return self.restart(); } - if !matches!(cursor.next().into(), Class::Number) { + if !matches!(cursor.next.into(), Class::Number) { return self.restart(); } @@ -278,19 +278,18 @@ impl Machine for NamedUtilityMachine { // Class::Number => { if !matches!( - cursor.prev().into(), + cursor.prev.into(), Class::Dash | Class::Underscore | Class::Dot | Class::Number | Class::AlphaLower - | Class::AlphaUpper ) { return self.restart(); } if !matches!( - cursor.next().into(), + cursor.next.into(), Class::Dot | Class::Number | Class::AlphaLower @@ -314,7 +313,7 @@ impl Machine for NamedUtilityMachine { // ^^ // ``` Class::Percent => { - if !matches!(cursor.prev().into(), Class::Number) { + if !matches!(cursor.prev.into(), Class::Number) { return self.restart(); } @@ -426,8 +425,6 @@ mod tests { // With number followed by dash or underscore ("text-title1-strong", vec!["text-title1-strong"]), ("text-title1_strong", vec!["text-title1_strong"]), - // With capital letter followed by number - ("text-titleV1-strong", vec!["text-titleV1-strong"]), // With trailing % sign ("from-15%", vec!["from-15%"]), // Arbitrary value with bracket notation diff --git a/crates/oxide/src/extractor/named_variant_machine.rs b/crates/oxide/src/extractor/named_variant_machine.rs index 285fe6ab5..b6de8220b 100644 --- a/crates/oxide/src/extractor/named_variant_machine.rs +++ b/crates/oxide/src/extractor/named_variant_machine.rs @@ -81,8 +81,8 @@ impl Machine for NamedVariantMachine { #[inline(always)] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { - match cursor.curr().into() { - Class::AlphaLower | Class::Star => match cursor.next().into() { + match cursor.curr.into() { + Class::AlphaLower | Class::Star => match cursor.next.into() { // Valid single character variant, must be followed by a `:` // // E.g.: `
` @@ -134,8 +134,8 @@ impl Machine for NamedVariantMachine { let len = cursor.input.len(); while cursor.pos < len { - match cursor.curr().into() { - Class::Dash => match cursor.next().into() { + match cursor.curr.into() { + Class::Dash => match cursor.next.into() { // Start of an arbitrary value // // E.g.: `data-[state=pending]:`. @@ -192,7 +192,7 @@ impl Machine for NamedVariantMachine { }; } - Class::Underscore => match cursor.next().into() { + Class::Underscore => match cursor.next.into() { // Valid characters _if_ followed by another valid character. These characters are // only valid inside of the variant but not at the end of the variant. // @@ -240,11 +240,11 @@ impl Machine for NamedVariantMachine { // E.g.: `2.5xl:flex` // ^^^ Class::Dot => { - if !matches!(cursor.prev().into(), Class::Number) { + if !matches!(cursor.prev.into(), Class::Number) { return self.restart(); } - if !matches!(cursor.next().into(), Class::Number) { + if !matches!(cursor.next.into(), Class::Number) { return self.restart(); } @@ -263,7 +263,7 @@ impl Machine for NamedVariantMachine { impl NamedVariantMachine { #[inline(always)] fn parse_arbitrary_end(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { - match cursor.next().into() { + match cursor.next.into() { Class::Slash => { cursor.advance(); self.transition::().next(cursor) @@ -285,7 +285,7 @@ impl Machine for NamedVariantMachine { fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { match self.modifier_machine.next(cursor) { MachineState::Idle => self.restart(), - MachineState::Done(_) => match cursor.next().into() { + MachineState::Done(_) => match cursor.next.into() { // Modifier must be followed by a `:` // // E.g.: `group-hover/name:` @@ -308,7 +308,7 @@ impl Machine for NamedVariantMachine { #[inline(always)] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { - match cursor.curr().into() { + match cursor.curr.into() { // The end of a variant must be the `:` // // E.g.: `hover:` diff --git a/crates/oxide/src/extractor/pre_processors/clojure.rs b/crates/oxide/src/extractor/pre_processors/clojure.rs index 3fb4ce601..ecf02b4ff 100644 --- a/crates/oxide/src/extractor/pre_processors/clojure.rs +++ b/crates/oxide/src/extractor/pre_processors/clojure.rs @@ -32,14 +32,14 @@ impl PreProcessor for Clojure { let mut cursor = cursor::Cursor::new(&content); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Consume strings as-is b'"' => { result[cursor.pos] = b' '; cursor.advance(); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Escaped character, skip ahead to the next character b'\\' => cursor.advance_twice(), @@ -57,8 +57,8 @@ impl PreProcessor for Clojure { // Discard line comments until the end of the line. // Comments start with `;;` - b';' if matches!(cursor.next(), b';') => { - while cursor.pos < len && cursor.curr() != b'\n' { + b';' if matches!(cursor.next, b';') => { + while cursor.pos < len && cursor.curr != b'\n' { result[cursor.pos] = b' '; cursor.advance(); } @@ -70,7 +70,7 @@ impl PreProcessor for Clojure { cursor.advance(); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // A `.` surrounded by digits is a decimal number, so we don't want to replace it. // // E.g.: @@ -78,8 +78,8 @@ impl PreProcessor for Clojure { // gap-1.5 // ^ // ``` - b'.' if cursor.prev().is_ascii_digit() - && cursor.next().is_ascii_digit() => + b'.' if cursor.prev.is_ascii_digit() + && cursor.next.is_ascii_digit() => { // Keep the `.` as-is } @@ -95,7 +95,7 @@ impl PreProcessor for Clojure { result[cursor.pos] = b' '; } // End of keyword. - _ if !is_keyword_character(cursor.curr()) => { + _ if !is_keyword_character(cursor.curr) => { result[cursor.pos] = b' '; break; } @@ -110,11 +110,11 @@ impl PreProcessor for Clojure { // Handle quote with a list, e.g.: `'(…)` // and with a vector, e.g.: `'[…]` - b'\'' if matches!(cursor.next(), b'[' | b'(') => { + b'\'' if matches!(cursor.next, b'[' | b'(') => { result[cursor.pos] = b' '; cursor.advance(); result[cursor.pos] = b' '; - let end = match cursor.curr() { + let end = match cursor.curr { b'[' => b']', b'(' => b')', _ => unreachable!(), @@ -122,7 +122,7 @@ impl PreProcessor for Clojure { // Consume until the closing `]` while cursor.pos < len { - match cursor.curr() { + match cursor.curr { x if x == end => { result[cursor.pos] = b' '; break; @@ -134,7 +134,7 @@ impl PreProcessor for Clojure { cursor.advance(); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Escaped character, skip ahead to the next character b'\\' => cursor.advance_twice(), @@ -157,14 +157,14 @@ impl PreProcessor for Clojure { } // Handle quote with a keyword, e.g.: `'bg-white` - b'\'' if !cursor.next().is_ascii_whitespace() => { + b'\'' if !cursor.next.is_ascii_whitespace() => { result[cursor.pos] = b' '; cursor.advance(); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // End of keyword. - _ if !is_keyword_character(cursor.curr()) => { + _ if !is_keyword_character(cursor.curr) => { result[cursor.pos] = b' '; break; } diff --git a/crates/oxide/src/extractor/pre_processors/elixir.rs b/crates/oxide/src/extractor/pre_processors/elixir.rs index 7737a6b2d..87b89a2a9 100644 --- a/crates/oxide/src/extractor/pre_processors/elixir.rs +++ b/crates/oxide/src/extractor/pre_processors/elixir.rs @@ -13,13 +13,13 @@ impl PreProcessor for Elixir { while cursor.pos < content.len() { // Look for a sigil marker - if cursor.curr() != b'~' { + if cursor.curr != b'~' { cursor.advance(); continue; } // Scan charlists, strings, and wordlists - if !matches!(cursor.next(), b'c' | b'C' | b's' | b'S' | b'w' | b'W') { + if !matches!(cursor.next, b'c' | b'C' | b's' | b'S' | b'w' | b'W') { cursor.advance(); continue; } @@ -27,7 +27,7 @@ impl PreProcessor for Elixir { cursor.advance_twice(); // Match the opening for a sigil - if !matches!(cursor.curr(), b'(' | b'[' | b'{') { + if !matches!(cursor.curr, b'(' | b'[' | b'{') { continue; } @@ -35,19 +35,19 @@ impl PreProcessor for Elixir { result[cursor.pos] = b' '; // Scan until we find a balanced closing one and replace it too - bracket_stack.push(cursor.curr()); + bracket_stack.push(cursor.curr); while cursor.pos < content.len() { cursor.advance(); - match cursor.curr() { + match cursor.curr { // Escaped character, skip ahead to the next character b'\\' => cursor.advance_twice(), b'(' | b'[' | b'{' => { - bracket_stack.push(cursor.curr()); + bracket_stack.push(cursor.curr); } b')' | b']' | b'}' if !bracket_stack.is_empty() => { - bracket_stack.pop(cursor.curr()); + bracket_stack.pop(cursor.curr); if bracket_stack.is_empty() { // Replace the closing bracket with a space diff --git a/crates/oxide/src/extractor/pre_processors/haml.rs b/crates/oxide/src/extractor/pre_processors/haml.rs index ec8be0817..a131618ab 100644 --- a/crates/oxide/src/extractor/pre_processors/haml.rs +++ b/crates/oxide/src/extractor/pre_processors/haml.rs @@ -101,7 +101,7 @@ impl PreProcessor for Haml { let mut last_newline_position = 0; while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Escape the next character b'\\' => { cursor.advance_twice(); @@ -119,7 +119,7 @@ impl PreProcessor for Haml { b'-' if cursor.input[last_newline_position..cursor.pos] .iter() .all(u8::is_ascii_whitespace) - && matches!(cursor.next(), b'#') => + && matches!(cursor.next, b'#') => { // Just consume the comment let updated_last_newline_position = @@ -159,7 +159,7 @@ impl PreProcessor for Haml { // Override the last known newline position last_newline_position = end; - let replaced = pre_process_input(ruby_code.to_vec(), "rb"); + let replaced = pre_process_input(ruby_code, "rb"); result.replace_range(start..end, replaced); } @@ -190,7 +190,7 @@ impl PreProcessor for Haml { // digit. // E.g.: `bg-red-500.2xl:flex` // ^^^ - if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() { + if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() { let mut next_cursor = cursor.clone(); next_cursor.advance(); @@ -203,81 +203,6 @@ impl PreProcessor for Haml { } } - // Handle Ruby syntax with `%w[]` arrays embedded in Haml attribute hashes. E.g.: - // - // ```haml - // %div{class: %w[bg-blue-500 w-10 h-10]} - // ``` - // - // A `%` that follows a value is not a percent literal. E.g.: the `50%w` in - // `hit rate 50%w.` - b'%' if matches!(cursor.next(), b'w' | b'W') - && !cursor.prev().is_ascii_alphanumeric() - && !matches!(cursor.prev(), b'_' | b')' | b']' | b'}') => - { - // Boundary characters - let (open, close) = match cursor.input.get(cursor.pos + 2) { - Some(b'[') => (b'[', b']'), - Some(b'(') => (b'(', b')'), - Some(b'{') => (b'{', b'}'), - Some(b'<') => (b'<', b'>'), - - // Any other ASCII punctuation can be used as a custom delimiter - Some(&c) if c.is_ascii_punctuation() => (c, c), - - // Everything else is not a valid delimiter - _ => { - cursor.advance(); - continue; - } - }; - - result[cursor.pos] = b' '; // Replace `%` - cursor.advance(); - result[cursor.pos] = b' '; // Replace `w` - cursor.advance(); - result[cursor.pos] = b' '; // Replace the opening delimiter - cursor.advance(); - - // Paired delimiters can be nested as long as they are balanced. E.g.: - // `%w[foo[bar]baz]` produces a flat array. - let mut depth = 1_usize; - - while cursor.pos < len { - match cursor.curr() { - // Skip escaped characters, unless the backslash is the delimiter - // itself - b'\\' if close != b'\\' => { - // Use backslash to embed spaces in the strings. - if cursor.next() == b' ' { - result[cursor.pos] = b' '; - } - - cursor.advance(); - } - - // Start of a nested delimiter pair - c if c == open && open != close => depth += 1, - - // Closing delimiter - c if c == close => { - depth -= 1; - - // End of the literal, replace the closing delimiter with a space - if depth == 0 { - result[cursor.pos] = b' '; - break; - } - } - - // Everything else is valid content - _ => {} - } - - cursor.advance(); - } - } - // Replace following characters with spaces if they are not inside of brackets b'#' | b'=' if bracket_stack.is_empty() => { result[cursor.pos] = b' '; @@ -288,11 +213,11 @@ impl PreProcessor for Haml { if bracket_stack.is_empty() { result[cursor.pos] = b' '; } - bracket_stack.push(cursor.curr()); + bracket_stack.push(cursor.curr); } b')' | b']' | b'}' if !bracket_stack.is_empty() => { - bracket_stack.pop(cursor.curr()); + bracket_stack.pop(cursor.curr); // Replace closing bracket with a space if bracket_stack.is_empty() { @@ -332,7 +257,7 @@ impl Haml { // :url => { :action => "add", :id => product.id }, // :update => { :success => "cart", :failure => "error" } // ``` - let evaluation_type = cursor.curr(); + let evaluation_type = cursor.curr; let block_indentation_level = cursor .pos @@ -342,17 +267,17 @@ impl Haml { let mut last_newline_position = last_known_newline_position; // Consume until the end of the line first - while cursor.pos < len && cursor.curr() != b'\n' { + while cursor.pos < len && cursor.curr != b'\n' { cursor.advance(); } // Block is already done, aka just a line - if evaluation_type == b'=' && cursor.prev() != b',' { + if evaluation_type == b'=' && cursor.prev != b',' { return cursor.pos; } 'outer: while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Escape the next character b'\\' => { cursor.advance_twice(); @@ -364,7 +289,7 @@ impl Haml { last_newline_position = cursor.pos; // We are done with this block - if evaluation_type == b'=' && cursor.prev() != b',' { + if evaluation_type == b'=' && cursor.prev != b',' { break; } @@ -375,11 +300,11 @@ impl Haml { // Skip whitespace and compute the indentation level x if x.is_ascii_whitespace() => { // Find first non-whitespace character - while cursor.pos < len && cursor.curr().is_ascii_whitespace() { - if cursor.curr() == b'\n' { + while cursor.pos < len && cursor.curr.is_ascii_whitespace() { + if cursor.curr == b'\n' { last_newline_position = cursor.pos; - if evaluation_type == b'=' && cursor.prev() != b',' { + if evaluation_type == b'=' && cursor.prev != b',' { // We are done with this block break 'outer; } @@ -499,81 +424,6 @@ mod tests { ); } - // https://github.com/tailwindlabs/tailwindcss/issues/20386 - #[test] - fn test_embedded_ruby_percent_w_delimiters() { - for (input, expected) in [ - // %w[…] in an attribute hash - ( - "%div{class: %w[flex px-2.5]}", - "%div class: flex px-2.5 ", - ), - // %w<…> - ( - "%div{class: %w}", - "%div class: flex px-2.5 ", - ), - // Nested `<…>` does not end the literal - ( - "%div{class: %w px-2.5>}", - "%div class: flex 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} - %div{class: %w|underline font-bold|} - - classes = %w - "#; - - 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() { diff --git a/crates/oxide/src/extractor/pre_processors/json.rs b/crates/oxide/src/extractor/pre_processors/json.rs index cd457050c..809eb501e 100644 --- a/crates/oxide/src/extractor/pre_processors/json.rs +++ b/crates/oxide/src/extractor/pre_processors/json.rs @@ -11,13 +11,13 @@ impl PreProcessor for Json { let mut cursor = cursor::Cursor::new(content); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Consume strings as-is b'"' => { cursor.advance(); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Escaped character, skip ahead to the next character b'\\' => cursor.advance_twice(), diff --git a/crates/oxide/src/extractor/pre_processors/markdown.rs b/crates/oxide/src/extractor/pre_processors/markdown.rs index 488614ee5..63dc37d1e 100644 --- a/crates/oxide/src/extractor/pre_processors/markdown.rs +++ b/crates/oxide/src/extractor/pre_processors/markdown.rs @@ -9,27 +9,20 @@ impl PreProcessor for Markdown { let len = content.len(); let mut result = content.to_vec(); let mut cursor = cursor::Cursor::new(content); - let mut bracket_stack = vec![]; let mut in_directive = false; while cursor.pos < len { - match (in_directive, cursor.curr()) { + match (in_directive, cursor.curr) { (false, b'{') => { result[cursor.pos] = b' '; in_directive = true; } - (true, b'(' | b'[' | b'{' | b'<') => { - bracket_stack.push(cursor.curr()); - } - (true, b')' | b']' | b'}' | b'>') if !bracket_stack.is_empty() => { - bracket_stack.pop(); - } (true, b'}') => { result[cursor.pos] = b' '; in_directive = false; } - (true, b'.') if bracket_stack.is_empty() => { + (true, b'.') => { result[cursor.pos] = b' '; } _ => {} @@ -67,17 +60,4 @@ mod tests { Markdown::test(input, expected); } } - - #[test] - fn test_nested_classes_keep_the_dots() { - for (input, expected) in [ - ( - r#"{
}"#, - r#"
"#, - ), - (r#"{content-['example.js']}"#, r#" content-['example.js'] "#), - ] { - Markdown::test(input, expected); - } - } } diff --git a/crates/oxide/src/extractor/pre_processors/mod.rs b/crates/oxide/src/extractor/pre_processors/mod.rs index 1a04af5c9..efcbc53d8 100644 --- a/crates/oxide/src/extractor/pre_processors/mod.rs +++ b/crates/oxide/src/extractor/pre_processors/mod.rs @@ -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::*; diff --git a/crates/oxide/src/extractor/pre_processors/pre_processor.rs b/crates/oxide/src/extractor/pre_processors/pre_processor.rs index 8e02b8f69..8d42e744a 100644 --- a/crates/oxide/src/extractor/pre_processors/pre_processor.rs +++ b/crates/oxide/src/extractor/pre_processors/pre_processor.rs @@ -25,33 +25,7 @@ pub trait PreProcessor: Sized + Default { } #[cfg(test)] - fn test_extract_exact(input: &str, expected: Vec<&str>) { - use crate::extractor::{Extracted, Extractor}; - - let input = input.as_bytes(); - - let processor = Self::default(); - let transformed = processor.process(input); - - let extracted = Extractor::new(&transformed).extract(); - - // Extract all candidates and css variables. - let candidates = extracted - .iter() - .filter_map(|x| match x { - Extracted::Candidate(bytes) => std::str::from_utf8(bytes).ok(), - Extracted::CssVariable(bytes) => std::str::from_utf8(bytes).ok(), - }) - .collect::>(); - - if candidates != expected { - dbg!(&candidates, &expected); - panic!("Extracted candidates do not match expected candidates"); - } - } - - #[cfg(test)] - fn test_extract_contains(input: &str, expected: Vec<&str>) { + fn test_extract_contains(input: &str, items: Vec<&str>) { use crate::extractor::{Extracted, Extractor}; let input = input.as_bytes(); @@ -72,7 +46,7 @@ pub trait PreProcessor: Sized + Default { // Ensure all items are present in the candidates. let mut missing = vec![]; - for item in &expected { + for item in &items { if !candidates.contains(item) { missing.push(item); } diff --git a/crates/oxide/src/extractor/pre_processors/pug.rs b/crates/oxide/src/extractor/pre_processors/pug.rs index 70dfe5683..7b44fcf5b 100644 --- a/crates/oxide/src/extractor/pre_processors/pug.rs +++ b/crates/oxide/src/extractor/pre_processors/pug.rs @@ -15,7 +15,7 @@ impl PreProcessor for Pug { let mut bracket_stack = BracketStack::default(); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Only replace `.` with a space if it's not surrounded by numbers. E.g.: // // ```diff @@ -43,7 +43,7 @@ impl PreProcessor for Pug { // digit. // E.g.: `bg-red-500.2xl:flex` // ^^^ - if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() { + if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() { let mut next_cursor = cursor.clone(); next_cursor.advance(); @@ -68,17 +68,17 @@ impl PreProcessor for Pug { // // However, we also need to make sure that we keep the parens that are part of the // utility class. E.g.: `bg-(--my-color)`. - b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => { + b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => { result[cursor.pos] = b' '; - bracket_stack.push(cursor.curr()); + bracket_stack.push(cursor.curr); } b'(' | b'[' | b'{' => { - bracket_stack.push(cursor.curr()); + bracket_stack.push(cursor.curr); } b')' | b']' | b'}' if !bracket_stack.is_empty() => { - bracket_stack.pop(cursor.curr()); + bracket_stack.pop(cursor.curr); } // Consume everything else diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index d9a57ab77..6bb1d4923 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -34,135 +34,51 @@ impl PreProcessor for Ruby { // Extract embedded template languages // https://viewcomponent.org/guide/templates.html#interpolations - // Only process if content is valid UTF-8, otherwise skip HEREDOC extraction - // but still perform the byte-level Ruby processing below - if let Ok(content_as_str) = std::str::from_utf8(content) { - let starts = TEMPLATE_START_REGEX - .captures_iter(content_as_str) - .collect::>(); - let ends = TEMPLATE_END_REGEX - .captures_iter(content_as_str) - .collect::>(); + 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::>(); + let ends = TEMPLATE_END_REGEX + .captures_iter(content_as_str) + .collect::>(); - // The HEREDOC delimiter - let delimiter_start = start.get(2).unwrap().as_str(); + for start in starts.iter() { + // The language for this block + let lang = start.get(1).unwrap().as_str(); - // Where the "body" starts for the HEREDOC block - let body_start = start.get(0).unwrap().end(); + // The HEREDOC delimiter + let delimiter_start = start.get(2).unwrap().as_str(); - // Look through all of the ends to find a matching language - for end in ends.iter() { - // 1. This must appear after the start - let body_end = end.get(0).unwrap().start(); - if body_end < body_start { - continue; - } + // Where the "body" starts for the HEREDOC block + let body_start = start.get(0).unwrap().end(); - // The languages must match otherwise we haven't found the end - let delimiter_end = end.get(1).unwrap().as_str(); - if delimiter_end != delimiter_start { - continue; - } - - let body = &content_as_str[body_start..body_end]; - let replaced = - pre_process_input(body.as_bytes().to_vec(), &lang.to_ascii_lowercase()); - - result.replace_range(body_start..body_end, replaced); - break; + // Look through all of the ends to find a matching language + for end in ends.iter() { + // 1. This must appear after the start + let body_end = end.get(0).unwrap().start(); + if body_end < body_start { + continue; } + + // The languages must match otherwise we haven't found the end + let delimiter_end = end.get(1).unwrap().as_str(); + if delimiter_end != delimiter_start { + continue; + } + + let body = &content_as_str[body_start..body_end]; + let replaced = pre_process_input(body.as_bytes(), &lang.to_ascii_lowercase()); + + result.replace_range(body_start..body_end, replaced); + break; } } // Ruby extraction while cursor.pos < len { - match cursor.curr() { - b'"' => { - cursor.advance(); - - while cursor.pos < len { - match cursor.curr() { - // Escaped character, skip ahead to the next character - b'\\' => cursor.advance_twice(), - - // End of the string - b'"' => break, - - // Everything else is valid - _ => cursor.advance(), - }; - } - - cursor.advance(); - continue; - } - - b'\'' => { - cursor.advance(); - - while cursor.pos < len { - match cursor.curr() { - // Escaped character, skip ahead to the next character - b'\\' => cursor.advance_twice(), - - // End of the string - b'\'' => break, - - // Everything else is valid - _ => cursor.advance(), - }; - } - - cursor.advance(); - continue; - } - - // Replace comments in Ruby files - // - // Except for strict locals, these are defined in a `<%# locals: … %>`. Checking if - // the comment is preceded by a `%` should be enough without having to perform more - // parsing logic. Worst case we _do_ scan a few comments. - // - // We also want to skip interpolation syntax, which look like `#{…}`. - b'#' if !matches!(cursor.prev(), b'%') && !matches!(cursor.next(), b'{') => { - result[cursor.pos] = b' '; - cursor.advance(); - - while cursor.pos < len { - match cursor.curr() { - // End of the comment - b'\n' => break, - - // Everything else is part of the comment and replaced - _ => { - result[cursor.pos] = b' '; - cursor.advance(); - } - }; - } - - cursor.advance(); - continue; - } - - _ => {} - } - - // Looking for `%w`, `%W`, or `%p` - if cursor.curr() != b'%' || !matches!(cursor.next(), b'w' | b'W' | b'p') { - cursor.advance(); - continue; - } - - // A `%` that follows a value is a modulo operation, not a percent literal. E.g.: the - // `50%w` in `hit rate 50%w.` - if cursor.prev().is_ascii_alphanumeric() - || matches!(cursor.prev(), b'_' | b')' | b']' | b'}') - { + // Looking for `%w` or `%W` + if cursor.curr != b'%' && !matches!(cursor.next, b'w' | b'W') { cursor.advance(); continue; } @@ -170,17 +86,10 @@ impl PreProcessor for Ruby { cursor.advance_twice(); // Boundary character - let boundary = match cursor.curr() { + let boundary = match cursor.curr { b'[' => b']', b'(' => b')', b'{' => b'}', - b'<' => b'>', - b' ' => b'\n', - - // Any other ASCII punctuation can be used as a custom delimiter - c if c.is_ascii_punctuation() => c, - - // Everything else is not a valid delimiter _ => { cursor.advance(); continue; @@ -196,11 +105,11 @@ impl PreProcessor for Ruby { cursor.advance(); while cursor.pos < len { - match cursor.curr() { - // Skip escaped characters, unless the backslash is the delimiter itself - b'\\' if boundary != b'\\' => { + match cursor.curr { + // Skip escaped characters + b'\\' => { // Use backslash to embed spaces in the strings. - if cursor.next() == b' ' { + if cursor.next == b' ' { result[cursor.pos] = b' '; } @@ -209,36 +118,20 @@ impl PreProcessor for Ruby { // Start of a nested bracket b'[' | b'(' | b'{' => { - bracket_stack.push(cursor.curr()); - } - - // Start of a nested `<…>`, which Ruby allows inside a `%w<…>` literal - b'<' if boundary == b'>' => { - bracket_stack.push(cursor.curr()); + bracket_stack.push(cursor.curr); } // End of a nested bracket b']' | b')' | b'}' if !bracket_stack.is_empty() => { - if !bracket_stack.pop(cursor.curr()) { - // Unbalanced - cursor.advance(); - } - } - - // End of a nested `<…>` - b'>' if boundary == b'>' && !bracket_stack.is_empty() => { - if !bracket_stack.pop(cursor.curr()) { + if !bracket_stack.pop(cursor.curr) { // Unbalanced cursor.advance(); } } // End of the pattern, replace the boundary character with a space - _ if cursor.curr() == boundary => { - if boundary != b'\n' { - result[cursor.pos] = b' '; - } - + _ if cursor.curr == boundary => { + result[cursor.pos] = b' '; break; } @@ -280,69 +173,12 @@ mod tests { "%w(flex data-[state=pending]:bg-(--my-color) flex-col)", "%w flex data-[state=pending]:bg-(--my-color) flex-col ", ), - // %w<…> - ("%w", "%w flex px-2.5 "), - ( - "%w", - "%w flex data-[state=pending]:bg-(--my-color) flex-col ", - ), - // Nested `<…>` does not end the literal - ("%w px-2.5>", "%w flex px-2.5 "), - // %w|…|, %w:…:, %w!…! - ("%w|flex px-2.5|", "%w flex px-2.5 "), - ("%w:flex px-2.5:", "%w flex px-2.5 "), - ("%w!flex px-2.5!", "%w flex px-2.5 "), - (r#"%w\flex px-2.5\"#, r#"%w flex px-2.5 "#), - // A `%` that follows a value is a modulo operation, not a percent literal - ( - "hit rate 50%w.\n%w[flex px-2.5]", - "hit rate 50%w.\n%w flex px-2.5 ", - ), - - // %w …\n - ("%w flex px-2.5\n", "%w flex px-2.5\n"), - // Use backslash to embed spaces in the strings. (r#"%w[foo\ bar baz\ bat]"#, r#"%w foo bar baz bat "#), (r#"%W[foo\ bar baz\ bat]"#, r#"%W foo bar baz bat "#), - // The nested delimiters evaluated to a flat array of strings // (not nested array). (r#"%w[foo[bar baz]qux]"#, r#"%w foo[bar baz]qux "#), - - ( - "# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]", - " \n \n \n%w flex px-2.5 " - ), - - (r#""foo # bar""#, r#""foo # bar""#), - (r#"'foo # bar'"#, r#"'foo # bar'"#), - ( - r#"def call = tag.span "Foo", class: %w[rounded-full h-0.75 w-0.75]"#, - r#"def call = tag.span "Foo", class: %w rounded-full h-0.75 w-0.75 "# - ), - - (r#"%w[foo ' bar]"#, r#"%w foo ' bar "#), - (r#"%w[foo " bar]"#, r#"%w foo " bar "#), - (r#"%W[foo ' bar]"#, r#"%W foo ' bar "#), - (r#"%W[foo " bar]"#, r#"%W foo " bar "#), - - (r#"%p foo ' bar "#, r#"%p foo ' bar "#), - (r#"%p foo " bar "#, r#"%p foo " bar "#), - - ( - "%p has a ' quote\n# this should be removed\n%p has a ' quote", - "%p has a ' quote\n \n%p has a ' quote" - ), - ( - "%p has a \" quote\n# this should be removed\n%p has a \" quote", - "%p has a \" quote\n \n%p has a \" quote" - ), - - ( - "%w#this text is kept# # this text is not", - "%w this text is kept ", - ), ] { Ruby::test(input, expected); } @@ -375,30 +211,6 @@ mod tests { "%w(flex data-[state=pending]:bg-(--my-color) flex-col)", vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"], ), - // %w<…> - ("%w", vec!["flex", "px-2.5"]), - ("%w", vec!["flex", "px-2.5"]), - ("%w<2xl:flex>", vec!["2xl:flex"]), - ( - "%w", - vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"], - ), - // Nested `<…>` does not end the literal - ("%w px-2.5>", vec!["flex", "px-2.5"]), - // %w|…|, %w:…:, %w!…! - ("%w|flex px-2.5|", vec!["flex", "px-2.5"]), - ("%w:flex px-2.5:", vec!["flex", "px-2.5"]), - ("%w!flex px-2.5!", vec!["flex", "px-2.5"]), - - ( - "# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]", - vec!["flex", "px-2.5"], - ), - - (r#""foo # bar""#, vec!["foo", "bar"]), - (r#"'foo # bar'"#, vec!["foo", "bar"]), - - (r#"%w[foo ' bar]"#, vec!["foo", "bar"]), ] { Ruby::test_extract_contains(input, expected); } @@ -450,79 +262,4 @@ mod tests { "#; Ruby::test_extract_contains(input, vec!["z-1", "z-2", "z-3"]); } - - // https://github.com/tailwindlabs/tailwindcss/issues/19728 - #[test] - fn test_interpolated_expressions() { - let input = r#" - def width_class(width = nil) - <<~STYLE_CLASS - #{width || 'w-100'} - STYLE_CLASS - end - "#; - - Ruby::test_extract_contains(input, vec!["w-100"]); - } - - // https://github.com/tailwindlabs/tailwindcss/issues/19239 - #[test] - fn test_skip_comments() { - let input = r#" - # From activerecord-8.1.1/lib/active_record/errors.rb:147 - # Rails uses RDoc cross-reference syntax in inline documentation: - # {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] - "#; - - // Nothing should be extracted from comments, so expect an empty array. - Ruby::test_extract_exact(input, vec![]); - } - - // https://github.com/tailwindlabs/tailwindcss/issues/19481 - #[test] - fn test_strict_locals() { - // Strict locals are defined in a `<%# locals: … %>`, but the `#` looks like a comment - // which we should not ignore in this case. - let input = r#" - <%# locals: (css: "text-amber-600") %> - <% more_css = "text-sky-500" %> - -

- In a partial -

- -

- In a partial using explicit local variables -

- -

- In a partial using explicit local variables -

- "#; - - Ruby::test_extract_contains( - input, - vec!["text-amber-600", "text-sky-500", "text-green-500"], - ); - } - - #[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"]); - } } diff --git a/crates/oxide/src/extractor/pre_processors/rust.rs b/crates/oxide/src/extractor/pre_processors/rust.rs index 4fa53f793..6404fffb5 100644 --- a/crates/oxide/src/extractor/pre_processors/rust.rs +++ b/crates/oxide/src/extractor/pre_processors/rust.rs @@ -33,7 +33,7 @@ impl Rust { let mut bracket_stack = bracket_stack::BracketStack::default(); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Escaped character, skip ahead to the next character b'\\' => { cursor.advance_twice(); @@ -46,7 +46,7 @@ impl Rust { cursor.advance(); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Escaped character, skip ahead to the next character b'\\' => cursor.advance_twice(), @@ -89,7 +89,7 @@ impl Rust { // digit. // E.g.: `bg-red-500.2xl:flex` // ^^^ - if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() { + if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() { let mut next_cursor = cursor.clone(); next_cursor.advance(); @@ -103,17 +103,11 @@ impl Rust { } b'[' => { - bracket_stack.push(cursor.curr()); - - // Handle `p.flex[condition]`. If there is a `-` before it, it will likely be an - // arbitrary value e.g. `text-[red]` - if !matches!(cursor.prev(), b'-') { - result[cursor.pos] = b' '; - } + bracket_stack.push(cursor.curr); } b']' if !bracket_stack.is_empty() => { - bracket_stack.pop(cursor.curr()); + bracket_stack.pop(cursor.curr); } // Consume everything else @@ -219,20 +213,4 @@ mod tests { let input = r#"html! { \x.px-4.text-black { } }"#; Rust::test(input, r#"html! { \x px-4 text-black { } }"#); } - - // https://github.com/tailwindlabs/tailwindcss/issues/20233 - #[test] - fn test_maud_template_extraction_with_conditional_classes() { - let input = r#" - use maud::{html, Markup}; - - pub fn main() -> Markup { - html! { - p.text-black[cuteness > 50] { "Squee!" } - } - } - "#; - - Rust::test_extract_contains(input, vec!["text-black"]); - } } diff --git a/crates/oxide/src/extractor/pre_processors/slim.rs b/crates/oxide/src/extractor/pre_processors/slim.rs index 5683858d5..6d41e5a09 100644 --- a/crates/oxide/src/extractor/pre_processors/slim.rs +++ b/crates/oxide/src/extractor/pre_processors/slim.rs @@ -15,7 +15,7 @@ impl PreProcessor for Slim { let mut bracket_stack = BracketStack::default(); while cursor.pos < len { - match cursor.curr() { + match cursor.curr { // Only replace `.` with a space if it's not surrounded by numbers. E.g.: // // ```diff @@ -43,7 +43,7 @@ impl PreProcessor for Slim { // digit. // E.g.: `bg-red-500.2xl:flex` // ^^^ - if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() { + if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() { let mut next_cursor = cursor.clone(); next_cursor.advance(); @@ -65,74 +65,17 @@ impl PreProcessor for Slim { // class=%w[bg-blue-500 w-10 h-10] // ] // ``` - // - // A `%` that follows a value is not a percent literal. E.g.: the `50%w` in - // `hit rate 50%w.` - b'%' if matches!(cursor.next(), b'w' | b'W') - && !cursor.prev().is_ascii_alphanumeric() - && !matches!(cursor.prev(), b'_' | b')' | b']' | b'}') => + b'%' if matches!(cursor.next, b'w' | b'W') + && matches!(cursor.input.get(cursor.pos + 2), Some(b'[' | b'(' | b'{')) => { - // Boundary characters - let (open, close) = match cursor.input.get(cursor.pos + 2) { - Some(b'[') => (b'[', b']'), - Some(b'(') => (b'(', b')'), - Some(b'{') => (b'{', b'}'), - Some(b'<') => (b'<', b'>'), - - // Any other ASCII punctuation can be used as a custom delimiter - Some(&c) if c.is_ascii_punctuation() => (c, c), - - // Everything else is not a valid delimiter - _ => { - cursor.advance(); - continue; - } - }; - result[cursor.pos] = b' '; // Replace `%` cursor.advance(); result[cursor.pos] = b' '; // Replace `w` cursor.advance(); - result[cursor.pos] = b' '; // Replace the opening delimiter - cursor.advance(); - - // Paired delimiters can be nested as long as they are balanced. E.g.: - // `%w[foo[bar]baz]` produces a flat array. - let mut depth = 1_usize; - - while cursor.pos < len { - match cursor.curr() { - // Skip escaped characters, unless the backslash is the delimiter - // itself - b'\\' if close != b'\\' => { - // Use backslash to embed spaces in the strings. - if cursor.next() == b' ' { - result[cursor.pos] = b' '; - } - - cursor.advance(); - } - - // Start of a nested delimiter pair - c if c == open && open != close => depth += 1, - - // Closing delimiter - c if c == close => { - depth -= 1; - - // End of the literal, replace the closing delimiter with a space - if depth == 0 { - result[cursor.pos] = b' '; - break; - } - } - - // Everything else is valid content - _ => {} - } - - cursor.advance(); - } + result[cursor.pos] = b' '; // Replace `[` or `(` or `{` + bracket_stack.push(cursor.curr); + cursor.advance(); // Move past the bracket + continue; } // Any `[` preceded by an alphanumeric value will not be part of a candidate. @@ -153,10 +96,10 @@ impl PreProcessor for Slim { // Instead of listing all boundary characters, let's list the characters we know // will be invalid instead. b'[' if bracket_stack.is_empty() - && matches!(cursor.prev(), b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') => + && matches!(cursor.prev, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') => { result[cursor.pos] = b' '; - bracket_stack.push(cursor.curr()); + bracket_stack.push(cursor.curr); } // In Slim the class name shorthand can be followed by a parenthesis. E.g.: @@ -171,17 +114,17 @@ impl PreProcessor for Slim { // // However, we also need to make sure that we keep the parens that are part of the // utility class. E.g.: `bg-(--my-color)`. - b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => { + b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => { result[cursor.pos] = b' '; - bracket_stack.push(cursor.curr()); + bracket_stack.push(cursor.curr); } b'(' | b'[' | b'{' => { - bracket_stack.push(cursor.curr()); + bracket_stack.push(cursor.curr); } b')' | b']' | b'}' if !bracket_stack.is_empty() => { - bracket_stack.pop(cursor.curr()); + bracket_stack.pop(cursor.curr); } // Consume everything else @@ -373,77 +316,16 @@ mod tests { ] "#; - let expected = " - div \n class= bg-blue-500 w-10 h-10 \n ] - div \n class= w-10 bg-green-500 h-10 \n ] - "; + let expected = r#" + div + class= bg-blue-500 w-10 h-10] + ] + div + class= w-10 bg-green-500 h-10] + ] + "#; Slim::test(input, expected); Slim::test_extract_contains(input, vec!["bg-blue-500", "bg-green-500", "w-10", "h-10"]); } - - // https://github.com/tailwindlabs/tailwindcss/issues/20386 - #[test] - fn test_embedded_ruby_percent_w_delimiters() { - for (input, expected) in [ - // %w<…>, Slim only counts `[({` nesting in attribute values, so the code must be - // wrapped in parentheses to contain spaces - ( - "div class=(%w)", - "div class= bg-blue-500 w-10 h-10 )", - ), - // Nested `<…>` does not end the literal - ( - "div class=(%w px-2.5>)", - "div class= flex 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", "- 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) - ] - - 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", - ], - ); - } } diff --git a/crates/oxide/src/extractor/pre_processors/template_toolkit.rs b/crates/oxide/src/extractor/pre_processors/template_toolkit.rs deleted file mode 100644 index 6456a2694..000000000 --- a/crates/oxide/src/extractor/pre_processors/template_toolkit.rs +++ /dev/null @@ -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 { - 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#"
"#, - vec!["bg-white/40", "bg-white/10"], - ); - } -} diff --git a/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-17051.haml b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-17051.haml index 56a38cbe3..b233f3b8b 100644 --- a/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-17051.haml +++ b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-17051.haml @@ -7,6 +7,7 @@ .relative ^^^^^^^^ - # Blurred background star + ^^^^^^^^^^ ^^^^ .absolute.left-0.z-0{ class: "-top-[400px] -right-[400px]" } ^^^^^^^^ ^^^^^^ ^^^ ^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ .flex.justify-end.blur-3xl @@ -74,8 +75,8 @@ ^^^^^^^^^^ ^^^^^^ libraries, our components are designed to be fast, flexible, and easy to ^^^ ^^^^^^^^^^ ^^^ ^^^^^^^^ ^^ ^^ ^^^ ^^^^ ^^ - use directly in Ruby on Rails! - ^^^ ^^^^^^^^ ^^ ^^ + use directy in Ruby on Rails! + ^^^ ^^^^^^^ ^^ ^^ = doc_example(css: "mt-8 lg:mt-0 lg:basis-3/5 h-44") do ^^^ ^^^^ ^^^^^^^ ^^^^^^^^^^^^ ^^^^ ^^ @@ -101,7 +102,7 @@ %p.mt-2 ^^^^ - Utilize + Utlize = daisy_link("HAML", "https://haml.info/", target: "_blank") ^^^^^^ so your views are simple, concise, and easy to understand. @@ -119,8 +120,8 @@ %p.mt-2 ^^^^ :markdown - **PLUS!** You can utilize filters like _Markdown_, CoffeeScript, - ^^^ ^^^^^^^ ^^^^^^^ ^^^^ + **PLUS!** You can utlize filters like _Markdown_, CoffeeScript, + ^^^ ^^^^^^ ^^^^^^^ ^^^^ Textile, and many more! ^^^ ^^^^ ^^^^^ @@ -195,6 +196,7 @@ ^^^^^^^^ ^^^^ ^^^ ^^^^ ^^ :escaped - # app/components/character_component.html.haml + ^^^^ = part(:component) do ^^ = part(:head) diff --git a/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/src-17051.haml b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/src-17051.haml index 8f9b61941..38e17b134 100644 --- a/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/src-17051.haml +++ b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/src-17051.haml @@ -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 directly in Ruby on Rails! + use directy 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 diff --git a/crates/oxide/src/extractor/pre_processors/twig.rs b/crates/oxide/src/extractor/pre_processors/twig.rs deleted file mode 100644 index b6f4a3319..000000000 --- a/crates/oxide/src/extractor/pre_processors/twig.rs +++ /dev/null @@ -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 { - 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#" -
- -
- "#; - - let expected = r#" -
- -
- "#; - - Twig::test(input, expected); - Twig::test_extract_contains(input, vec!["opacity-50"]); - - // Inside a component - let input = r#" - - "#; - - let expected = r#" - - "#; - - 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", - ], - ); - } -} diff --git a/crates/oxide/src/extractor/pre_processors/vue.rs b/crates/oxide/src/extractor/pre_processors/vue.rs index ae037435c..119e2a3d2 100644 --- a/crates/oxide/src/extractor/pre_processors/vue.rs +++ b/crates/oxide/src/extractor/pre_processors/vue.rs @@ -15,15 +15,13 @@ impl PreProcessor for Vue { fn process(&self, content: &[u8]) -> Vec { let mut result = content.to_vec(); - // Only process template tags if content is valid UTF-8 - if let Ok(content_as_str) = std::str::from_utf8(content) { - for (_, [lang, body]) in TEMPLATE_REGEX - .captures_iter(content_as_str) - .map(|c| c.extract()) - { - let replaced = pre_process_input(body.as_bytes().to_vec(), lang); - result = result.replace(body, replaced); - } + let content_as_str = std::str::from_utf8(content).unwrap(); + for (_, [lang, body]) in TEMPLATE_REGEX + .captures_iter(content_as_str) + .map(|c| c.extract()) + { + let replaced = pre_process_input(body.as_bytes(), lang); + result = result.replace(body, replaced); } result @@ -45,16 +43,4 @@ mod tests { Vue::test_extract_contains(input, vec!["bg-neutral-900", "text-red-500"]); } - - #[test] - fn test_invalid_utf8_does_not_panic() { - // Invalid UTF-8 sequence: 0x80 is a continuation byte without a leading byte - let invalid_utf8: &[u8] = &[0x80, 0x81, 0x82]; - - let processor = Vue::default(); - - // Should not panic, just return the input unchanged - let result = processor.process(invalid_utf8); - assert_eq!(result, invalid_utf8); - } } diff --git a/crates/oxide/src/extractor/string_machine.rs b/crates/oxide/src/extractor/string_machine.rs index 4fad49faa..9148a1fbf 100644 --- a/crates/oxide/src/extractor/string_machine.rs +++ b/crates/oxide/src/extractor/string_machine.rs @@ -30,20 +30,20 @@ impl Machine for StringMachine { #[inline] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { - if Class::Quote != cursor.curr().into() { + if Class::Quote != cursor.curr.into() { return MachineState::Idle; } // Start of a string let len = cursor.input.len(); let start_pos = cursor.pos; - let end_char = cursor.curr(); + let end_char = cursor.curr; cursor.advance(); while cursor.pos < len { - match cursor.curr().into() { - Class::Escape => match cursor.next().into() { + match cursor.curr.into() { + Class::Escape => match cursor.next.into() { // An escaped whitespace character is not allowed Class::Whitespace => return MachineState::Idle, @@ -52,7 +52,7 @@ impl Machine for StringMachine { }, // End of the string - Class::Quote if cursor.curr() == end_char => return self.done(start_pos, cursor), + Class::Quote if cursor.curr == end_char => return self.done(start_pos, cursor), // Any kind of whitespace is not allowed Class::Whitespace => return MachineState::Idle, diff --git a/crates/oxide/src/extractor/utility_machine.rs b/crates/oxide/src/extractor/utility_machine.rs index c116ec4e4..b9b1038cd 100644 --- a/crates/oxide/src/extractor/utility_machine.rs +++ b/crates/oxide/src/extractor/utility_machine.rs @@ -27,12 +27,12 @@ impl Machine for UtilityMachine { #[inline] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { - match cursor.curr().into() { + match cursor.curr.into() { // LEGACY: Important marker Class::Exclamation => { self.legacy_important = true; - match cursor.next().into() { + match cursor.next.into() { // Start of an arbitrary property // // E.g.: `![color:red]` @@ -78,7 +78,7 @@ impl UtilityMachine { fn parse_arbitrary_property(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { match self.arbitrary_property_machine.next(cursor) { MachineState::Idle => self.restart(), - MachineState::Done(_) => match cursor.next().into() { + MachineState::Done(_) => match cursor.next.into() { // End of arbitrary property, but there is a potential modifier. // // E.g.: `[color:#0088cc]/` @@ -109,7 +109,7 @@ impl UtilityMachine { fn parse_named_utility(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { match self.named_utility_machine.next(cursor) { MachineState::Idle => self.restart(), - MachineState::Done(_) => match cursor.next().into() { + MachineState::Done(_) => match cursor.next.into() { // End of a named utility, but there is a potential modifier. // // E.g.: `bg-red-500/` @@ -140,7 +140,7 @@ impl UtilityMachine { fn parse_modifier(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { match self.modifier_machine.next(cursor) { MachineState::Idle => self.restart(), - MachineState::Done(_) => match cursor.next().into() { + MachineState::Done(_) => match cursor.next.into() { // A modifier followed by a modifier is invalid Class::Slash => self.restart(), diff --git a/crates/oxide/src/extractor/variant_machine.rs b/crates/oxide/src/extractor/variant_machine.rs index 9ec2adc09..23aa61798 100644 --- a/crates/oxide/src/extractor/variant_machine.rs +++ b/crates/oxide/src/extractor/variant_machine.rs @@ -16,7 +16,7 @@ impl Machine for VariantMachine { #[inline] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { - match cursor.curr().into() { + match cursor.curr.into() { // Start of an arbitrary variant // // E.g.: `[&:hover]:` @@ -48,7 +48,7 @@ impl VariantMachine { start_pos: usize, cursor: &mut cursor::Cursor<'_>, ) -> MachineState { - match cursor.next().into() { + match cursor.next.into() { // End of an arbitrary value, must be followed by a `:` // // E.g.: `[&:hover]:` diff --git a/crates/oxide/src/fast_skip.rs b/crates/oxide/src/fast_skip.rs index 54930548c..488e61bd6 100644 --- a/crates/oxide/src/fast_skip.rs +++ b/crates/oxide/src/fast_skip.rs @@ -10,7 +10,7 @@ pub fn fast_skip(cursor: &Cursor) -> Option { return None; } - if !cursor.curr().is_ascii_whitespace() { + if !cursor.curr.is_ascii_whitespace() { return None; } diff --git a/crates/oxide/src/scanner/detect_sources.rs b/crates/oxide/src/scanner/detect_sources.rs index 6cfdd49cc..9dc340fba 100644 --- a/crates/oxide/src/scanner/detect_sources.rs +++ b/crates/oxide/src/scanner/detect_sources.rs @@ -30,9 +30,11 @@ fn sort_by_dir_and_name(a: &DirEntry, z: &DirEntry) -> Ordering { pub fn resolve_globs( base: PathBuf, - dirs: &FxHashSet, + dirs: &[PathBuf], extensions: &FxHashSet, ) -> Vec { + let allowed_paths: FxHashSet = FxHashSet::from_iter(dirs.iter().cloned()); + // A list of known extensions + a list of extensions we found in the project. let mut found_extensions: FxHashSet = FxHashSet::from_iter(KNOWN_EXTENSIONS.iter().map(|x| x.to_string())); @@ -70,7 +72,7 @@ pub fn resolve_globs( continue; } - if !dirs.contains(path) { + if !allowed_paths.contains(path) { let mut path = path; while let Some(parent) = path.parent() { if parent == base { @@ -111,8 +113,7 @@ pub fn resolve_globs( continue; } - if !dirs.contains(path) { - it.skip_current_dir(); + if !allowed_paths.contains(path) { continue; } diff --git a/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt b/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt index 79f01ecc1..0921d2ff8 100644 --- a/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt +++ b/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt @@ -1,15 +1,14 @@ .git .hg -.jj -.next -.parcel-cache -.pnpm-store -.svelte-kit .svn -.turbo -.venv -.vercel -.yarn -__pycache__ node_modules +.yarn +.venv venv +.next +.turbo +.parcel-cache +__pycache__ +.svelte-kit +.pnpm-store +.vercel diff --git a/crates/oxide/src/scanner/fixtures/ignored-files.txt b/crates/oxide/src/scanner/fixtures/ignored-files.txt index 1e8f1029f..d2d231ec7 100644 --- a/crates/oxide/src/scanner/fixtures/ignored-files.txt +++ b/crates/oxide/src/scanner/fixtures/ignored-files.txt @@ -2,5 +2,3 @@ package-lock.json pnpm-lock.yaml bun.lockb .gitignore -.env -.env.* diff --git a/crates/oxide/src/scanner/init_tracing.rs b/crates/oxide/src/scanner/init_tracing.rs deleted file mode 100644 index dd4c1d071..000000000 --- a/crates/oxide/src/scanner/init_tracing.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::fs::OpenOptions; -use std::io::{self, Write}; -use std::path::Path; -use std::sync::{self, Arc, Mutex}; -use tracing_subscriber::fmt::writer::BoxMakeWriter; - -pub static SHOULD_TRACE: sync::LazyLock = sync::LazyLock::new( - || matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))), -); - -fn dim(input: &str) -> String { - format!("\u{001b}[2m{input}\u{001b}[22m") -} - -fn blue(input: &str) -> String { - format!("\u{001b}[34m{input}\u{001b}[39m") -} - -fn highlight(input: &str) -> String { - format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`"))) -} - -struct MutexWriter(Arc>); - -impl Write for MutexWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.lock().unwrap().write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.0.lock().unwrap().flush() - } -} - -pub fn init_tracing() { - if !*SHOULD_TRACE { - return; - } - - let root = Path::new(".tailwindcss"); - let logs_dir = root.join("logs"); - if let Err(err) = std::fs::create_dir_all(&logs_dir) { - eprintln!( - "{} Failed to create {}, skipping debug logs ({err})", - dim("[DEBUG]"), - highlight(&logs_dir.display().to_string()) - ); - return; - } - - // Ensure everything inside `.tailwindcss/` is ignored by git. The file is only created if it - // doesn't exist yet, an existing `.gitignore` is left untouched. - if let Ok(mut file) = std::fs::File::create_new(root.join(".gitignore")) { - _ = file.write_all(b"*\n"); - } - - let file_path = logs_dir.join(format!( - "scanner-{}-{}.log", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis()) - .unwrap_or(0), - std::process::id() - )); - let file = match OpenOptions::new() - .create(true) - .append(true) - .open(&file_path) - { - Ok(file) => file, - Err(err) => { - eprintln!( - "{} Failed to create {}, skipping debug logs ({err})", - dim("[DEBUG]"), - highlight(&file_path.display().to_string()) - ); - return; - } - }; - - let absolute_file_path = dunce::canonicalize(&file_path).unwrap_or_else(|_| file_path.clone()); - eprintln!( - "{} Writing debug info to: {}\n", - dim("[DEBUG]"), - highlight(&absolute_file_path.display().to_string()) - ); - - let file = Arc::new(Mutex::new(file)); - - let writer: BoxMakeWriter = BoxMakeWriter::new({ - let file = file.clone(); - move || Box::new(MutexWriter(file.clone())) as Box - }); - - _ = tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE) - .with_writer(writer) - .with_ansi(false) - .compact() - .try_init(); -} diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index b7da9aa17..ec6aea642 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -1,6 +1,5 @@ pub mod auto_source_detection; pub mod detect_sources; -pub mod init_tracing; pub mod sources; use crate::extractor::{Extracted, Extractor}; @@ -15,12 +14,15 @@ use bstr::ByteSlice; use fast_glob::glob_match; use fxhash::{FxHashMap, FxHashSet}; use ignore::{gitignore::GitignoreBuilder, WalkBuilder}; -use init_tracing::{init_tracing, SHOULD_TRACE}; use rayon::prelude::*; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs::OpenOptions; +use std::io::{self, Write}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; +use std::sync::{self, Arc, Mutex}; use std::time::SystemTime; use tracing::event; +use tracing_subscriber::fmt::writer::BoxMakeWriter; // @source "some/folder"; // This is auto source detection // @source "some/folder/**/*"; // This is auto source detection @@ -32,6 +34,70 @@ use tracing::event; // // @source "do-include-me.bin"; // `.bin` is typically ignored, but now it's explicit so should be included // @source "git-ignored.html"; // A git ignored file that is listed explicitly, should be scanned +static SHOULD_TRACE: sync::LazyLock = sync::LazyLock::new( + || matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))), +); + +fn dim(input: &str) -> String { + format!("\u{001b}[2m{input}\u{001b}[22m") +} + +fn blue(input: &str) -> String { + format!("\u{001b}[34m{input}\u{001b}[39m") +} + +fn highlight(input: &str) -> String { + format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`"))) +} + +fn init_tracing() { + if !*SHOULD_TRACE { + return; + } + + let file_path = format!("tailwindcss-{}.log", std::process::id()); + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&file_path) + .unwrap_or_else(|_| panic!("Failed to open {file_path}")); + + let file_path = Path::new(&file_path); + let absolute_file_path = dunce::canonicalize(file_path) + .unwrap_or_else(|_| panic!("Failed to canonicalize {file_path:?}")); + eprintln!( + "{} Writing debug info to: {}\n", + dim("[DEBUG]"), + highlight(absolute_file_path.as_path().to_str().unwrap()) + ); + + let file = Arc::new(Mutex::new(file)); + + let writer: BoxMakeWriter = BoxMakeWriter::new({ + let file = file.clone(); + move || Box::new(MutexWriter(file.clone())) as Box + }); + + _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE) + .with_writer(writer) + .with_ansi(false) + .compact() + .try_init(); +} + +struct MutexWriter(Arc>); + +impl Write for MutexWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.lock().unwrap().write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.lock().unwrap().flush() + } +} #[derive(Debug, Clone)] pub enum ChangedContent { @@ -63,33 +129,26 @@ pub struct Scanner { /// The walker to detect all files that we have to scan walker: Option, + /// All changed content that we have to parse + changed_content: Vec, + /// All found extensions extensions: FxHashSet, + /// All CSS files we want to scan for CSS variable usage + css_files: Vec, + /// All files that we have to scan - files: FxHashSet, + files: Vec, /// All directories, sub-directories, etc… we saw during source detection - dirs: FxHashSet, + dirs: Vec, /// All generated globs, used for setting up watchers globs: Option>, /// Track unique set of candidates candidates: FxHashSet, - - /// Track mtimes for files so incremental scans can skip unchanged files. - mtimes: FxHashMap, - - /// Files that were scanned during the last `scan()` call. - scanned_files: Vec, - - /// Whether we've completed at least one full scan. When false, we skip - /// mtime tracking entirely so the initial build stays fast. - has_scanned_once: bool, - - /// Whether sources have been scanned since the last `scan()` call - sources_scanned: bool, } impl Scanner { @@ -104,6 +163,7 @@ impl Scanner { } let sources = Sources::new(public_source_entries_to_private_source_entries(sources)); + if *SHOULD_TRACE { event!(tracing::Level::INFO, "Optimized sources:"); for source in sources.iter() { @@ -111,27 +171,29 @@ impl Scanner { } } - let walker = create_walker(&sources); - Self { - sources, - walker, + sources: sources.clone(), + walker: create_walker(sources), ..Default::default() } } pub fn scan(&mut self) -> Vec { - self.sources_scanned = false; + self.scan_sources(); - let (scanned_blobs, css_files, files) = self.discover_sources(); + // TODO: performance improvement, bail early if we don't have any changed content + // if self.changed_content.is_empty() { + // return vec![]; + // } - self.extract_candidates(scanned_blobs, css_files); - self.scanned_files = files; + let _new_candidates = self.extract_candidates(); - // Return all candidates sorted - let mut result = self.candidates.iter().cloned().collect::>(); - result.par_sort_unstable(); - result + // Make sure we have a sorted list of candidates + let mut candidates = self.candidates.iter().cloned().collect::>(); + candidates.par_sort_unstable(); + + // Return all candidates instead of only the new ones + candidates } #[tracing::instrument(skip_all)] @@ -146,7 +208,7 @@ impl Scanner { // Raw content can be parsed directly, no need to verify if the file exists and is allowed // to be scanned. - let mut content_to_scan: Vec = changed_contents; + self.changed_content.extend(changed_contents); // Fully resolve all files let changed_files = changed_files @@ -170,7 +232,7 @@ impl Scanner { }); // All known files are allowed to be scanned - content_to_scan.extend(known_files); + self.changed_content.extend(known_files); // Figure out if the new unknown files are allowed to be scanned if !new_unknown_files.is_empty() { @@ -181,11 +243,6 @@ impl Scanner { continue; } - // The walked path can contain symlinks, while the changed files have already - // been canonicalized. Lazily canonicalize the walked path so we can compare - // the real paths as well. - let mut canonical_path: Option = None; - let mut drop_file_indexes = vec![]; for (idx, changed_file) in new_unknown_files.iter().enumerate().rev() { let ChangedContent::File(file, _) = changed_file else { @@ -194,17 +251,9 @@ impl Scanner { // When the file is found on disk it means that all the rules pass. We can // extract the current file and remove it from the list of passed in files. - let matches = file == path - || (file.file_name() == path.file_name() && { - if canonical_path.is_none() { - canonical_path = dunce::canonicalize(path).ok(); - } - canonical_path.as_deref() == Some(file.as_path()) - }); - - if matches { - self.files.insert(path.to_path_buf()); // Track for future use - content_to_scan.push(changed_file.clone()); // Track for parsing + if file == path { + self.files.push(path.to_path_buf()); // Track for future use + self.changed_content.push(changed_file.clone()); // Track for parsing drop_file_indexes.push(idx); } } @@ -225,17 +274,18 @@ impl Scanner { } } - // Read all content into blobs for extraction - let blobs = read_all_files(content_to_scan); - self.extract_candidates(blobs, vec![]) + self.extract_candidates() } #[tracing::instrument(skip_all)] - fn extract_candidates(&mut self, blobs: Vec>, css_files: Vec) -> Vec { - // Extract all candidates from the pre-read blobs - let mut new_candidates = parse_all_blobs(blobs); + fn extract_candidates(&mut self) -> Vec { + let changed_content = self.changed_content.drain(..).collect::>(); + + // Extract all candidates from the changed content + let mut new_candidates = parse_all_blobs(read_all_files(changed_content)); // Extract all CSS variables from the CSS files + let css_files = self.css_files.drain(..).collect::>(); if !css_files.is_empty() { let css_variables = extract_css_variables(read_all_files( css_files @@ -247,23 +297,63 @@ impl Scanner { new_candidates.extend(css_variables); } - // Only keep candidates we haven't seen before - for existing in self.candidates.iter() { - new_candidates.remove(existing); - } + // Only compute the new candidates and ignore the ones we already have. This is for + // subsequent calls to prevent serializing the entire set of candidates every time. + let mut new_candidates = new_candidates + .into_par_iter() + .filter(|candidate| !self.candidates.contains(candidate)) + .collect::>(); + + new_candidates.par_sort_unstable(); // Track new candidates for subsequent calls - self.candidates.extend(new_candidates.iter().cloned()); + self.candidates.par_extend(new_candidates.clone()); - let mut result: Vec = new_candidates.into_iter().collect(); - result.par_sort_unstable(); + new_candidates + } - result + #[tracing::instrument(skip_all)] + fn scan_sources(&mut self) { + let Some(walker) = &mut self.walker else { + return; + }; + + for entry in walker.build().filter_map(Result::ok) { + let path = entry.into_path(); + let Ok(metadata) = path.metadata() else { + continue; + }; + if metadata.is_dir() { + self.dirs.push(path); + } else if metadata.is_file() { + let extension = path + .extension() + .and_then(|x| x.to_str()) + .unwrap_or_default(); // In case the file has no extension + + match extension { + // Special handing for CSS files, we don't want to extract candidates from + // these files, but we do want to extract used CSS variables. + "css" => { + self.css_files.push(path.clone()); + } + _ => { + self.changed_content.push(ChangedContent::File( + path.to_path_buf(), + extension.to_owned(), + )); + } + } + + self.extensions.insert(extension.to_owned()); + self.files.push(path); + } + } } #[tracing::instrument(skip_all)] pub fn get_files(&mut self) -> Vec { - let _ = self.discover_sources(); + self.scan_sources(); self.files .par_iter() @@ -271,18 +361,13 @@ impl Scanner { .collect() } - #[tracing::instrument(skip_all)] - pub fn get_scanned_files(&self) -> Vec { - self.scanned_files.clone() - } - #[tracing::instrument(skip_all)] pub fn get_globs(&mut self) -> Vec { if let Some(globs) = &self.globs { return globs.clone(); } - let _ = self.discover_sources(); + self.scan_sources(); let mut globs = vec![]; for source in self.sources.iter() { @@ -356,167 +441,21 @@ impl Scanner { let i = s.as_ptr() as usize - offset; let original = &original_content[i..i + s.len()]; if original.contains_str("-[]") { - return String::from_utf8(original.to_vec()) - .ok() - .map(|candidate| (candidate, i)); + return Some(unsafe { + (String::from_utf8_unchecked(original.to_vec()), i) + }); } - String::from_utf8(s.to_vec()) - .ok() - .map(|candidate| (candidate, i)) + // SAFETY: When we parsed the candidates, we already guaranteed that the byte + // slices are valid, therefore we don't have to re-check here when we want to + // convert it back to a string. + Some(unsafe { (String::from_utf8_unchecked(s.to_vec()), i) }) } _ => None, }) .collect() } - - #[tracing::instrument(skip_all)] - fn discover_sources(&mut self) -> (Vec>, Vec, Vec) { - if self.sources_scanned { - return (vec![], vec![], vec![]); - } - self.sources_scanned = true; - - let Some(walker) = &mut self.walker else { - return (vec![], vec![], vec![]); - }; - - // Use synchronous walk for the initial build (lower overhead) and parallel - // walk for subsequent calls (watch mode) where the overhead is amortised. - let all_entries = if self.has_scanned_once { - walk_parallel(walker) - } else { - walk_synchronous(walker) - }; - - let mut css_files: Vec = vec![]; - let mut content_paths: Vec<(PathBuf, String)> = vec![]; - let mut changed_files = vec![]; - - // Fresh state - self.files.clear(); - self.dirs.clear(); - self.extensions.clear(); - self.globs = None; - - // Cache canonicalized folders in case a file itself is not symlinked, but any of the parent - // folders are symlinked. - let mut cached_canonical_dirs: FxHashMap = FxHashMap::default(); - - for entry in all_entries { - match entry { - WalkEntry::Dir(path) => { - self.dirs.insert(path); - } - WalkEntry::File { - path, - mtime, - is_symlink, - } => { - // Deduplicate: parallel walk can visit the same file from multiple threads - if !self.files.insert(path.clone()) { - continue; - } - - // Track canonicalized paths in addition to potentially symlinked file paths - let canonical = if is_symlink { - dunce::canonicalize(&path).ok() - } else { - path.parent().and_then(|parent| { - // Perf: cache the canonicalized parent path such that sibling files don't - // have to canonicalize over and over again. - let canonical_parent = cached_canonical_dirs - .entry(parent.to_path_buf()) - .or_insert_with(|| { - dunce::canonicalize(parent) - .unwrap_or_else(|_| parent.to_path_buf()) - }); - - if canonical_parent.as_path() != parent { - path.file_name() - .map(|file_name| canonical_parent.join(file_name)) - } else { - None - } - }) - }; - - if let Some(canonical) = canonical { - if canonical != path { - self.files.insert(canonical); - } - } - let extension = path - .extension() - .and_then(|x| x.to_str()) - .unwrap_or_default() - .to_owned(); - - self.extensions.insert(extension.to_owned()); - - // On incremental scans, check mtime to skip unchanged files. - // On the first scan, track mtimes while still scanning every file. - let changed = if self.has_scanned_once { - match mtime { - Some(mtime) => { - let prev = self.mtimes.insert(path.clone(), mtime); - prev.is_none_or(|prev| prev != mtime) - } - None => true, - } - } else { - if let Some(mtime) = mtime { - self.mtimes.insert(path.clone(), mtime); - } - - true - }; - - if !changed { - continue; - } - - if let Ok(file) = path.clone().into_os_string().into_string() { - changed_files.push(file); - } - - match extension.as_str() { - // Special handing for CSS files, we don't want to extract candidates from - // these files, but we do want to extract used CSS variables. - "css" => css_files.push(path), - _ => content_paths.push((path, extension)), - } - } - } - } - - // Ensure `mtimes` don't include stale files - self.mtimes.retain(|path, _| self.files.contains(path)); - - // Read + preprocess all discovered files in parallel - let scanned_blobs: Vec> = content_paths - .into_par_iter() - .filter_map(|(path, ext)| { - let content = std::fs::read(&path).ok()?; - event!(tracing::Level::INFO, "Reading {:?}", path); - let processed = pre_process_input(content, &ext); - if processed.is_empty() { - None - } else { - Some(processed) - } - }) - .collect(); - - if !self.has_scanned_once { - self.has_scanned_once = true; - } - - changed_files.par_sort_unstable(); - - (scanned_blobs, css_files, changed_files) - } } fn read_changed_content(c: ChangedContent) -> Option> { @@ -535,28 +474,26 @@ fn read_changed_content(c: ChangedContent) -> Option> { ChangedContent::Content(contents, extension) => (contents.into_bytes(), extension), }; - Some(pre_process_input(content, &extension)) + Some(pre_process_input(&content, &extension)) } -pub fn pre_process_input(content: Vec, extension: &str) -> Vec { +pub fn pre_process_input(content: &[u8], extension: &str) -> Vec { use crate::extractor::pre_processors::*; match extension { - "clj" | "cljs" | "cljc" => Clojure.process(&content), - "heex" | "eex" | "ex" | "exs" => Elixir.process(&content), - "cshtml" | "razor" => Razor.process(&content), - "haml" => Haml.process(&content), - "json" | "jsonl" | "ndjson" => Json.process(&content), - "md" | "mdx" => Markdown.process(&content), - "pug" => Pug.process(&content), - "rb" | "erb" => Ruby.process(&content), - "slim" | "slang" => Slim.process(&content), - "svelte" => Svelte.process(&content), - "rs" => Rust.process(&content), - "tt" | "tt2" | "tx" => TemplateToolkit.process(&content), - "twig" => Twig.process(&content), - "vue" => Vue.process(&content), - _ => content, + "clj" | "cljs" | "cljc" => Clojure.process(content), + "heex" | "eex" | "ex" | "exs" => Elixir.process(content), + "cshtml" | "razor" => Razor.process(content), + "haml" => Haml.process(content), + "json" => Json.process(content), + "md" | "mdx" => Markdown.process(content), + "pug" => Pug.process(content), + "rb" | "erb" => Ruby.process(content), + "slim" | "slang" => Slim.process(content), + "svelte" => Svelte.process(content), + "rs" => Rust.process(content), + "vue" => Vue.process(content), + _ => content.to_vec(), } } @@ -575,23 +512,23 @@ fn read_all_files(changed_content: Vec) -> Vec> { } #[tracing::instrument(skip_all)] -fn extract_css_variables(blobs: Vec>) -> FxHashSet { +fn extract_css_variables(blobs: Vec>) -> Vec { extract(blobs, |mut extractor| { extractor.extract_variables_from_css() }) } #[tracing::instrument(skip_all)] -fn parse_all_blobs(blobs: Vec>) -> FxHashSet { +fn parse_all_blobs(blobs: Vec>) -> Vec { extract(blobs, |mut extractor| extractor.extract()) } #[tracing::instrument(skip_all)] -fn extract(blobs: Vec>, handle: H) -> FxHashSet +fn extract(blobs: Vec>, handle: H) -> Vec where H: Fn(Extractor) -> Vec + std::marker::Sync, { - blobs + let mut result: Vec<_> = blobs .par_iter() .flat_map(|blob| blob.par_split(|x| *x == b'\n')) .filter_map(|blob| { @@ -616,113 +553,26 @@ where a }) .into_iter() - .filter_map(|s| String::from_utf8(s.to_vec()).ok()) - .collect() + .map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) }) + .collect(); + + // SAFETY: Unstable sort is faster and in this scenario it's also safe because we are + // guaranteed to have unique candidates. + result.par_sort_unstable(); + + result } -#[derive(Debug)] -enum WalkEntry { - Dir(PathBuf), - File { - path: PathBuf, - mtime: Option, - - /// Whether the path itself is a symlink - is_symlink: bool, - }, -} - -impl From for WalkEntry { - fn from(entry: ignore::DirEntry) -> Self { - let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false); - let is_symlink = entry.path_is_symlink(); - let path = entry.into_path(); - - if is_dir { - WalkEntry::Dir(path) - } else { - let mtime = path.metadata().ok().and_then(|m| m.modified().ok()); - WalkEntry::File { - path, - mtime, - is_symlink, - } - } - } -} - -/// Walk the file system synchronously. Used for the initial build where the overhead of spawning -/// parallel walker threads is not worth it. -#[tracing::instrument(skip_all)] -fn walk_synchronous(walker: &mut WalkBuilder) -> Vec { - walker - .build() - .filter_map(Result::ok) - .map(WalkEntry::from) - .collect() -} - -/// Walk the file system in parallel. Used in watch mode where the parallel walker overhead is -/// amortised across many rebuilds and subsequent calls are much faster. -#[tracing::instrument(skip_all)] -fn walk_parallel(walker: &mut WalkBuilder) -> Vec { - struct FlushOnDrop { - local: Vec, - shared: Arc>>, - } - - impl Drop for FlushOnDrop { - fn drop(&mut self) { - if !self.local.is_empty() { - self.shared.lock().unwrap().append(&mut self.local); - } - } - } - - let collected: Arc>> = Arc::new(Mutex::new(vec![])); - - walker.build_parallel().run(|| { - let mut buf = FlushOnDrop { - local: Vec::with_capacity(256), - shared: collected.clone(), - }; - - Box::new(move |entry| { - let Ok(entry) = entry else { - return ignore::WalkState::Continue; - }; - - buf.local.push(WalkEntry::from(entry)); - - if buf.local.len() >= 256 { - buf.shared.lock().unwrap().append(&mut buf.local); - } - - ignore::WalkState::Continue - }) - }); - - // All threads have finished and flushed their buffers via FlushOnDrop::drop - Arc::try_unwrap(collected).unwrap().into_inner().unwrap() -} - -/// Sets up a WalkBuilder with all source roots, gitignore rules, and source pattern matching. +/// Create a walker for the given sources to detect all the files that we have to scan. /// -/// This is the common setup shared between the full walker (with mtime tracking for re-scans) -/// and the parallel walker (without mtime tracking for the initial scan). -fn create_walker(sources: &Sources) -> Option { +/// The `mtimes` map is used to keep track of the last modified time of each file. This is used to +/// determine if a file or folder has changed since the last scan and we can skip folders that +/// haven't changed. +fn create_walker(sources: Sources) -> Option { + let mtimes: Arc>> = Default::default(); let mut other_roots: FxHashSet<&PathBuf> = FxHashSet::default(); let mut first_root: Option<&PathBuf> = None; - - let mut ignores: Vec<(&PathBuf, Vec)> = 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> = Default::default(); for source in sources.iter() { match source { @@ -734,7 +584,7 @@ fn create_walker(sources: &Sources) -> Option { } } SourceEntry::Pattern { base, pattern } => { - let pattern = pattern.to_owned(); + let mut pattern = pattern.to_string(); if first_root.is_none() { first_root = Some(base); @@ -743,19 +593,35 @@ fn create_walker(sources: &Sources) -> Option { } if !pattern.contains("**") { + // Ensure that the pattern is pinned to the base path. + if !pattern.starts_with("/") { + pattern = format!("/{pattern}"); + } + // Specific patterns should take precedence even over git-ignored files: - emit(base, format!("!{}", pattern)); + ignores + .entry(base) + .or_default() + .insert(format!("!{}", pattern)); } else { // Assumption: the pattern we receive will already be brace expanded. So // `*.{html,jsx}` will result in two separate patterns: `*.html` and `*.jsx`. if let Some(extension) = Path::new(&pattern).extension() { // Extend auto source detection to include the extension - emit(base, format!("!*.{}", extension.to_string_lossy())); + ignores + .entry(base) + .or_default() + .insert(format!("!*.{}", extension.to_string_lossy())); } } } SourceEntry::Ignored { base, pattern } => { - emit(base, pattern.to_owned()); + let mut pattern = pattern.to_string(); + // Ensure that the pattern is pinned to the base path. + if !pattern.starts_with("/") { + pattern = format!("/{pattern}"); + } + ignores.entry(base).or_default().insert(pattern); } SourceEntry::External { base } => { if first_root.is_none() { @@ -765,10 +631,16 @@ fn create_walker(sources: &Sources) -> Option { } // External sources should take precedence even over git-ignored files: - emit(base, "!/**/*".to_owned()); + ignores + .entry(base) + .or_default() + .insert(format!("!{}", "/**/*")); // External sources should still disallow binary extensions: - emit(base, BINARY_EXTENSIONS_GLOB.clone()); + ignores + .entry(base) + .or_default() + .insert(BINARY_EXTENSIONS_GLOB.clone()); } } } @@ -845,61 +717,75 @@ fn create_walker(sources: &Sources) -> Option { builder.add_gitignore(ignore); } - // Pre-compute source matching data to avoid allocations in the hot filter_entry path - let auto_bases: Vec = sources - .iter() - .filter_map(|source| match source { - SourceEntry::Auto { base } | SourceEntry::External { base } => Some(base.clone()), - _ => None, - }) - .collect(); + builder.filter_entry({ + move |entry| { + let path = entry.path(); - let pattern_sources: Vec<(PathBuf, String)> = sources - .iter() - .filter_map(|source| match source { - SourceEntry::Pattern { base, pattern } => Some((base.into(), pattern.into())), - _ => None, - }) - .collect(); - - // Source pattern matching filter (lock-free, safe for parallel walking) - builder.filter_entry(move |entry| { - let path = entry.path(); - - // Ensure the entries are matching any of the provided source patterns (this is - // necessary for manual-patterns that can filter the file extension) - if path.is_file() { - let mut matches = false; - - for base in &auto_bases { - if path.starts_with(base) { - matches = true; - break; - } - } - - if !matches { - for (base, pattern) in &pattern_sources { - let remainder = path.strip_prefix(base); - if remainder.is_ok_and(|remainder| { - let mut path_str = remainder.to_string_lossy().to_string(); - if !path_str.starts_with("/") { - path_str = format!("/{path_str}"); + // Ensure the entries are matching any of the provided source patterns (this is + // necessary for manual-patterns that can filter the file extension) + if path.is_file() { + let mut matches = false; + for source in sources.iter() { + match source { + SourceEntry::Auto { base } | SourceEntry::External { base } => { + if path.starts_with(base) { + matches = true; + break; + } } - glob_match(pattern, path_str.as_bytes()) - }) { - matches = true; - break; + SourceEntry::Pattern { base, pattern } => { + let mut pattern = pattern.to_string(); + // Ensure that the pattern is pinned to the base path. + if !pattern.starts_with("/") { + pattern = format!("/{pattern}"); + } + + // Check if path starts with base, if so, remove the prefix and check the remainder against the pattern + let remainder = path.strip_prefix(base); + if remainder.is_ok_and(|remainder| { + let mut path_str = remainder.to_string_lossy().to_string(); + if !path_str.starts_with("/") { + path_str = format!("/{path_str}"); + } + glob_match(pattern, path_str.as_bytes()) + }) { + matches = true; + break; + } + } + _ => {} } } + + if !matches { + return false; + } } - if !matches { - return false; + let mut mtimes = mtimes.lock().unwrap(); + let current_time = match entry.metadata() { + Ok(metadata) if metadata.is_file() => { + if let Ok(time) = metadata.modified() { + Some(time) + } else { + None + } + } + _ => None, + }; + + let previous_time = + current_time.and_then(|time| mtimes.insert(entry.clone().into_path(), time)); + + match (current_time, previous_time) { + (Some(current), Some(prev)) if prev == current => false, + _ => { + event!(tracing::Level::INFO, "Discovering {:?}", path); + + true + } } } - - true }); Some(builder) diff --git a/crates/oxide/src/scanner/sources.rs b/crates/oxide/src/scanner/sources.rs index 53c09387f..f28b16a49 100644 --- a/crates/oxide/src/scanner/sources.rs +++ b/crates/oxide/src/scanner/sources.rs @@ -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) -> Vec { - 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::>(); - - // Bases of restricted patterns. Each of these becomes its own walk root with its own - // `*` + `!` 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::>(); - - let mut restricted_roots: FxHashSet = 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::>(); - // 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> = 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::>(); - - expand_restricted_patterns(sources) + .map(Into::into) + .collect::>() } /// Convert a public source entry to a source entry impl From 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 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, + }, } } } diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index fb5354a87..4003ff253 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod scanner { - use insta::assert_snapshot; use pretty_assertions::assert_eq; use std::path::{Path, PathBuf}; use std::process::Command; @@ -21,16 +20,6 @@ mod scanner { result } - fn symlink_file, Q: AsRef>(original: P, link: Q) -> std::io::Result<()> { - #[cfg(not(windows))] - let result = std::os::unix::fs::symlink(original, link); - - #[cfg(windows)] - let result = std::os::windows::fs::symlink_file(original, link); - - result - } - fn public_source_entry_from_pattern(dir: PathBuf, pattern: &str) -> PublicSourceEntry { let mut parts = pattern.split_whitespace(); let _ = parts.next().unwrap_or_default(); @@ -56,157 +45,6 @@ mod scanner { globs: Vec, normalized_sources: Vec, candidates: Vec, - tree: String, - } - - /// Renders the directory as a tree, annotating every file and folder with an indicator that - /// shows whether the scanner picked it up: - /// - /// - `✓` — scanned - /// - `✗` — ignored / skipped - /// - /// Symlinks are rendered as `link → target`, where the target is shown relative to the folder - /// containing the symlink. The contents of every `.gitignore` file are printed right below the - /// file itself. - /// - /// Folders that are a git repository root (they contain a `.git` folder) are marked with - /// `(git)`, because ignore rules behave differently inside and outside of a repository. - fn fs_tree(root: &Path, scanned: &[String]) -> String { - /// Computes the relative path from `parent` to `target`, where both are relative to the - /// same root, e.g.: `b/c` seen from `b` is `c`, and the root itself seen from `b` is `..`. - fn relative_to(target: &Path, parent: &Path) -> String { - let target = target.components().collect::>(); - let parent = parent.components().collect::>(); - - let common = target - .iter() - .zip(&parent) - .take_while(|(a, b)| **a == **b) - .count(); - - let mut parts = vec![".."; parent.len() - common]; - parts.extend( - target[common..] - .iter() - .map(|component| component.as_os_str().to_str().unwrap()), - ); - - if parts.is_empty() { - // The symlink points to its own parent folder - ".".into() - } else { - parts.join("/") - } - } - - fn walk( - dir: &Path, - root: &Path, - prefix: &str, - scanned: &[String], - visited: &mut Vec, - out: &mut String, - ) { - let mut entries = fs::read_dir(dir) - .unwrap() - .filter_map(Result::ok) - .filter(|entry| entry.file_name() != ".git") - .collect::>(); - entries.sort_by_key(|entry| entry.file_name()); - - for (i, entry) in entries.iter().enumerate() { - let last = i == entries.len() - 1; - let connector = if last { "└── " } else { "├── " }; - let child_prefix = if last { " " } else { "│ " }; - - let path = entry.path(); - let name = entry.file_name().to_string_lossy().to_string(); - let rel = path - .strip_prefix(root) - .unwrap() - .to_string_lossy() - .replace('\\', "/"); - - let file_type = entry.file_type().unwrap(); - - // a file is scanned when it shows up in the scanned files list. a folder is - // considered scanned when any scanned file lives inside of it. paths that go - // through a symlink count towards the symlink, not towards the target directory. - let dir_prefix = format!("{rel}/"); - let is_scanned = scanned - .iter() - .any(|file| file == &rel || file.starts_with(&dir_prefix)); - let indicator = if is_scanned { "✓" } else { "✗" }; - - let mut display_name = name.clone(); - if path.join(".git").exists() { - display_name = format!("{display_name} (git)"); - } - if file_type.is_symlink() { - if let Ok(target) = fs::read_link(&path) { - // Show the target relative to the folder containing the symlink, or as-is - // when it points outside of the tree. - let target = match (target.strip_prefix(root), dir.strip_prefix(root)) { - (Ok(target), Ok(parent)) => relative_to(target, parent), - _ => target.to_string_lossy().replace('\\', "/"), - }; - display_name = format!("{name} → {target}"); - - // The symlink points to a target that doesn't exist - if path.metadata().is_err() { - display_name = format!("{display_name} (broken)"); - } - } - } - - out.push_str(&format!("{prefix}{connector}{indicator} {display_name}\n")); - - // Print the contents of `.gitignore` files below the file itself, so the ignore - // rules are visible in the same output. - if name == ".gitignore" { - if let Ok(contents) = fs::read_to_string(&path) { - for line in contents.lines() { - let line = format!("{prefix}{child_prefix} {line}"); - out.push_str(line.trim_end()); - out.push('\n'); - } - } - } - - // Descend into directories, including symlinked ones. The paths inside a symlinked - // directory are computed through the symlink, so the indicators show what the - // scanner saw via that route. A visited stack prevents symlink cycles from - // recursing forever. - if path.metadata().map(|meta| meta.is_dir()).unwrap_or(false) { - let Ok(canonical) = dunce::canonicalize(&path) else { - continue; - }; - if visited.contains(&canonical) { - continue; - } - - visited.push(canonical); - walk( - &path, - root, - &format!("{prefix}{child_prefix}"), - scanned, - visited, - out, - ); - visited.pop(); - } - } - } - - let mut visited = vec![dunce::canonicalize(root).unwrap()]; - let mut out = if root.join(".git").exists() { - String::from(". (git)\n") - } else { - String::from(".\n") - }; - walk(root, root, "", scanned, &mut visited, &mut out); - out } fn create_files_in(dir: &path::Path, paths: &[(&str, &str)]) { @@ -223,53 +61,6 @@ mod scanner { } } - fn scanned_files(scanner: &mut Scanner, base: &Path) -> Vec { - let base_dir = - format!("{}{}", dunce::canonicalize(base).unwrap().display(), "/").replace('\\', "/"); - - let mut files = scanner - .get_files() - .iter() - // Normalize paths to use unix style separators - .map(|file| file.replace('\\', "/").replace(&base_dir, "")) - .collect::>(); - files.sort(); - files - } - - fn scanned_globs(scanner: &mut Scanner, base: &Path) -> Vec { - let base_dir = - format!("{}{}", dunce::canonicalize(base).unwrap().display(), "/").replace('\\', "/"); - - let mut globs = scanner - .get_globs() - .iter() - .map(|glob| { - if glob.pattern.starts_with('/') { - format!("{}{}", glob.base, glob.pattern) - } else { - format!("{}/{}", glob.base, glob.pattern) - } - }) - // Normalize paths to use unix style separators - .map(|file| file.replace('\\', "/").replace(&base_dir, "")) - .collect::>(); - globs.sort(); - globs - } - - fn normalize_files(files: Vec, base: &Path) -> Vec { - let base_dir = - format!("{}{}", dunce::canonicalize(base).unwrap().display(), "/").replace('\\', "/"); - - let mut files = files - .iter() - .map(|file| file.replace('\\', "/").replace(&base_dir, "")) - .collect::>(); - files.sort(); - files - } - fn scan_with_globs( paths_with_content: &[(&str, &str)], source_directives: Vec<&str>, @@ -294,14 +85,34 @@ mod scanner { let mut scanner = Scanner::new(sources); let candidates = scanner.scan(); + let base_dir = format!("{}{}", dunce::canonicalize(&base).unwrap().display(), "/").replace('\\', "/"); // Get all scanned files as strings relative to the base directory - let files = scanned_files(&mut scanner, Path::new(&base)); + let mut files = scanner + .get_files() + .iter() + // Normalize paths to use unix style separators + .map(|file| file.replace('\\', "/").replace(&base_dir, "")) + .collect::>(); + files.sort(); // Get all scanned globs as strings relative to the base directory - let globs = scanned_globs(&mut scanner, Path::new(&base)); + let mut globs = scanner + .get_globs() + .iter() + .map(|glob| { + if glob.pattern.starts_with('/') { + format!("{}{}", glob.base, glob.pattern) + } else { + format!("{}/{}", glob.base, glob.pattern) + } + }) + // Normalize paths to use unix style separators + .map(|file| file.replace('\\', "/").replace(&base_dir, "")) + .collect::>(); + globs.sort(); // Get all normalized sources as strings relative to the base directory let mut normalized_sources = scanner @@ -319,14 +130,11 @@ mod scanner { .collect::>(); normalized_sources.sort(); - let tree = fs_tree(&dir, &files); - ScanResult { files, globs, normalized_sources, candidates, - tree, } } @@ -340,7 +148,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ ("index.html", ""), @@ -348,14 +155,6 @@ mod scanner { ("b.html", ""), ("c.html", ""), ]); - - assert_snapshot!(tree, @" - . (git) - ├── ✓ a.html - ├── ✓ b.html - ├── ✓ c.html - └── ✓ index.html - "); assert_eq!(files, vec!["a.html", "b.html", "c.html", "index.html"]); assert_eq!(globs, vec!["*"]); assert_eq!(normalized_sources, vec!["**/*"]); @@ -367,7 +166,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ (".gitignore", "b.html"), @@ -376,17 +174,6 @@ mod scanner { ("b.html", ""), ("c.html", ""), ]); - - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ b.html - ├── ✓ a.html - ├── ✗ b.html - ├── ✓ c.html - └── ✓ index.html - "); - assert_eq!(files, vec!["a.html", "c.html", "index.html"]); assert_eq!(globs, vec!["*"]); assert_eq!(normalized_sources, vec!["**/*"]); @@ -398,7 +185,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ ("index.html", ""), @@ -409,20 +195,6 @@ mod scanner { ("public/deeply/nested/c.html", ""), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✓ index.html - └── ✓ public - ├── ✓ a.html - ├── ✓ b.html - ├── ✓ c.html - ├── ✓ deeply - │ └── ✓ nested - │ └── ✓ c.html - └── ✓ nested - └── ✓ c.html - "); - assert_eq!( files, vec![ @@ -444,7 +216,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ ("index.html", ""), @@ -458,25 +229,6 @@ mod scanner { ("public/very/deeply/nested/a.html", ""), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✓ index.html - └── ✓ public - ├── ✓ a.html - ├── ✓ b.html - ├── ✓ c.html - ├── ✓ nested - │ ├── ✓ a.html - │ ├── ✓ again - │ │ └── ✓ a.html - │ ├── ✓ b.html - │ └── ✓ c.html - └── ✓ very - └── ✓ deeply - └── ✓ nested - └── ✓ a.html - "); - assert_eq!( files, vec![ @@ -501,7 +253,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ (".gitignore", "public/b.html\na.html"), @@ -511,18 +262,6 @@ mod scanner { ("public/c.html", ""), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ public/b.html - │ a.html - ├── ✓ index.html - └── ✓ public - ├── ✗ a.html - ├── ✗ b.html - └── ✓ c.html - "); - assert_eq!(files, vec!["index.html", "public/c.html",]); assert_eq!(globs, vec!["*"]); assert_eq!(normalized_sources, vec!["**/*"]); @@ -534,7 +273,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ ("index.html", ""), @@ -543,15 +281,6 @@ mod scanner { ("src/c.html", ""), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✓ index.html - └── ✓ src - ├── ✓ a.html - ├── ✓ b.html - └── ✓ c.html - "); - assert_eq!( files, vec!["index.html", "src/a.html", "src/b.html", "src/c.html"] @@ -569,7 +298,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ ("index.html", ""), @@ -578,14 +306,6 @@ mod scanner { ("c.lock", ""), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✗ a.mp4 - ├── ✗ b.png - ├── ✗ c.lock - └── ✓ index.html - "); - assert_eq!(files, vec!["index.html"]); assert_eq!(globs, vec!["*"]); assert_eq!(normalized_sources, vec!["**/*"]); @@ -598,7 +318,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ // Looks like `.pages` binary extension, but it's a folder @@ -612,16 +331,6 @@ mod scanner { ), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ other.pages - ├── ✗ other.pages - │ └── ✗ index.html - └── ✓ some.pages - └── ✓ index.html - "); - assert_eq!(files, vec!["some.pages/index.html"]); assert_eq!(globs, vec!["*", "some.pages/**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}"]); assert_eq!(normalized_sources, vec!["**/*"]); @@ -633,7 +342,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ ("index.html", ""), @@ -642,14 +350,6 @@ mod scanner { ("c.less", ""), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✓ a.css - ├── ✗ b.sass - ├── ✗ c.less - └── ✓ index.html - "); - assert_eq!(files, vec!["a.css", "index.html"]); assert_eq!(globs, vec!["*"]); assert_eq!(normalized_sources, vec!["**/*"]); @@ -661,16 +361,9 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[("src/index.my-extension", "")]); - assert_snapshot!(tree, @" - . (git) - └── ✓ src - └── ✓ index.my-extension - "); - assert_eq!(files, vec!["src/index.my-extension"]); assert_eq!(globs, vec!["*", "src/**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,my-extension,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}"]); assert_eq!(normalized_sources, vec!["**/*"]); @@ -682,7 +375,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ ("index.html", ""), @@ -690,13 +382,6 @@ mod scanner { ("yarn.lock", ""), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✓ index.html - ├── ✗ package-lock.json - └── ✗ yarn.lock - "); - assert_eq!(files, vec!["index.html"]); assert_eq!(globs, vec!["*"]); assert_eq!(normalized_sources, vec!["**/*"]); @@ -708,7 +393,6 @@ mod scanner { files, globs, normalized_sources, - tree, .. } = scan(&[ // Explicitly listed root files @@ -753,58 +437,6 @@ mod scanner { ("nested-d/very/deeply/nested/directory/again/foo.html", ""), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✓ bar.html - ├── ✓ baz.html - ├── ✓ foo.html - ├── ✓ nested-a - │ ├── ✓ bar.html - │ ├── ✓ baz.html - │ └── ✓ foo.html - ├── ✓ nested-b - │ └── ✓ deeply-nested - │ ├── ✓ bar.html - │ ├── ✓ baz.html - │ └── ✓ foo.html - ├── ✓ nested-c - │ ├── ✗ .gitignore - │ │ ignored-folder/ - │ ├── ✓ bar.html - │ ├── ✓ baz.html - │ ├── ✓ foo.html - │ ├── ✗ ignored-folder - │ │ ├── ✗ bar.html - │ │ ├── ✗ baz.html - │ │ └── ✗ foo.html - │ └── ✓ sibling-folder - │ ├── ✓ bar.html - │ ├── ✓ baz.html - │ └── ✓ foo.html - └── ✓ nested-d - ├── ✗ .gitignore - │ deep/ - ├── ✓ bar.html - ├── ✓ baz.html - ├── ✓ foo.html - └── ✓ very - └── ✓ deeply - └── ✓ nested - ├── ✓ bar.html - ├── ✓ baz.html - ├── ✗ deep - │ ├── ✗ bar.html - │ ├── ✗ baz.html - │ └── ✗ foo.html - ├── ✓ directory - │ ├── ✓ again - │ │ └── ✓ foo.html - │ ├── ✓ bar.html - │ ├── ✓ baz.html - │ └── ✓ foo.html - └── ✓ foo.html - "); - assert_eq!( files, vec![ @@ -859,7 +491,6 @@ mod scanner { let ScanResult { candidates, normalized_sources, - tree, .. } = scan(&[ // The gitignore file is used to filter out files but not scanned for candidates @@ -882,21 +513,6 @@ mod scanner { ("index4.svelte", ""), ]); - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ # md:font-bold - │ foo.html - ├── ✗ foo.html - ├── ✗ foo.jpg - ├── ✓ index.angular.html - ├── ✓ index.html - ├── ✓ index.svelte - ├── ✓ index2.svelte - ├── ✓ index3.svelte - └── ✓ index4.svelte - "); - assert_eq!( candidates, vec![ @@ -920,86 +536,28 @@ mod scanner { let ScanResult { candidates, normalized_sources, - tree, .. } = scan_with_globs( &[("foo/bar/baz/foo.html", "content-['foo.html']")], vec!["@source '**/*'", "@source './foo/bar/baz/..'"], ); - assert_snapshot!(tree, @" - . (git) - └── ✓ foo - └── ✓ bar - └── ✓ baz - └── ✓ foo.html - "); - assert_eq!(candidates, vec!["content-['foo.html']"]); assert_eq!(normalized_sources, vec!["**/*", "foo/bar/**/*"]); } - #[test] - fn it_should_preserve_source_order_when_referencing_a_sibling_project() { - // Create a temporary working directory - let dir = tempdir().unwrap().into_path(); - - // Create files - create_files_in( - &dir, - &[ - ("project-a/src/index.css", ""), - ("project-b/keep/keep.html", "content-['GOOD-1']"), - ("project-b/ignored/ignored.html", "content-['BAD-1']"), - ("project-b/ignored/except.html", "content-['GOOD-2']"), - ], - ); - - let base = dir.join("project-a/src"); - let mut scanner = Scanner::new(vec![ - public_source_entry_from_pattern(base.clone(), "@source '../../project-b'"), - public_source_entry_from_pattern(base.clone(), "@source not '../../project-b/ignored'"), - public_source_entry_from_pattern( - base.clone(), - "@source '../../project-b/ignored/except.html'", - ), - ]); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ project-a - │ └── ✗ src - │ └── ✗ index.css - └── ✓ project-b - ├── ✓ ignored - │ ├── ✓ except.html - │ └── ✗ ignored.html - └── ✓ keep - └── ✓ keep.html - "); - - assert_eq!(candidates, vec!["content-['GOOD-1']", "content-['GOOD-2']"]); - } - #[test] fn it_should_scan_files_without_extensions() { // These look like folders, but they are files let ScanResult { candidates, normalized_sources, - tree, .. } = scan_with_globs( &[("my-file", "content-['my-file']")], vec!["@source '**/*'", "@source './my-file'"], ); - assert_snapshot!(tree, @" - . (git) - └── ✓ my-file - "); - assert_eq!(candidates, vec!["content-['my-file']"]); assert_eq!(normalized_sources, vec!["**/*", "my-file"]); } @@ -1010,7 +568,6 @@ mod scanner { let ScanResult { candidates, normalized_sources, - tree, .. } = scan_with_globs( &[ @@ -1030,14 +587,6 @@ mod scanner { ], ); - assert_snapshot!(tree, @" - . (git) - ├── ✓ my-folder.bin - │ └── ✓ foo.html - └── ✓ my-folder.templates - └── ✓ foo.html - "); - assert_eq!( candidates, vec![ @@ -1056,7 +605,6 @@ mod scanner { let ScanResult { candidates, normalized_sources, - tree, .. } = scan_with_globs( &[ @@ -1067,161 +615,15 @@ mod scanner { vec!["@source '**/*'", "@source '*.styl'"], ); - assert_snapshot!(tree, @" - . (git) - └── ✓ foo.styl - "); - assert_eq!(candidates, vec!["content-['foo.styl']"]); assert_eq!(normalized_sources, vec!["**/*", "*.styl"]); } - #[test] - fn it_should_drop_invalid_utf8_candidates() { - let dir = tempdir().unwrap(); - fs::write(dir.path().join("index.html"), b"flex bg-[\x80] block").unwrap(); - - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - dir.path().to_path_buf(), - "@source '*.html'", - )]); - - let candidates = scanner - .scan() - .into_iter() - .map(String::into_bytes) - .collect::>(); - - assert_eq!(candidates, vec![b"block".to_vec(), b"flex".to_vec()]); - } - - #[test] - fn it_should_not_store_invalid_utf8_candidates_during_incremental_scans() { - let dir = tempdir().unwrap(); - let file = dir.path().join("index.html"); - fs::write(&file, b"flex bg-[\x80]").unwrap(); - - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - dir.path().to_path_buf(), - "@source '*.html'", - )]); - - let candidates = scanner - .scan_content(vec![ChangedContent::File(file, "html".into())]) - .into_iter() - .map(String::into_bytes) - .collect::>(); - assert_eq!(candidates, vec![b"flex".to_vec()]); - - let candidates = - scanner.scan_content(vec![ChangedContent::Content("block".into(), "html".into())]); - assert_eq!(candidates, vec!["block"]); - - let candidates = scanner - .scan() - .into_iter() - .map(String::into_bytes) - .collect::>(); - assert_eq!(candidates, vec![b"block".to_vec(), b"flex".to_vec()]); - } - - #[test] - fn it_should_drop_invalid_utf8_candidates_with_positions() { - let dir = tempdir().unwrap(); - let file = dir.path().join("index.html"); - fs::write( - &file, - b"flex bg-[\x80] group-[]:block group-[]:bg-[\x80] grid", - ) - .unwrap(); - - let mut scanner = Scanner::new(vec![]); - let candidates = scanner - .get_candidates_with_positions(ChangedContent::File(file, "html".into())) - .into_iter() - .map(|(candidate, position)| (candidate.into_bytes(), position)) - .collect::>(); - - assert_eq!( - candidates, - vec![ - (b"flex".to_vec(), 0), - (b"group-[]:block".to_vec(), 12), - (b"grid".to_vec(), 43), - ] - ); - } - - #[test] - fn it_should_preserve_valid_utf8_candidates() { - let dir = tempdir().unwrap(); - fs::write( - dir.path().join("index.html"), - "before:content-['💩'] bg-[é] font-[中文]".as_bytes(), - ) - .unwrap(); - - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - dir.path().to_path_buf(), - "@source '*.html'", - )]); - - assert_eq!( - scanner.scan(), - vec!["before:content-['💩']", "bg-[é]", "font-[中文]"] - ); - } - - #[test] - fn it_should_preserve_paths_for_sources_ending_in_a_deep_glob() { - let ScanResult { - candidates, - files, - globs, - normalized_sources, - tree, - .. - } = scan_with_globs( - &[ - ( - "blog/2024/foo/bar/baz/index.html", - "content-['blog/2024/foo/bar/baz/index.html']", - ), - ( - "blog/2024/foo/bar/qux/index.html", - "content-['blog/2024/foo/bar/qux/index.html']", - ), - ], - vec!["@source './blog/*/foo/bar/baz/**/*'"], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ blog - └── ✓ 2024 - └── ✓ foo - └── ✓ bar - ├── ✓ baz - │ └── ✓ index.html - └── ✗ qux - └── ✗ index.html - "); - - assert_eq!( - candidates, - vec!["content-['blog/2024/foo/bar/baz/index.html']"] - ); - assert_eq!(files, vec!["blog/2024/foo/bar/baz/index.html"]); - assert_eq!(globs, vec!["blog/*/foo/bar/baz/**/*"]); - assert_eq!(normalized_sources, vec!["blog/*/foo/bar/baz/**/*"]); - } - #[test] fn it_should_scan_next_dynamic_folders() { let ScanResult { candidates, normalized_sources, - tree, .. } = scan_with_globs( &[ @@ -1235,19 +637,6 @@ mod scanner { vec!["@source '**/*'", "@source './**/*.{styl}'"], ); - assert_snapshot!(tree, @" - . (git) - └── ✓ app - ├── ✓ (theme) - │ └── ✓ page.styl - ├── ✓ [...slug] - │ └── ✓ page.styl - ├── ✓ [[...slug]] - │ └── ✓ page.styl - └── ✓ [slug] - └── ✓ page.styl - "); - assert_eq!( candidates, vec![ @@ -1289,14 +678,6 @@ mod scanner { let mut scanner = Scanner::new(sources); let candidates = scanner.scan(); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . (git) - ├── ✓ project-a - │ └── ✓ index.html - └── ✓ project-b - └── ✓ index.html - "); - // We've done the initial scan and found the files assert_eq!( candidates, @@ -1312,7 +693,6 @@ mod scanner { let ScanResult { candidates, normalized_sources, - tree, .. } = scan_with_globs( &[ @@ -1325,13 +705,6 @@ mod scanner { vec!["@source '**/*'", "@source 'foo.styl'"], ); - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ foo.styl - └── ✓ foo.styl - "); - assert_eq!(candidates, vec!["content-['foo.styl']"]); assert_eq!(normalized_sources, vec!["**/*", "foo.styl"]); } @@ -1361,14 +734,6 @@ mod scanner { let mut scanner = Scanner::new(sources); let candidates = scanner.scan(); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . (git) - ├── ✓ project-a - │ └── ✓ index.html - └── ✓ project-b - └── ✓ index.html - "); - // We've done the initial scan and found the files assert_eq!( candidates, @@ -1473,238 +838,6 @@ mod scanner { "content-['project-b/sub1/sub2/new.html']" ] ); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . (git) - ├── ✓ project-a - │ ├── ✓ index.html - │ ├── ✓ new.html - │ └── ✓ sub1 - │ └── ✓ sub2 - │ ├── ✓ index.html - │ └── ✓ new.html - └── ✓ project-b - ├── ✓ index.html - ├── ✓ new.html - └── ✓ sub1 - └── ✓ sub2 - ├── ✓ index.html - └── ✓ new.html - "); - } - - #[test] - fn it_should_remove_deleted_files_from_scanned_files() { - // Create a temporary working directory - let dir = tempdir().unwrap().into_path(); - - // Initialize this directory as a git repository - let _ = Command::new("git").arg("init").current_dir(&dir).output(); - - // Create files - create_files_in( - &dir, - &[ - ("src/index.html", "content-['src/index.html']"), - ("src/keep.html", "content-['src/keep.html']"), - ("src/remove.html", "content-['src/remove.html']"), - ], - ); - - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - dir.clone(), - "@source '**/*'", - )]); - - scanner.scan(); - assert_eq!( - scanned_files(&mut scanner, &dir), - vec!["src/index.html", "src/keep.html", "src/remove.html"] - ); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . (git) - └── ✓ src - ├── ✓ index.html - ├── ✓ keep.html - └── ✓ remove.html - "); - - fs::remove_file(dir.join("src/remove.html")).unwrap(); - - scanner.scan(); - assert_eq!( - scanned_files(&mut scanner, &dir), - vec!["src/index.html", "src/keep.html"] - ); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . (git) - └── ✓ src - ├── ✓ index.html - └── ✓ keep.html - "); - } - - #[test] - fn it_should_remove_deleted_directories_from_scanned_files() { - // Create a temporary working directory - let dir = tempdir().unwrap().into_path(); - - // Initialize this directory as a git repository - let _ = Command::new("git").arg("init").current_dir(&dir).output(); - - // Create files - create_files_in( - &dir, - &[ - ("src/index.html", "content-['src/index.html']"), - ("src/keep/index.html", "content-['src/keep/index.html']"), - ("src/remove/index.html", "content-['src/remove/index.html']"), - ( - "src/remove/nested/index.html", - "content-['src/remove/nested/index.html']", - ), - ], - ); - - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - dir.clone(), - "@source '**/*'", - )]); - - scanner.scan(); - assert_eq!( - scanned_files(&mut scanner, &dir), - vec![ - "src/index.html", - "src/keep/index.html", - "src/remove/index.html", - "src/remove/nested/index.html", - ] - ); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . (git) - └── ✓ src - ├── ✓ index.html - ├── ✓ keep - │ └── ✓ index.html - └── ✓ remove - ├── ✓ index.html - └── ✓ nested - └── ✓ index.html - "); - - fs::remove_dir_all(dir.join("src/remove")).unwrap(); - - scanner.scan(); - assert_eq!( - scanned_files(&mut scanner, &dir), - vec!["src/index.html", "src/keep/index.html"] - ); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . (git) - └── ✓ src - ├── ✓ index.html - └── ✓ keep - └── ✓ index.html - "); - } - - #[test] - fn it_should_remove_deleted_directories_from_scanned_globs() { - // Create a temporary working directory - let dir = tempdir().unwrap().into_path(); - - // Initialize this directory as a git repository - let _ = Command::new("git").arg("init").current_dir(&dir).output(); - - // Create files - create_files_in( - &dir, - &[ - ("index.html", "content-['index.html']"), - ("src/index.html", "content-['src/index.html']"), - ], - ); - - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - dir.clone(), - "@source '**/*'", - )]); - - scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . (git) - ├── ✓ index.html - └── ✓ src - └── ✓ index.html - "); - - let globs = scanned_globs(&mut scanner, &dir); - assert!(globs.iter().any(|glob| glob.starts_with("src/**/*"))); - - fs::remove_dir_all(dir.join("src")).unwrap(); - - scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . (git) - └── ✓ index.html - "); - - let globs = scanned_globs(&mut scanner, &dir); - assert!(!globs.iter().any(|glob| glob.starts_with("src/**/*"))); - } - - #[test] - fn it_should_track_files_scanned_by_the_last_scan() { - let dir = tempdir().unwrap().into_path(); - - let _ = Command::new("git").arg("init").current_dir(&dir).output(); - - create_files_in( - &dir, - &[ - ("src/index.html", "content-['src/index.html']"), - ("src/keep.html", "content-['src/keep.html']"), - ], - ); - - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - dir.clone(), - "@source '**/*'", - )]); - - assert_eq!( - scanner.scan(), - vec!["content-['src/index.html']", "content-['src/keep.html']"] - ); - - assert_eq!( - scanner.scan(), - vec!["content-['src/index.html']", "content-['src/keep.html']"] - ); - assert_eq!(scanner.get_scanned_files(), Vec::::new()); - - sleep(Duration::from_millis(10)); - fs::write(dir.join("src/index.html"), "content-['src/changed.html']").unwrap(); - - assert_eq!( - scanner.scan(), - vec![ - "content-['src/changed.html']", - "content-['src/index.html']", - "content-['src/keep.html']", - ] - ); - assert_eq!( - normalize_files(scanner.get_scanned_files(), &dir), - vec!["src/index.html"] - ); } #[test] @@ -1714,8 +847,6 @@ mod scanner { files, globs, normalized_sources, - tree, - .. } = scan_with_globs( &[ ("src/index.ts", "content-['src/index.ts']"), @@ -1743,25 +874,6 @@ mod scanner { ], ); - assert_snapshot!(tree, @" - . (git) - └── ✓ src - ├── ✓ admin - │ └── ✓ foo - │ └── ✓ template.html - ├── ✗ colors - │ ├── ✗ blue.tsx - │ ├── ✗ green.tsx - │ └── ✗ red.jsx - ├── ✗ index.ts - ├── ✓ templates - │ └── ✓ index.html - └── ✗ utils - ├── ✗ date.ts - ├── ✗ file.ts - └── ✗ string.ts - "); - assert_eq!( candidates, vec![ @@ -1807,8 +919,6 @@ mod scanner { files, globs, normalized_sources, - tree, - .. } = scan_with_globs( // Typically skipped &[ @@ -1820,15 +930,6 @@ mod scanner { vec!["@source '**/*'", "@source 'src/**/*.{exe,bin}'"], ); - assert_snapshot!(tree, @" - . (git) - ├── ✗ out - │ └── ✗ out.exe - └── ✓ src - ├── ✓ index.bin - └── ✓ index.exe - "); - assert_eq!( candidates, vec!["content-['src/index.bin']", "content-['src/index.exe']",] @@ -1856,8 +957,6 @@ mod scanner { files, globs, normalized_sources, - tree, - .. } = scan_with_globs( &[ ("index.html", "content-['index.html']"), @@ -1882,21 +981,6 @@ mod scanner { ], ); - assert_snapshot!(tree, @" - . (git) - ├── ✗ index.html - └── ✓ src - ├── ✓ admin - │ ├── ✗ ignore.html - │ └── ✓ index.html - ├── ✓ dashboard - │ ├── ✗ ignore.html - │ └── ✓ index.html - ├── ✗ ignore.html - ├── ✗ index.html - └── ✗ lib.ts - "); - assert_eq!( candidates, vec![ @@ -1913,409 +997,6 @@ mod scanner { assert_eq!(normalized_sources, vec!["src/**/*.html"]); } - #[test] - fn it_should_restrict_explicit_file_sources_to_the_matching_file() { - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - &[ - ("src/foo.html", "content-['src/foo.html']"), - ("src/bar.html", "content-['src/bar.html']"), - ], - vec!["@source './src/foo.html'"], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ src - ├── ✗ bar.html - └── ✓ foo.html - "); - - assert_eq!(candidates, vec!["content-['src/foo.html']"]); - assert_eq!(files, vec!["src/foo.html"]); - } - - #[test] - fn it_should_combine_multiple_restricted_sources_for_the_same_base() { - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - &[ - ("src/foo.html", "content-['src/foo.html']"), - ("src/bar.html", "content-['src/bar.html']"), - ("src/baz.html", "content-['src/baz.html']"), - ], - vec!["@source './src/foo.html'", "@source './src/bar.html'"], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ src - ├── ✓ bar.html - ├── ✗ baz.html - └── ✓ foo.html - "); - - assert_eq!( - candidates, - vec!["content-['src/bar.html']", "content-['src/foo.html']"] - ); - assert_eq!(files, vec!["src/bar.html", "src/foo.html"]); - } - - // https://github.com/tailwindlabs/tailwindcss/issues/20333 - #[test] - fn it_should_combine_nested_and_root_restricted_sources() { - // The restriction (`*`) added for the root-level file must not prevent walking into the - // `nested` folder that another explicit source points into. At the same time, relaxing - // that restriction to `/*` must not accidentally open up sibling folders (`ignore-me`) - // or files that no explicit source points at. - let paths_with_content = &[ - ("nested/component.html", "content-['nested/component.html']"), - ("nested/ignore-me.html", "content-['nested/ignore-me.html']"), - ("ignore-me/component.html", "content-['ignore-me']"), - ("component-sources.classes.txt", "content-['classes.txt']"), - ("ignore-me.txt", "content-['ignore-me.txt']"), - ]; - - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - paths_with_content, - vec![ - "@source './nested/component.html'", - "@source './component-sources.classes.txt'", - ], - ); - - assert_snapshot!(tree, @" - . (git) - ├── ✓ component-sources.classes.txt - ├── ✗ ignore-me - │ └── ✗ component.html - ├── ✗ ignore-me.txt - └── ✓ nested - ├── ✓ component.html - └── ✗ ignore-me.html - "); - - assert_eq!( - candidates, - vec![ - "content-['classes.txt']", - "content-['nested/component.html']" - ] - ); - assert_eq!( - files, - vec!["component-sources.classes.txt", "nested/component.html"] - ); - - // Same setup, but with the root-level source declared first - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - paths_with_content, - vec![ - "@source './component-sources.classes.txt'", - "@source './nested/component.html'", - ], - ); - - assert_snapshot!(tree, @" - . (git) - ├── ✓ component-sources.classes.txt - ├── ✗ ignore-me - │ └── ✗ component.html - ├── ✗ ignore-me.txt - └── ✓ nested - ├── ✓ component.html - └── ✗ ignore-me.html - "); - - assert_eq!( - candidates, - vec![ - "content-['classes.txt']", - "content-['nested/component.html']" - ] - ); - assert_eq!( - files, - vec!["component-sources.classes.txt", "nested/component.html"] - ); - } - - #[test] - fn it_should_allow_later_ignores_to_override_restricted_sources() { - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - &[("src/foo.html", "content-['src/foo.html']")], - vec!["@source './src/foo.html'", "@source not './src/foo.html'"], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✗ src - └── ✗ foo.html - "); - - assert!(candidates.is_empty()); - assert!(files.is_empty()); - } - - #[test] - fn it_should_handle_sources_with_parent_patterns() { - { - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - &[ - ("src/foo.html", "content-['src/foo.html']"), - ("src/bar/index.html", "content-['src/bar/index.html']"), - ("src/bar/ignore.html", "content-['src/bar/ignore.html']"), - ], - vec!["@source './src/ba*/*.html'"], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ src - ├── ✓ bar - │ ├── ✓ ignore.html - │ └── ✓ index.html - └── ✗ foo.html - "); - - assert_eq!( - candidates, - vec![ - "content-['src/bar/ignore.html']", - "content-['src/bar/index.html']" - ] - ); - assert_eq!(files, vec!["src/bar/ignore.html", "src/bar/index.html"]); - } - - { - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - &[ - ("src/foo.html", "content-['src/foo.html']"), - ("src/bar/index.html", "content-['src/bar/index.html']"), - ("src/bar/ignore.html", "content-['src/bar/ignore.html']"), - ], - vec![ - "@source './src/ba*/*.html'", - "@source not './src/bar/ignore.html'", // Ignore the ignore.html - ], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ src - ├── ✓ bar - │ ├── ✗ ignore.html - │ └── ✓ index.html - └── ✗ foo.html - "); - - assert_eq!(candidates, vec!["content-['src/bar/index.html']"]); - assert_eq!(files, vec!["src/bar/index.html"]); - } - - { - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - &[ - ("src/foo.html", "content-['src/foo.html']"), - ("src/bar/index.html", "content-['src/bar/index.html']"), - ("src/bar/ignore.html", "content-['src/bar/ignore.html']"), - ], - vec!["@source '**/*'", "@source not './src/ba*/*.html'"], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ src - ├── ✗ bar - │ ├── ✗ ignore.html - │ └── ✗ index.html - └── ✓ foo.html - "); - - assert_eq!(candidates, vec!["content-['src/foo.html']"]); - assert_eq!(files, vec!["src/foo.html"]); - } - - { - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - &[ - ("src/foo.html", "content-['src/foo.html']"), - ("src/bar/index.html", "content-['src/bar/index.html']"), - ("src/bar/ignore.html", "content-['src/bar/ignore.html']"), - ], - vec![ - "@source '**/*'", - "@source not './src/ba*/*.html'", - "@source './src/bar/index.html'", // - ], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ src - ├── ✓ bar - │ ├── ✗ ignore.html - │ └── ✓ index.html - └── ✓ foo.html - "); - - assert_eq!( - candidates, - vec!["content-['src/bar/index.html']", "content-['src/foo.html']"] - ); - assert_eq!(files, vec!["src/bar/index.html", "src/foo.html"]); - } - } - - #[test] - fn nested_explicit_source_should_not_suppress_siblings_of_broad_source() { - // A broad `**/*` source should keep auto-detecting every file, even when a more - // specific `@source` points at a single file inside a subdirectory. The restriction - // added for the explicit file must not hide its siblings from the broad source. - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - &[ - ("src/components/button.html", "content-['button']"), - ("src/components/card.html", "content-['card']"), - ], - vec!["@source '**/*'", "@source './src/components/button.html'"], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ src - └── ✓ components - ├── ✓ button.html - └── ✓ card.html - "); - - assert_eq!(candidates, vec!["content-['button']", "content-['card']"]); - assert_eq!( - files, - vec!["src/components/button.html", "src/components/card.html"] - ); - } - - #[test] - fn nested_explicit_source_should_not_suppress_siblings_of_auto_source() { - // Same as above, but the broad source is an auto-detected directory (`@source "src"`) - // and the explicit file lives in a nested subdirectory of it. - let ScanResult { - candidates, - files, - tree, - .. - } = scan_with_globs( - &[ - ("src/components/button.html", "content-['button']"), - ("src/components/card.html", "content-['card']"), - ], - vec!["@source 'src'", "@source './src/components/button.html'"], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ src - └── ✓ components - ├── ✓ button.html - └── ✓ card.html - "); - - assert_eq!(candidates, vec!["content-['button']", "content-['card']"]); - assert_eq!( - files, - vec!["src/components/button.html", "src/components/card.html"] - ); - } - - #[test] - fn root_file_source_should_not_suppress_sibling_source_roots() { - let ScanResult { - candidates, tree, .. - } = scan_with_globs( - &[ - ("index.css", ""), - ("src/index.html", "content-['src/index.html']"), - ("pages/foo.html", "content-['pages/foo.html']"), - ("pages/nested/foo.html", "content-['pages/nested/foo.html']"), - ], - vec![ - "@source './src'", - "@source './pages/**/*.html'", - "@source './index.css'", - ], - ); - - assert_snapshot!(tree, @" - . (git) - ├── ✓ index.css - ├── ✓ pages - │ ├── ✓ foo.html - │ └── ✓ nested - │ └── ✓ foo.html - └── ✓ src - └── ✓ index.html - "); - - assert_eq!( - candidates, - vec![ - "content-['pages/foo.html']", - "content-['pages/nested/foo.html']", - "content-['src/index.html']", - ] - ); - } - #[test] fn it_respects_gitignore_in_workspace_root() { let ScanResult { @@ -2323,8 +1004,6 @@ mod scanner { files, globs, normalized_sources, - tree, - .. } = scan_with_globs( &[ (".gitignore", "ignore-1.html\nweb/ignore-2.html"), @@ -2336,19 +1015,6 @@ mod scanner { vec!["@source './src'", "@source './web'"], ); - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ ignore-1.html - │ web/ignore-2.html - ├── ✓ src - │ └── ✓ index.html - └── ✓ web - ├── ✗ ignore-1.html - ├── ✗ ignore-2.html - └── ✓ index.html - "); - assert_eq!( candidates, vec!["content-['src/index.html']", "content-['web/index.html']",] @@ -2366,8 +1032,6 @@ mod scanner { files, globs, normalized_sources, - tree, - .. } = scan_with_globs( &[ ("src/logo.jpg", "content-['/src/logo.jpg']"), @@ -2376,13 +1040,6 @@ mod scanner { vec!["@source './src/logo.{jpg,png}'"], ); - assert_snapshot!(tree, @" - . (git) - └── ✓ src - ├── ✓ logo.jpg - └── ✓ logo.png - "); - assert_eq!( candidates, vec!["content-['/src/logo.jpg']", "content-['/src/logo.png']"] @@ -2399,8 +1056,6 @@ mod scanner { files, globs, normalized_sources, - tree, - .. } = scan_with_globs( &[ (".gitignore", "ignore-1.html\n/web/ignore-2.html"), @@ -2410,17 +1065,6 @@ mod scanner { ], vec!["@source './web'", "@source './web/ignore-1.html'"], ); - - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ ignore-1.html - │ /web/ignore-2.html - └── ✓ web - ├── ✓ ignore-1.html - ├── ✗ ignore-2.html - └── ✓ index.html - "); assert_eq!( candidates, vec![ @@ -2434,201 +1078,6 @@ mod scanner { assert_eq!(normalized_sources, vec!["web/**/*", "web/ignore-1.html"]); } - #[test] - fn it_respects_gitignore_files_with_whitelist_patterns() { - // https://github.com/tailwindlabs/tailwindcss/discussions/20382 - // - // A `.gitignore` that ignores everything (`/*`) and then whitelists specific - // directories and files using negated patterns. - let ScanResult { - files, - candidates, - tree, - .. - } = scan(&[ - ( - ".gitignore", - "/*\n!/app\n!/public\n!/package.json\n!/.gitignore\n", - ), - ("app/index.html", "content-['app/index.html']"), - ("public/index.html", "content-['public/index.html']"), - ("package.json", ""), - // These are all ignored by the `/*` rule because they are not whitelisted - ("build/generated.html", "content-['build/generated.html']"), - ("logs/dev.log", "content-['logs/dev.log']"), - ( - "node_modules/my-ui-lib/index.html", - "content-['node_modules/my-ui-lib/index.html']", - ), - ]); - - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ /* - │ !/app - │ !/public - │ !/package.json - │ !/.gitignore - ├── ✓ app - │ └── ✓ index.html - ├── ✗ build - │ └── ✗ generated.html - ├── ✗ logs - │ └── ✗ dev.log - ├── ✗ node_modules - │ └── ✗ my-ui-lib - │ └── ✗ index.html - ├── ✓ package.json - └── ✓ public - └── ✓ index.html - "); - - assert_eq!( - files, - vec!["app/index.html", "package.json", "public/index.html"] - ); - assert_eq!( - candidates, - vec![ - "content-['app/index.html']", - "content-['public/index.html']" - ] - ); - } - - #[test] - fn it_respects_gitignore_files_with_nested_whitelist_patterns() { - // Example from the official `.gitignore` documentation: - // - // ```gitignore - // # exclude everything except directory foo/bar - // /* - // !/foo - // /foo/* - // !/foo/bar - // ``` - let ScanResult { - files, - candidates, - tree, - .. - } = scan(&[ - ( - ".gitignore", - "# exclude everything except directory foo/bar\n/*\n!/foo\n/foo/*\n!/foo/bar\n", - ), - ("foo/bar/index.html", "content-['foo/bar/index.html']"), - ( - "foo/bar/nested/index.html", - "content-['foo/bar/nested/index.html']", - ), - // These are all ignored because they are not inside `foo/bar` - ("index.html", "content-['index.html']"), - ("foo/index.html", "content-['foo/index.html']"), - ("foo/baz/index.html", "content-['foo/baz/index.html']"), - ]); - - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ # exclude everything except directory foo/bar - │ /* - │ !/foo - │ /foo/* - │ !/foo/bar - ├── ✓ foo - │ ├── ✓ bar - │ │ ├── ✓ index.html - │ │ └── ✓ nested - │ │ └── ✓ index.html - │ ├── ✗ baz - │ │ └── ✗ index.html - │ └── ✗ index.html - └── ✗ index.html - "); - - assert_eq!( - files, - vec!["foo/bar/index.html", "foo/bar/nested/index.html"] - ); - assert_eq!( - candidates, - vec![ - "content-['foo/bar/index.html']", - "content-['foo/bar/nested/index.html']" - ] - ); - } - - #[test] - fn explicit_source_directories_respect_their_own_gitignore() { - // A directory referenced via `@source` behaves like an auto source detection - // root. The `.gitignore` file _inside_ that directory still applies to its - // contents. Only when the directory itself is ignored (by an ancestor - // `.gitignore`, see the tests above) do we bypass the ignore rules. - let ScanResult { - files, - candidates, - tree, - .. - } = scan_with_globs( - &[ - ("vendor/.gitignore", "ignored.html"), - ("vendor/index.html", "content-['vendor/index.html']"), - ("vendor/ignored.html", "content-['vendor/ignored.html']"), - ], - vec!["@source 'vendor'"], - ); - - assert_snapshot!(tree, @" - . (git) - └── ✓ vendor - ├── ✗ .gitignore - │ ignored.html - ├── ✗ ignored.html - └── ✓ index.html - "); - - assert_eq!(files, vec!["vendor/index.html"]); - assert_eq!(candidates, vec!["content-['vendor/index.html']"]); - } - - #[test] - fn explicit_sources_can_include_files_inside_gitignored_parent_directories() { - let ScanResult { - candidates, - files, - globs, - tree, - .. - } = scan_with_globs( - &[ - (".gitignore", "src/efgh/"), - ("src/efgh/index.html", "content-['src/efgh/index.html']"), - ("src/efgh/ignore.js", "content-['src/efgh/ignore.js']"), - ("src/abcd/index.html", "content-['src/abcd/index.html']"), - ], - vec!["@source './src/ef*/*.html'"], - ); - - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ src/efgh/ - └── ✓ src - ├── ✗ abcd - │ └── ✗ index.html - └── ✓ efgh - ├── ✗ ignore.js - └── ✓ index.html - "); - - assert_eq!(candidates, vec!["content-['src/efgh/index.html']"]); - assert_eq!(files, vec!["src/efgh/index.html"]); - assert_eq!(globs, vec!["src/ef*/*.html"]); - } - #[test] fn skips_ignore_files_outside_of_a_repo() { // Create a temporary working directory @@ -2699,33 +1148,7 @@ mod scanner { ), ]; - let mut scanner = Scanner::new(sources.clone()); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✓ home - ├── ✗ .gitignore - │ ignore-home.html - └── ✓ project - ├── ✗ .gitignore - │ ignore-project.html - └── ✓ apps - ├── ✗ .gitignore - │ ignore-apps.html - ├── ✓ admin - │ └── ✓ index.html - ├── ✓ dashboard - │ └── ✓ index.html - └── ✓ web - ├── ✗ .gitignore - │ ignore-web.html - ├── ✗ ignore-apps.html - ├── ✗ ignore-home.html - ├── ✗ ignore-project.html - ├── ✗ ignore-web.html - └── ✓ index.html - "); + let candidates = Scanner::new(sources.clone()).scan(); // All ignore files are applied because there's no git repo assert_eq!( @@ -2743,33 +1166,7 @@ mod scanner { .arg("init") .current_dir(dir.join("home")) .output(); - let mut scanner = Scanner::new(sources.clone()); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✓ home (git) - ├── ✗ .gitignore - │ ignore-home.html - └── ✓ project - ├── ✗ .gitignore - │ ignore-project.html - └── ✓ apps - ├── ✗ .gitignore - │ ignore-apps.html - ├── ✓ admin - │ └── ✓ index.html - ├── ✓ dashboard - │ └── ✓ index.html - └── ✓ web - ├── ✗ .gitignore - │ ignore-web.html - ├── ✗ ignore-apps.html - ├── ✗ ignore-home.html - ├── ✗ ignore-project.html - ├── ✗ ignore-web.html - └── ✓ index.html - "); + let candidates = Scanner::new(sources.clone()).scan(); assert_eq!( candidates, @@ -2788,33 +1185,7 @@ mod scanner { .arg("init") .current_dir(dir.join("home/project")) .output(); - let mut scanner = Scanner::new(sources.clone()); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✓ home - ├── ✗ .gitignore - │ ignore-home.html - └── ✓ project (git) - ├── ✗ .gitignore - │ ignore-project.html - └── ✓ apps - ├── ✗ .gitignore - │ ignore-apps.html - ├── ✓ admin - │ └── ✓ index.html - ├── ✓ dashboard - │ └── ✓ index.html - └── ✓ web - ├── ✗ .gitignore - │ ignore-web.html - ├── ✗ ignore-apps.html - ├── ✓ ignore-home.html - ├── ✗ ignore-project.html - ├── ✗ ignore-web.html - └── ✓ index.html - "); + let candidates = Scanner::new(sources.clone()).scan(); assert_eq!( candidates, @@ -2834,33 +1205,7 @@ mod scanner { .arg("init") .current_dir(dir.join("home/project/apps")) .output(); - let mut scanner = Scanner::new(sources.clone()); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✓ home - ├── ✗ .gitignore - │ ignore-home.html - └── ✓ project - ├── ✗ .gitignore - │ ignore-project.html - └── ✓ apps (git) - ├── ✗ .gitignore - │ ignore-apps.html - ├── ✓ admin - │ └── ✓ index.html - ├── ✓ dashboard - │ └── ✓ index.html - └── ✓ web - ├── ✗ .gitignore - │ ignore-web.html - ├── ✗ ignore-apps.html - ├── ✓ ignore-home.html - ├── ✓ ignore-project.html - ├── ✗ ignore-web.html - └── ✓ index.html - "); + let candidates = Scanner::new(sources.clone()).scan(); assert_eq!( candidates, @@ -2882,33 +1227,7 @@ mod scanner { .current_dir(dir.join("home/project/apps/web")) .output(); - let mut scanner = Scanner::new(sources.clone()); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✓ home - ├── ✗ .gitignore - │ ignore-home.html - └── ✓ project - ├── ✗ .gitignore - │ ignore-project.html - └── ✓ apps - ├── ✗ .gitignore - │ ignore-apps.html - ├── ✓ admin - │ └── ✓ index.html - ├── ✓ dashboard - │ └── ✓ index.html - └── ✓ web (git) - ├── ✗ .gitignore - │ ignore-web.html - ├── ✓ ignore-apps.html - ├── ✓ ignore-home.html - ├── ✓ ignore-project.html - ├── ✗ ignore-web.html - └── ✓ index.html - "); + let candidates = Scanner::new(sources.clone()).scan(); assert_eq!( candidates, @@ -2942,15 +1261,7 @@ mod scanner { public_source_entry_from_pattern(dir.clone(), "@source not 'src/ignore-me.html'"), ]; - let mut scanner = Scanner::new(sources.clone()); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✓ src - ├── ✗ ignore-me.html - └── ✓ keep-me.html - "); + let candidates = Scanner::new(sources.clone()).scan(); assert_eq!(candidates, vec!["content-['keep-me.html']"]); } @@ -2977,17 +1288,7 @@ mod scanner { ), ]; - let mut scanner = Scanner::new(sources.clone()); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✓ src - └── ✓ app - └── ✓ [foo] - ├── ✗ ignore-me.html - └── ✓ keep-me.html - "); + let candidates = Scanner::new(sources.clone()).scan(); assert_eq!(candidates, vec!["content-['keep-me.html']"]); } @@ -3014,12 +1315,6 @@ mod scanner { let candidates = scanner.scan(); assert_eq!(candidates, vec!["content-['src/keep-me.html']"]); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✓ src - └── ✓ keep-me.html - "); - // Create new files that should definitely be ignored create_files_in( &dir, @@ -3047,18 +1342,6 @@ mod scanner { let candidates = scanner.scan(); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ src/ignored-by-gitignore.html - └── ✓ src - ├── ✗ ignore-by-extension.bin - ├── ✓ ignored-by-gitignore.html - ├── ✗ ignored-by-source-not.html - ├── ✓ keep-me.html - └── ✓ new-file.html - "); - assert_eq!( candidates, vec![ @@ -3088,11 +1371,6 @@ mod scanner { let candidates = scanner.scan(); assert!(candidates.is_empty()); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✗ foo.styl - "); - // Explicitly allow `.styl` files let mut scanner = Scanner::new(vec![ public_source_entry_from_pattern(dir.clone(), "@source '**/*'"), @@ -3101,11 +1379,6 @@ mod scanner { let candidates = scanner.scan(); assert_eq!(candidates, vec!["content-['foo.styl']"]); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - └── ✓ foo.styl - "); } #[test] @@ -3130,13 +1403,6 @@ mod scanner { let candidates = scanner.scan(); assert!(candidates.is_empty()); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ index.html - └── ✗ index.html - "); - let mut scanner = Scanner::new(vec![ public_source_entry_from_pattern(dir.clone(), "@source '**/*'"), public_source_entry_from_pattern(dir.clone(), "@source './*.html'"), @@ -3144,13 +1410,6 @@ mod scanner { let candidates = scanner.scan(); assert_eq!(candidates, vec!["content-['index.html']"]); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ index.html - └── ✓ index.html - "); } #[test] @@ -3185,17 +1444,6 @@ mod scanner { let candidates = scanner.scan(); assert_eq!(candidates, vec!["content-['src/index.html']"]); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ node_modules - ├── ✗ node_modules - │ └── ✗ my-ui-lib - │ └── ✗ index.html - └── ✓ src - └── ✓ index.html - "); - // Explicitly listing all `*.html` files, should not include `node_modules` because it's // ignored let sources = vec![public_source_entry_from_pattern( @@ -3207,17 +1455,6 @@ mod scanner { let candidates = scanner.scan(); assert_eq!(candidates, vec!["content-['src/index.html']"]); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ node_modules - ├── ✗ node_modules - │ └── ✗ my-ui-lib - │ └── ✗ index.html - └── ✓ src - └── ✓ index.html - "); - // Explicitly listing all `*.html` files // Explicitly list the `node_modules/my-ui-lib` // @@ -3235,64 +1472,6 @@ mod scanner { "content-['src/index.html']" ] ); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ node_modules - ├── ✓ node_modules - │ └── ✓ my-ui-lib - │ └── ✓ index.html - └── ✓ src - └── ✓ index.html - "); - } - - // https://github.com/tailwindlabs/tailwindcss/issues/19844 - #[test] - fn test_allow_explicit_sources_ignored_by_allow_list_gitignore() { - let ScanResult { - candidates, tree, .. - } = scan_with_globs( - &[ - (".gitignore", "*\n!/app\n!/app/design\n!/app/design/**\n"), - ( - "app/design/frontend/theme/templates/component.phtml", - "content-['app/design/frontend/theme/templates/component.phtml']", - ), - ( - "vendor/acme/theme/module/templates/component.phtml", - "content-['vendor/acme/theme/module/templates/component.phtml']", - ), - ], - vec!["@source 'vendor/acme/theme'"], - ); - - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ * - │ !/app - │ !/app/design - │ !/app/design/** - ├── ✗ app - │ └── ✗ design - │ └── ✗ frontend - │ └── ✗ theme - │ └── ✗ templates - │ └── ✗ component.phtml - └── ✓ vendor - └── ✓ acme - └── ✓ theme - └── ✓ module - └── ✓ templates - └── ✓ component.phtml - "); - - assert_eq!( - candidates, - vec!["content-['vendor/acme/theme/module/templates/component.phtml']"] - ); } #[test] @@ -3302,8 +1481,6 @@ mod scanner { files, globs, normalized_sources, - tree, - .. } = scan_with_globs( &[ ( @@ -3322,17 +1499,6 @@ mod scanner { vec!["@source '**/*'"], ); - assert_snapshot!(tree, @" - . (git) - ├── ✗ node_modules - │ └── ✗ index.html - └── ✓ packages - └── ✓ web - ├── ✓ index.html - └── ✗ node_modules - └── ✗ index.html - "); - assert_eq!(candidates, vec!["content-['packages/web/index.html']"]); assert_eq!(files, vec!["packages/web/index.html",]); @@ -3347,8 +1513,6 @@ mod scanner { files, globs, normalized_sources, - tree, - .. } = scan_with_globs( &[ (".gitignore", "node_modules\ndist"), @@ -3364,18 +1528,6 @@ mod scanner { vec!["@source 'node_modules/my-ui-lib'"], ); - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ node_modules - │ dist - └── ✓ node_modules - └── ✓ my-ui-lib - ├── ✓ dist - │ └── ✓ index.html - └── ✗ node.exe - "); - assert_eq!( candidates, vec!["content-['node_modules/my-ui-lib/dist/index.html']"] @@ -3417,17 +1569,6 @@ mod scanner { let candidates = scanner.scan(); assert_eq!(candidates, vec!["content-['src/components/button.tsx']"]); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ *.jsx - │ generated/ - └── ✓ src - └── ✓ components - ├── ✗ button.jsx - └── ✓ button.tsx - "); - // Create 2 new files, one "good" and one "bad" file, and manually scan them. This should // only return the "good" file because the "bad" one is ignored by a `.gitignore` file. create_files_in( @@ -3488,8 +1629,6 @@ mod scanner { files, globs, normalized_sources, - tree, - .. } = scan_with_globs( &[ ("src/💩.js", "content-['src/💩.js']"), @@ -3499,15 +1638,6 @@ mod scanner { vec!["@source '**/*'", "@source not 'src/🤦‍♂️'"], ); - assert_snapshot!(tree, @" - . (git) - └── ✓ src - ├── ✓ 💩.js - ├── ✗ 🤦‍♂️ - │ └── ✗ foo.tsx - └── ✓ 🤦‍♂️.tsx - "); - assert_eq!( candidates, vec!["content-['src/💩.js']", "content-['src/🤦‍♂️.tsx']"] @@ -3544,23 +1674,6 @@ mod scanner { )]); let candidates = scanner.scan(); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ node_modules - │ dist - └── ✓ node_modules - ├── ✓ .pnpm - │ └── ✓ @org+my-ui-library - │ └── ✓ dist - │ └── ✓ index.ts - └── ✓ @org - ├── ✓ .gitkeep - └── ✓ my-ui-library → ../.pnpm/@org+my-ui-library - └── ✓ dist - └── ✓ index.ts - "); - assert_eq!( candidates, vec!["content-['node_modules/.pnpm/@org+my-ui-library/dist/index.ts']"] @@ -3572,23 +1685,6 @@ mod scanner { )]); let candidates = scanner.scan(); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ node_modules - │ dist - └── ✓ node_modules - ├── ✓ .pnpm - │ └── ✓ @org+my-ui-library - │ └── ✓ dist - │ └── ✓ index.ts - └── ✓ @org - ├── ✗ .gitkeep - └── ✓ my-ui-library → ../.pnpm/@org+my-ui-library - └── ✓ dist - └── ✓ index.ts - "); - assert_eq!( candidates, vec!["content-['node_modules/.pnpm/@org+my-ui-library/dist/index.ts']"] @@ -3600,23 +1696,6 @@ mod scanner { )]); let candidates = scanner.scan(); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ .gitignore - │ node_modules - │ dist - └── ✓ node_modules - ├── ✓ .pnpm - │ └── ✓ @org+my-ui-library - │ └── ✓ dist - │ └── ✓ index.ts - └── ✓ @org - ├── ✓ .gitkeep - └── ✓ my-ui-library → ../.pnpm/@org+my-ui-library - └── ✓ dist - └── ✓ index.ts - "); - assert_eq!( candidates, vec!["content-['node_modules/.pnpm/@org+my-ui-library/dist/index.ts']"] @@ -3634,14 +1713,11 @@ mod scanner { ], ); - // Create recursive symlinks: - // - // - `a → b`, `b/c → c`, `c → a` form a cycle - // - `b/root → .` points back at the root directory - let _ = symlink(dir.join("b"), dir.join("a")); - let _ = symlink(dir.join("c"), dir.join("b/c")); - let _ = symlink(&dir, dir.join("b/root")); - let _ = symlink(dir.join("a"), dir.join("c")); + // Create recursive symlinks + let _ = symlink(dir.join("a"), dir.join("b")); + let _ = symlink(dir.join("b/c"), dir.join("c")); + let _ = symlink(dir.join("b/root"), &dir); + let _ = symlink(dir.join("c"), dir.join("a")); let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( dir.clone(), @@ -3649,24 +1725,6 @@ mod scanner { )]); let candidates = scanner.scan(); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✓ a → b - │ ├── ✗ c → ../c - │ ├── ✓ index.html - │ └── ✗ root → .. - ├── ✓ b - │ ├── ✗ c → ../c - │ ├── ✓ index.html - │ └── ✗ root → .. - ├── ✓ c → a - │ ├── ✗ c → . - │ ├── ✓ index.html - │ └── ✗ root → .. - └── ✓ z - └── ✓ index.html - "); - assert_eq!( candidates, vec!["content-['b/index.html']", "content-['z/index.html']"] @@ -3691,14 +1749,6 @@ mod scanner { )]); let candidates = scanner.scan(); - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✓ abcd - │ └── ✓ xyz.html - └── ✓ efgh → abcd - └── ✓ xyz.html - "); - assert_eq!(candidates, vec!["content-['abcd/xyz.html']"]); // Partially referencing the symlinked folder with a glob, should find the file @@ -3711,333 +1761,6 @@ mod scanner { assert_eq!(candidates, vec!["content-['abcd/xyz.html']"]); } - #[test] - fn test_symlinked_sources_within_the_scanned_tree_can_be_ignored() { - let dir = tempdir().unwrap().into_path(); - create_files_in( - &dir, - &[ - ("src/keep.html", "content-['src/keep.html']"), - ( - "actual-dir/ignore.html", - "content-['actual-dir/ignore.html']", - ), - ("actual-file.html", "content-['actual-file.html']"), - ], - ); - - let _ = symlink(dir.join("actual-dir"), dir.join("linked-dir")); - let _ = symlink_file(dir.join("actual-file.html"), dir.join("linked-file.html")); - - let base = dir.join("src"); - let mut scanner = Scanner::new(vec![ - public_source_entry_from_pattern(base.clone(), "@source '../**/*'"), - public_source_entry_from_pattern(base.clone(), "@source not '../actual-dir'"), - public_source_entry_from_pattern(base.clone(), "@source not '../linked-dir'"), - public_source_entry_from_pattern(base.clone(), "@source not '../actual-file.html'"), - public_source_entry_from_pattern(base.clone(), "@source not '../linked-file.html'"), - ]); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ actual-dir - │ └── ✗ ignore.html - ├── ✗ actual-file.html - ├── ✗ linked-dir → actual-dir - │ └── ✗ ignore.html - ├── ✗ linked-file.html → actual-file.html - └── ✓ src - └── ✓ keep.html - "); - - assert_eq!(candidates, vec!["content-['src/keep.html']"]); - - let mut scanner = Scanner::new(vec![ - public_source_entry_from_pattern(base.clone(), "@source '../**/*'"), - public_source_entry_from_pattern(base.clone(), "@source not '../actual-dir/**/*.html'"), - public_source_entry_from_pattern(base.clone(), "@source not '../linked-dir/**/*.html'"), - public_source_entry_from_pattern(base.clone(), "@source not '../actual-file.html'"), - public_source_entry_from_pattern(base.clone(), "@source not '../linked-file.html'"), - ]); - let candidates = scanner.scan(); - - assert_eq!(candidates, vec!["content-['src/keep.html']"]); - } - - #[test] - fn test_symlinked_sources_outside_the_scanned_tree_can_be_ignored() { - let dir = tempdir().unwrap().into_path(); - create_files_in( - &dir, - &[ - ( - "actual-dir/ignore.html", - "content-['actual-dir/ignore.html']", - ), - ("project/keep.html", "content-['project/keep.html']"), - ], - ); - - let _ = symlink(dir.join("actual-dir"), dir.join("project/linked-dir")); - - let base = dir.join("project"); - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - base.clone(), - "@source '**/*'", - )]); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✓ actual-dir - │ └── ✓ ignore.html - └── ✓ project - ├── ✓ keep.html - └── ✓ linked-dir → ../actual-dir - └── ✓ ignore.html - "); - - assert_eq!( - candidates, - vec![ - "content-['actual-dir/ignore.html']", - "content-['project/keep.html']", - ] - ); - - let mut scanner = Scanner::new(vec![ - public_source_entry_from_pattern(base.clone(), "@source '**/*'"), - public_source_entry_from_pattern(base.clone(), "@source not 'linked-dir'"), - ]); - let candidates = scanner.scan(); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✗ actual-dir - │ └── ✗ ignore.html - └── ✓ project - ├── ✓ keep.html - └── ✗ linked-dir → ../actual-dir - └── ✗ ignore.html - "); - - assert_eq!(candidates, vec!["content-['project/keep.html']"]); - } - - #[test] - fn test_files_behind_symlinks_are_tracked_with_their_canonical_paths() { - let dir = tempdir().unwrap().into_path(); - create_files_in( - &dir, - &[ - ( - "packages/repro/source.html", - "content-['packages/repro/source.html']", - ), - ( - "packages/repro/nested/deep.html", - "content-['packages/repro/nested/deep.html']", - ), - ( - "packages/other/index.html", - "content-['packages/other/index.html']", - ), - ], - ); - - // Mimic a pnpm workspace where `node_modules` contains a symlink to the actual package - fs::create_dir_all(dir.join("node_modules")).unwrap(); - let _ = symlink(dir.join("packages/repro"), dir.join("node_modules/repro")); - - // A directly symlinked file - let _ = symlink_file( - dir.join("packages/other/index.html"), - dir.join("linked.html"), - ); - - let mut scanner = Scanner::new(vec![ - public_source_entry_from_pattern( - dir.clone(), - "@source 'node_modules/repro/source.html'", - ), - // The symlink sits multiple levels up from the file - public_source_entry_from_pattern( - dir.clone(), - "@source 'node_modules/repro/nested/deep.html'", - ), - public_source_entry_from_pattern(dir.clone(), "@source 'linked.html'"), - ]); - - let candidates = scanner.scan(); - assert_eq!( - candidates, - vec![ - "content-['packages/other/index.html']", - "content-['packages/repro/nested/deep.html']", - "content-['packages/repro/source.html']", - ] - ); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✓ linked.html → packages/other/index.html - ├── ✓ node_modules - │ └── ✓ repro → ../packages/repro - │ ├── ✓ nested - │ │ └── ✓ deep.html - │ └── ✓ source.html - └── ✓ packages - ├── ✓ other - │ └── ✓ index.html - └── ✓ repro - ├── ✓ nested - │ └── ✓ deep.html - └── ✓ source.html - "); - - // Both the symlinked paths and the canonical paths should be tracked, such that file - // watchers watching the returned files also watch the real files on disk. - let files = scanned_files(&mut scanner, &dir); - assert_eq!( - files, - vec![ - "linked.html", - "node_modules/repro/nested/deep.html", - "node_modules/repro/source.html", - "packages/other/index.html", - "packages/repro/nested/deep.html", - "packages/repro/source.html", - ] - ); - } - - #[test] - fn test_changes_to_the_canonical_path_of_a_symlinked_file_are_detected() { - let dir = tempdir().unwrap().into_path(); - create_files_in(&dir, &[("packages/repro/source.html", "content-['v1']")]); - - // Mimic a pnpm workspace where `node_modules` contains a symlink to the actual package - fs::create_dir_all(dir.join("node_modules")).unwrap(); - let _ = symlink(dir.join("packages/repro"), dir.join("node_modules/repro")); - - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - dir.clone(), - "@source 'node_modules/repro/source.html'", - )]); - - let candidates = scanner.scan(); - assert_eq!(candidates, vec!["content-['v1']"]); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✓ node_modules - │ └── ✓ repro → ../packages/repro - │ └── ✓ source.html - └── ✓ packages - └── ✓ repro - └── ✓ source.html - "); - - // Update the real file on disk. This is the path file watchers will report changes for. - create_files_in(&dir, &[("packages/repro/source.html", "content-['v2']")]); - - let candidates = scanner.scan_content(vec![ChangedContent::File( - dir.join("packages/repro/source.html"), - "html".into(), - )]); - assert_eq!(candidates, vec!["content-['v2']"]); - } - - #[test] - fn test_new_files_behind_symlinks_are_detected_at_their_canonical_path() { - let dir = tempdir().unwrap().into_path(); - create_files_in(&dir, &[("packages/repro/a.html", "content-['a']")]); - - // Mimic a pnpm workspace where `node_modules` contains a symlink to the actual package - fs::create_dir_all(dir.join("node_modules")).unwrap(); - let _ = symlink(dir.join("packages/repro"), dir.join("node_modules/repro")); - - let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( - dir.clone(), - "@source 'node_modules/repro/*.html'", - )]); - - let candidates = scanner.scan(); - assert_eq!(candidates, vec!["content-['a']"]); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✓ node_modules - │ └── ✓ repro → ../packages/repro - │ └── ✓ a.html - └── ✓ packages - └── ✓ repro - └── ✓ a.html - "); - - // Create a new file in the real directory. File watchers watching the real directory - // will report the new file with its canonical path. - create_files_in(&dir, &[("packages/repro/b.html", "content-['b']")]); - - let candidates = scanner.scan_content(vec![ChangedContent::File( - dir.join("packages/repro/b.html"), - "html".into(), - )]); - assert_eq!(candidates, vec!["content-['b']"]); - - assert_snapshot!(fs_tree(&dir, &scanned_files(&mut scanner, &dir)), @" - . - ├── ✓ node_modules - │ └── ✓ repro → ../packages/repro - │ ├── ✓ a.html - │ └── ✓ b.html - └── ✓ packages - └── ✓ repro - ├── ✓ a.html - └── ✗ b.html - "); - } - - // https://github.com/tailwindlabs/tailwindcss/pull/20408 - #[test] - fn test_resolving_globs_does_not_traverse_gitignored_directories() { - let ScanResult { - files, globs, tree, .. - } = scan_with_globs( - &[ - (".gitignore", "/vendor\n"), - ("vendor/pkg/canary/index.html", ""), - ("src/index.html", ""), - ], - vec!["@source '**/*'", "@source './vendor/pkg/canary'"], - ); - - assert_snapshot!(tree, @" - . (git) - ├── ✗ .gitignore - │ /vendor - ├── ✓ src - │ └── ✓ index.html - └── ✓ vendor - └── ✓ pkg - └── ✓ canary - └── ✓ index.html - "); - - assert_eq!( - files, - vec!["src/index.html", "vendor/pkg/canary/index.html"] - ); - assert_eq!(globs, vec![ - "*", - "src/**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}", - - // This should not include `**` or `**.*.{aspx,...}` otherwise this might be scanned recursively. - "vendor/pkg/canary/*", - ]); - } - #[test] fn test_extract_used_css_variables_from_css() { let dir = tempdir().unwrap().into_path(); @@ -4063,11 +1786,6 @@ mod scanner { ("src/defined-at-start.css", "--color-defined-at-start: red;"), ], ); - fs::write( - dir.join("src/invalid.css"), - b".button { color: var(--color-\x80); }", - ) - .unwrap(); let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( dir.clone(), diff --git a/integrations/cli/config.test.ts b/integrations/cli/config.test.ts index dda70814b..dd93ade64 100644 --- a/integrations/cli/config.test.ts +++ b/integrations/cli/config.test.ts @@ -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` -
- `, - '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)', { diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index 07f8bcb68..be48a2ac4 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -1,11 +1,8 @@ import dedent from 'dedent' import os from 'node:os' import path from 'node:path' -import { fileURLToPath } from 'node:url' import { describe } from 'vitest' -import { candidate, css, html, js, json, retryAssertion, test, ts, txt, yaml } from '../utils' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) +import { candidate, css, html, js, json, test, ts, yaml } from '../utils' const STANDALONE_BINARY = (() => { switch (os.platform()) { @@ -375,120 +372,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` -
- `, - }, - }, - 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` -
- `) - - 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` -
- `, - '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` -
- `, - '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 +556,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 +593,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 +641,7 @@ describe.each([ } } `, - 'src/index.html': html` + 'ssrc/index.html': html`
`, 'src/index.css': css` @@ -828,7 +702,7 @@ describe.each([ } } `, - 'src/index.html': html` + 'ssrc/index.html': html`
`, 'src/index.css': css` @@ -875,7 +749,7 @@ describe.each([ expect(map.at(4, 0)).toMatchObject({ source: null, original: '(none)', - generated: '}\n\n/*# sou...', + generated: '}...', }) }, ) @@ -894,7 +768,7 @@ describe.each([ } } `, - 'src/index.html': html` + 'ssrc/index.html': html`
`, 'src/index.css': css` @@ -935,566 +809,11 @@ describe.each([ generated: 'display: f...', }) - expect(map.at(4, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n\n/*# sou...', - }) - }, - ) - - test( - 'watch mode + inline source maps', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "workspace:^", - "@tailwindcss/cli": "workspace:^" - } - } - `, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/utilities'; - /* */ - `, - }, - }, - async ({ spawn, expect, fs, parseSourceMap }) => { - let process = await spawn( - `${command} --input src/index.css --output dist/out.css --map --watch`, - ) - await process.onStderr((m) => m.includes('Done in')) - - await fs.expectFileToContain('dist/out.css', [candidate`flex`]) - - let originalCss = await fs.read('dist/out.css') - let currentCss = originalCss - - // Make sure we can find a source map - let map = parseSourceMap(currentCss) - - expect(map.at(1, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '/*! tailwi...', - }) - - expect(map.at(2, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.flex {...', - }) - - expect(map.at(3, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'display: f...', - }) - expect(map.at(4, 0)).toMatchObject({ source: null, original: '(none)', generated: '}...', }) - - // Write to project source files - await fs.write('src/index.html', html` -
- `) - - // Wait for the CSS to be rebuilt - await retryAssertion(async () => { - currentCss = await fs.read('dist/out.css') - expect(currentCss).not.toEqual(originalCss) - originalCss = currentCss - }) - - // Make sure the source map was updated - map = parseSourceMap(currentCss) - - expect(map.at(1, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '/*! tailwi...', - }) - - expect(map.at(2, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.flex {...', - }) - - expect(map.at(3, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'display: f...', - }) - - expect(map.at(4, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n.underli...', - }) - - expect(map.at(5, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.underline...', - }) - - expect(map.at(6, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'text-decor...', - }) - - expect(map.at(7, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}...', - }) - - // Write to the main CSS file - await fs.write( - 'src/index.css', - css` - @import 'tailwindcss/utilities'; - @source inline("w-auto"); - `, - ) - - // Wait for the CSS to be rebuilt - await retryAssertion(async () => { - currentCss = await fs.read('dist/out.css') - expect(currentCss).not.toEqual(originalCss) - originalCss = currentCss - }) - - // Make sure the source map was updated - map = parseSourceMap(currentCss) - - expect(map.at(1, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '/*! tailwi...', - }) - - expect(map.at(2, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.flex {...', - }) - - expect(map.at(3, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'display: f...', - }) - - expect(map.at(4, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n.w-auto...', - }) - - expect(map.at(5, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.w-auto {...', - }) - - expect(map.at(6, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'width: aut...', - }) - - expect(map.at(7, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n.underli...', - }) - - expect(map.at(8, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.underline...', - }) - - expect(map.at(9, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'text-decor...', - }) - - expect(map.at(10, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}...', - }) - }, - ) - - test( - 'watch mode + separate source maps', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "workspace:^", - "@tailwindcss/cli": "workspace:^" - } - } - `, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/utilities'; - /* */ - `, - }, - }, - async ({ spawn, expect, fs, parseSourceMap }) => { - let process = await spawn( - `${command} --input src/index.css --output dist/out.css --map dist/out.css.map --watch`, - ) - await process.onStderr((m) => m.includes('Done in')) - - await fs.expectFileToContain('dist/out.css', [candidate`flex`]) - - // Make sure we can find a source map - let originalCss = await fs.read('dist/out.css') - let originalMap = await fs.read('dist/out.css.map') - let currentCss = originalCss - let currentMap = originalMap - - // Make sure we can find a source map - let map = parseSourceMap({ map: currentMap, content: currentCss }) - - expect(map.at(1, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '/*! tailwi...', - }) - - expect(map.at(2, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.flex {...', - }) - - expect(map.at(3, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'display: f...', - }) - - expect(map.at(4, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n\n/*# sou...', - }) - - // Write to project source files - await fs.write('src/index.html', html` -
- `) - - // Wait for the CSS to be rebuilt - await retryAssertion(async () => { - currentCss = await fs.read('dist/out.css') - currentMap = await fs.read('dist/out.css.map') - expect(currentCss).not.toEqual(originalCss) - expect(currentMap).not.toEqual(originalMap) - originalCss = currentCss - originalMap = currentMap - }) - - // Make sure the source map was updated - map = parseSourceMap({ map: currentMap, content: currentCss }) - - expect(map.at(1, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '/*! tailwi...', - }) - - expect(map.at(2, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.flex {...', - }) - - expect(map.at(3, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'display: f...', - }) - - expect(map.at(4, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n.underli...', - }) - - expect(map.at(5, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.underline...', - }) - - expect(map.at(6, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'text-decor...', - }) - - expect(map.at(7, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n\n/*# sou...', - }) - - // Write to the main CSS file - await fs.write( - 'src/index.css', - css` - @import 'tailwindcss/utilities'; - @source inline("w-auto"); - `, - ) - - // Wait for the CSS to be rebuilt - await retryAssertion(async () => { - currentCss = await fs.read('dist/out.css') - currentMap = await fs.read('dist/out.css.map') - expect(currentCss).not.toEqual(originalCss) - expect(currentMap).not.toEqual(originalMap) - originalCss = currentCss - originalMap = currentMap - }) - - // Make sure the source map was updated - map = parseSourceMap({ map: currentMap, content: currentCss }) - - expect(map.at(1, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '/*! tailwi...', - }) - - expect(map.at(2, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.flex {...', - }) - - expect(map.at(3, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'display: f...', - }) - - expect(map.at(4, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n.w-auto...', - }) - - expect(map.at(5, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.w-auto {...', - }) - - expect(map.at(6, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'width: aut...', - }) - - expect(map.at(7, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n.underli...', - }) - - expect(map.at(8, 0)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: '.underline...', - }) - - expect(map.at(9, 2)).toMatchObject({ - source: - kind === 'CLI' - ? expect.stringContaining('utilities.css') - : expect.stringMatching(/\/utilities-\w+\.css$/), - original: '@tailwind...', - generated: 'text-decor...', - }) - - expect(map.at(10, 0)).toMatchObject({ - source: null, - original: '(none)', - generated: '}\n\n/*# sou...', - }) - }, - ) - - 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` -
- `, - '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; - } - " - `) }, ) }) @@ -2013,394 +1332,6 @@ test( }, ) -test( - 'source(…) and `@source` are relative to the file they are in', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "workspace:^", - "@tailwindcss/cli": "workspace:^" - } - } - `, - 'index.css': css` @import './project-a/src/index.css'; `, - - 'project-a/src/index.css': css` - /* Run auto-content detection in ../../project-b */ - @import 'tailwindcss/utilities' source('../../project-b'); - - /* Explicitly using node_modules in the @source allows git ignored folders */ - @source '../../project-c'; - `, - - // Project A is the current folder, but we explicitly configured - // `source(project-b)`, therefore project-a should not be included in - // the output. - 'project-a/src/index.html': html` -
- `, - - // Project B is the configured `source(…)`, therefore auto source - // detection should include known extensions and folders in the output. - 'project-b/src/index.html': html` -
- `, - - // Project C should apply auto source detection, therefore known - // extensions and folders should be included in the output. - 'project-c/src/index.html': html` -
- `, - }, - }, - async ({ fs, exec, root, expect }) => { - await exec('pnpm tailwindcss --input ./index.css --output dist/out.css', { cwd: root }) - - let content = await fs.dumpFiles('./dist/*.css') - - expect(content).not.toContain(candidate`content-['project-a/src/index.html']`) - expect(content).toContain(candidate`content-['project-b/src/index.html']`) - expect(content).toContain(candidate`content-['project-c/src/index.html']`) - }, -) - -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`
`, - 'project-b/ignored/ignored.html': html`
`, - 'project-b/ignored/except.html': html`
`, - }, - }, - 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`
`, - 'project-c/ignored/ignored.html': html`
`, - 'project-c/ignored/except.html': html`
`, - - // 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` -
- `, - // 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` -
- `, - }, - }, - 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` -
- `, - // Included by the `!/public` pattern in `.gitignore` - // - // → Should be included - 'public/index.html': html` -
- `, - // Ignored by the `/*` in `.gitignore` - // - // → Should be ignored - 'build/index.html': html` -
- `, - // Ignored by the `/*` in `.gitignore` and the default `node_modules` rules - // - // → Should be ignored - 'node_modules/my-ui-lib/index.html': html` -
- `, - }, - }, - 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`
`, - 'vendor/ignored/ignored.html': html`
`, - 'vendor/ignored/except.html': html`
`, - - // 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 +1680,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`
`, - }, - }, - 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 +1783,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 +1803,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 +1855,7 @@ test( border-color: inherit; border-collapse: collapse; } - :-moz-focusring:where(:not(iframe)) { + :-moz-focusring { outline: auto; } progress { @@ -3154,33 +2042,6 @@ test( }, ) -test( - 'CSS parse errors should include filename and line number', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "workspace:^", - "@tailwindcss/cli": "workspace:^" - } - } - `, - 'input.css': txt` - .test { - color: red; - */ - } - `, - }, - }, - async ({ exec, expect }) => { - await expect(exec('pnpm tailwindcss --input input.css --output dist/out.css')).rejects.toThrow( - /CssSyntaxError: .*input.css:3:3: Invalid declaration: `\*\/`/, - ) - }, -) - function withBOM(text: string): string { return '\uFEFF' + text } diff --git a/integrations/cli/plugins.test.ts b/integrations/cli/plugins.test.ts index 0b810c538..0597e247b 100644 --- a/integrations/cli/plugins.test.ts +++ b/integrations/cli/plugins.test.ts @@ -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` -
-

Headline

-

- 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. -

-
- `, - 'src/index.css': css` - @import 'tailwindcss/utilities'; - @theme { - --breakpoint-sm: 640px; - } - @plugin '@tailwindcss/typography'; - @config '../tailwind.config.js'; - @custom-variant custom (&.custom); - `, - 'tailwind.config.js': ts` - module.exports = { - theme: { - typography: ({ theme }) => ({ - custom: { - css: { - hr: { - '--x': '1', - '@variant sm:custom': { - '--x': '2', - }, - }, - }, - }, - }), - }, - } - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm tailwindcss --input src/index.css --output dist/out.css') - - // We don't want to see `@variant` in the output - let contents = await fs.read('dist/out.css') - expect(contents).not.toContain('@variant') - - expect(await fs.dumpFiles('dist/out.css')).toMatchInlineSnapshot(` - " - --- dist/out.css --- - .prose-custom :where(hr):not(:where([class~="not-prose"], [class~="not-prose"] *)) { - --x: 1; - @media (width >= 640px) { - &.custom { - --x: 2; - } - } - } - " - `) - }, -) - test( 'builds the `@tailwindcss/forms` plugin utilities', { @@ -191,7 +117,7 @@ test( await fs.expectFileToContain('dist/out.css', [ // `::-webkit-date-and-time-value`, - `input:where([type='checkbox']):indeterminate`, + `[type='checkbox']:indeterminate`, ]) // No classes are included even though they are used in the HTML @@ -281,88 +207,3 @@ test( ]) }, ) - -// https://github.com/tailwindlabs/tailwindcss/issues/15844 -test( - 'builds CSS with a custom plugin compiled from TypeScript', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "tailwindcss": "workspace:^", - "@tailwindcss/cli": "workspace:^" - }, - "devDependencies": { - "typescript": "^5.7.2" - } - } - `, - 'tsconfig.json': json` - { - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "declaration": true, - "composite": true, - "rootDir": "./src", - "outDir": "./.build", - "skipLibCheck": true - }, - "include": ["src/**/*.ts"] - } - `, - 'index.html': html` -
- `, - '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`]) - }, -) diff --git a/integrations/cli/standalone.test.ts b/integrations/cli/standalone.test.ts index f84bf4511..07b8ebfee 100644 --- a/integrations/cli/standalone.test.ts +++ b/integrations/cli/standalone.test.ts @@ -1,7 +1,6 @@ -import nodeFs from 'node:fs/promises' import os from 'node:os' import path from 'node:path' -import { candidate, css, html, IS_WINDOWS, js, json, test, ts } from '../utils' +import { candidate, css, html, js, json, test } from '../utils' const STANDALONE_BINARY = (() => { switch (os.platform()) { @@ -16,41 +15,6 @@ const STANDALONE_BINARY = (() => { } })() -test( - 'does not scan itself for candidates', - { - fs: { - 'package.json': json` - { - "dependencies": {} - } - `, - 'src/index.css': css` @import 'tailwindcss'; `, - }, - }, - async ({ root, fs, exec }) => { - let sourceBinary = path.resolve( - __dirname, - `../../packages/@tailwindcss-standalone/dist/${STANDALONE_BINARY}`, - ) - let binary = IS_WINDOWS ? 'tailwindcss.exe' : 'tailwindcss' - let localBinary = path.join(root, binary) - await nodeFs.copyFile(sourceBinary, localBinary) - - if (!IS_WINDOWS) { - await nodeFs.chmod(localBinary, 0o755) - } - - await exec(`${IS_WINDOWS ? binary : `./${binary}`} --input src/index.css --output dist/out.css`) - - await fs.expectFileNotToContain('dist/out.css', [ - candidate`flex`, - candidate`grid`, - candidate`underline`, - ]) - }, -) - test( 'includes first-party plugins', { @@ -158,7 +122,7 @@ test( }) }) `, - 'src/plugin.ts': ts` + 'src/plugin.ts': js` import plugin from 'tailwindcss/plugin' // Make sure all available JS APIs can be imported and used @@ -168,7 +132,8 @@ test( import defaultTheme from 'tailwindcss/defaultTheme' import * as pkg from 'tailwindcss/package.json' - export interface PluginOptions {} + export interface PluginOptions { + } export default plugin(function ({ addUtilities }) { addUtilities({ diff --git a/integrations/oxide/wasm.test.ts b/integrations/oxide/wasm.test.ts index 2d63c707a..1cbdfce6e 100644 --- a/integrations/oxide/wasm.test.ts +++ b/integrations/oxide/wasm.test.ts @@ -1,6 +1,6 @@ -import { css, js, json, test, yaml } from '../utils' +import { css, js, json, test } from '../utils' -// This test runs the wasm build using the `node:wasi` runtime. +// This test runs the wasm build using the `node:wasi` runtine. // // There are currently a known problems that the Node WASI preview implementation does not properly // handle FS reads on macOS and it does not implement all APIs on Windows. Because of that, this @@ -57,187 +57,3 @@ testFn( `) }, ) - -testFn( - '`@tailwindcss/oxide` falls back to the wasm build when no native binding is available', - { - fs: { - 'package.json': json` - { - "dependencies": { - "@tailwindcss/oxide": "workspace:^" - } - } - `, - 'pnpm-workspace.yaml': yaml` - # Trick pnpm in only supporting an architecture that @tailwindcss/oxide - # doesn't support, and therefore should fallback to the wasm version. - supportedArchitectures: - os: - - openbsd - cpu: - - x64 - `, - 'src/index.js': js` - const className = "content-['src/index.js']" - module.exports = { className } - `, - 'index.mjs': js` - import { createRequire } from 'node:module' - import { join } from 'node:path' - - let require = createRequire(import.meta.url) - let { Scanner } = require('@tailwindcss/oxide') - - let loaded = Object.keys(require.cache) - - let scanner = new Scanner({ - sources: [ - { - base: join(process.cwd(), 'src'), - pattern: '**/*', - negated: false, - }, - ], - }) - - console.log( - JSON.stringify({ - native: loaded.filter((file) => file.endsWith('.node')), - wasi: loaded.some((file) => file.endsWith('tailwindcss-oxide.wasi.cjs')), - candidates: scanner.scan(), - }), - ) - process.exit() - `, - }, - }, - async ({ expect, exec }) => { - // Since vitest runs under `pnpm run`, pnpm's bin shims export a NODE_PATH - // that includes the repository's hidden hoist directory - // (`node_modules/.pnpm/node_modules`), which links every workspace package, - // including all native `@tailwindcss/oxide-*` bindings. - // - // Node uses `NODE_PATH` exactly when the local `node_modules` lookup fails, - // which would defeat the simulated unsupported platform, so clear it. - let output = await exec(`node index.mjs`, { env: { NODE_PATH: '' } }) - let { native, wasi, candidates } = JSON.parse(output) - - // No native binding was installed or loaded, ... - expect(native).toEqual([]) - - // ... the wasm32-wasi binding is what actually loaded, ... - expect(wasi).toBe(true) - - // ... and scanning real files on disk works through it. - expect(candidates).toMatchInlineSnapshot(` - [ - "className", - "const", - "content-['src/index.js']", - "exports", - ] - `) - }, -) - -testFn( - 'the wasm build loads even when preopening the filesystem root is denied', - { - fs: { - 'package.json': json` - { - "dependencies": { - "@tailwindcss/oxide": "workspace:^" - } - } - `, - 'pnpm-workspace.yaml': yaml` - # Trick pnpm in only supporting an architecture that @tailwindcss/oxide - # doesn't support, and therefore should fallback to the wasm version. - supportedArchitectures: - os: - - openbsd - cpu: - - x64 - `, - // The wasm bindings generated by `@napi-rs/cli` preopen the filesystem - // root, which sandboxed platforms (e.g. OpenHarmony, Android) deny with - // `UVWASI_EACCES`, making the wasm fallback fail to load on exactly the - // platforms that need it. - // - // We patch `@napi-rs/cli`'s templates to retry with narrower preopens - // (see `patches/@napi-rs__cli@3.7.4.patch`). Simulate such a sandbox by - // denying `/` preopens. - 'preload.cjs': js` - const wasi = require('node:wasi') - const RealWASI = wasi.WASI - - wasi.WASI = class WASI extends RealWASI { - constructor(options) { - if (options?.preopens?.['/'] !== undefined) { - const error = new Error('UVWASI_EACCES, uvwasi_init') - error.code = 'UVWASI_EACCES' - error.syscall = 'uvwasi_init' - throw error - } - super(options) - } - } - `, - 'src/index.js': js` - const className = "content-['src/index.js']" - module.exports = { className } - `, - 'index.mjs': js` - import { createRequire } from 'node:module' - import { join } from 'node:path' - - let require = createRequire(import.meta.url) - let { Scanner } = require('@tailwindcss/oxide') - - let loaded = Object.keys(require.cache) - - let scanner = new Scanner({ - sources: [ - { - base: join(process.cwd(), 'src'), - pattern: '**/*', - negated: false, - }, - ], - }) - - console.log( - JSON.stringify({ - wasi: loaded.some((file) => file.endsWith('tailwindcss-oxide.wasi.cjs')), - candidates: scanner.scan(), - }), - ) - process.exit() - `, - }, - }, - async ({ expect, exec }) => { - // See the note about NODE_PATH in the test above. - let output = await exec(`node --require ./preload.cjs index.mjs`, { env: { NODE_PATH: '' } }) - - // Only parse the first line, because Node prints an `ExperimentalWarning` - // about WASI to stderr, which `exec` appends to the output. - let { wasi, candidates } = JSON.parse(output.trim().split('\n')[0]) - - // The wasm32-wasi binding loaded despite `/` being denied, ... - expect(wasi).toBe(true) - - // ... and scanning files under the current working directory still works - // through the narrower preopen. - expect(candidates).toMatchInlineSnapshot(` - [ - "className", - "const", - "content-['src/index.js']", - "exports", - ] - `) - }, -) diff --git a/integrations/package.json b/integrations/package.json index ae337b5e4..c6e792fc4 100644 --- a/integrations/package.json +++ b/integrations/package.json @@ -3,9 +3,8 @@ "version": "0.0.0", "private": true, "devDependencies": { - "dedent": "catalog:", + "dedent": "1.7.0", "fast-glob": "^3.3.3", - "source-map-js": "^1.2.1", - "yaml": "^2.9.0" + "source-map-js": "^1.2.1" } } diff --git a/integrations/postcss/core-as-postcss-plugin.test.ts b/integrations/postcss/core-as-postcss-plugin.test.ts index 839c80e7b..17545fea3 100644 --- a/integrations/postcss/core-as-postcss-plugin.test.ts +++ b/integrations/postcss/core-as-postcss-plugin.test.ts @@ -48,9 +48,9 @@ describe.each(Object.keys(variantConfig))('%s', (variant) => { }, }, async ({ exec, expect }) => { - await expect( + expect( exec('pnpm postcss src/index.css --output dist/out.css', undefined, { ignoreStdErr: true }), - ).rejects.toThrow( + ).rejects.toThrowError( `It looks like you're trying to use \`tailwindcss\` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install \`@tailwindcss/postcss\` and update your PostCSS configuration.`, ) }, diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts index 80025f945..dd07fd90e 100644 --- a/integrations/postcss/index.test.ts +++ b/integrations/postcss/index.test.ts @@ -698,7 +698,9 @@ test(
`) - await process.onStderr((message) => message.includes('"./does-not-exist" is not exported')) + await process.onStderr((message) => + message.includes('does-not-exist is not exported from package'), + ) expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(` " @@ -741,7 +743,9 @@ test( `, ) - await process.onStderr((message) => message.includes('"./does-not-exist" is not exported')) + await process.onStderr((message) => + message.includes('does-not-exist is not exported from package'), + ) expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(` " @@ -821,73 +825,3 @@ test( }) }, ) - -test( - 'source(…) and `@source` are relative to the file they are in', - { - fs: { - 'package.json': json` - { - "dependencies": { - "postcss": "^8", - "postcss-cli": "^10", - "tailwindcss": "workspace:^", - "@tailwindcss/postcss": "workspace:^" - } - } - `, - - 'postcss.config.js': js` - module.exports = { - plugins: { - '@tailwindcss/postcss': {}, - }, - } - `, - - 'index.css': css` @import './project-a/src/index.css'; `, - - 'project-a/src/index.css': css` - /* Run auto-content detection in ../../project-b */ - @import 'tailwindcss/utilities' source('../../project-b'); - - /* Explicitly using node_modules in the @source allows git ignored folders */ - @source '../../project-c'; - `, - - // Project A is the current folder, but we explicitly configured - // `source(project-b)`, therefore project-a should not be included in - // the output. - 'project-a/src/index.html': html` -
- `, - - // Project B is the configured `source(…)`, therefore auto source - // detection should include known extensions and folders in the output. - 'project-b/src/index.html': html` -
- `, - - // Project C should apply auto source detection, therefore known - // extensions and folders should be included in the output. - 'project-c/src/index.html': html` -
- `, - }, - }, - async ({ fs, exec, root, expect }) => { - await exec('pnpm postcss ./index.css --output dist/out.css', { cwd: root }) - - let content = await fs.dumpFiles('./dist/*.css') - - expect(content).not.toContain(candidate`content-['project-a/src/index.html']`) - expect(content).toContain(candidate`content-['project-b/src/index.html']`) - expect(content).toContain(candidate`content-['project-c/src/index.html']`) - }, -) diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts index 7fc0ccdcf..969254a75 100644 --- a/integrations/postcss/next.test.ts +++ b/integrations/postcss/next.test.ts @@ -468,7 +468,7 @@ test( // the CSS await fetchStyles(url) - // At this point, no changes should trigger a compile step. If we see any + // At this point, no changes should triger a compile step. If we see any // changes, there is an infinite loop because we (the user) didn't write any // files to disk. // diff --git a/integrations/turbopack/loader.test.ts b/integrations/turbopack/loader.test.ts deleted file mode 100644 index 755cd60df..000000000 --- a/integrations/turbopack/loader.test.ts +++ /dev/null @@ -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 ( - - {children} - - ) - } - `, - 'app/page.js': jsx` - export default function Page() { - return
Hello, Next.js!
- } - `, - '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
Hello, Next.js!
- } - `, - ) - - await retryAssertion(async () => { - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`underline`) - }) - }, -) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index efac411db..75e21c590 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { isRepoDirty } from '../../packages/@tailwindcss-upgrade/src/utils/git' -import { candidate, css, html, js, json, test, ts, txt, yaml } from '../utils' +import { candidate, css, html, js, json, test, ts, yaml } from '../utils' test( 'error when no CSS file with @tailwind is used', @@ -28,7 +28,7 @@ test( }, }, async ({ fs, exec, expect }) => { - let output = await exec('pnpm exec upgrade') + let output = await exec('npx @tailwindcss/upgrade') expect(output).toContain('Cannot find any CSS files that reference Tailwind CSS.') // Files should not be modified @@ -100,7 +100,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` " @@ -158,7 +158,7 @@ test( }) // Ensure the v4 project compiles correctly - await exec('pnpm exec tailwindcss --input src/input.css --output dist/out.css') + await exec('npx tailwindcss --input src/input.css --output dist/out.css') await fs.expectFileToContain('dist/out.css', [ candidate`flex!`, @@ -171,62 +171,9 @@ test( }, ) -test( - 'only migrates files matched by `config.content` when upgrading from v3 to v4', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.js': js` - /** @type {import('tailwindcss').Config} */ - module.exports = { - content: ['./src/**/*.html'], - } - `, - 'src/index.html': html` -
- `, - 'src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - `, - 'templates/email.php': html` -
- `, - '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 --- -
- - --- templates/email.php --- -
- " - `) - }, -) - test( `upgrades a v3 project with prefixes to v4`, { - // Somehow this test takes *way* longer than the rest (but not always?) - timeout: 180_000, fs: { 'package.json': json` { @@ -263,7 +210,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` " @@ -335,7 +282,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(` " @@ -407,7 +354,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(` " @@ -484,7 +431,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(` " @@ -587,7 +534,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(` " @@ -654,7 +601,6 @@ test( test( 'migrates a simple postcss setup', { - timeout: 120_000, fs: { 'package.json': json` { @@ -695,7 +641,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') await fs.expectFileToContain( 'postcss.config.js', @@ -766,7 +712,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') let packageJsonContent = await fs.read('package.json') let packageJson = JSON.parse(packageJsonContent) @@ -813,7 +759,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') let packageJsonContent = await fs.read('package.json') let packageJson = JSON.parse(packageJsonContent) @@ -826,7 +772,6 @@ test( test( 'migrates a postcss setup using package.json config', { - timeout: 120_000, fs: { 'package.json': json` { @@ -865,7 +810,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`) await fs.expectFileToContain( @@ -900,7 +845,6 @@ test( test( 'migrates a postcss setup using a json based config file', { - timeout: 120_000, fs: { 'package.json': json` { @@ -941,7 +885,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`) await fs.expectFileToContain( @@ -1008,7 +952,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(` " @@ -1055,7 +999,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(` " @@ -1099,7 +1043,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(` " @@ -1169,10 +1113,9 @@ test( } `, }, - retry: 0, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(` " @@ -1281,10 +1224,9 @@ test( } `, }, - retry: 0, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(` " @@ -1417,7 +1359,7 @@ test( }, }, async ({ exec, fs, expect }) => { - let output = await exec('pnpm exec upgrade --force') + let output = await exec('npx @tailwindcss/upgrade --force') expect(output).toMatch( /You have one or more stylesheets that are imported into a utility layer and non-utility layer./, @@ -1541,7 +1483,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(` " @@ -1766,8 +1708,8 @@ test( }, }, async ({ exec, expect }) => { - let output = await exec('pnpm exec upgrade --force', {}, { ignoreStdErr: true }).catch((e) => - e.toString(), + let output = await exec('npx @tailwindcss/upgrade --force', {}, { ignoreStdErr: true }).catch( + (e) => e.toString(), ) expect(output).toMatch('Could not determine configuration file for:') @@ -1838,7 +1780,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(` " @@ -1974,7 +1916,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(` " @@ -2094,7 +2036,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(` " @@ -2174,7 +2116,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(` " @@ -2256,10 +2198,9 @@ test( } `, }, - retry: 0, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(` " @@ -2313,7 +2254,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') let pkg = JSON.parse(await fs.read('package.json')) @@ -2392,7 +2333,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') // Files should not be modified expect(await fs.dumpFiles('./*.{js,css,html}')).toMatchInlineSnapshot(` @@ -2404,7 +2345,8 @@ test( --shadow-*: initial; --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --default-ring-width: 4px; + --ring-width-*: initial; + --ring-width: 4px; --blur: var(--custom-default-blur); @@ -2490,7 +2432,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade --force') + await exec('npx @tailwindcss/upgrade --force') // Files should not be modified expect(await fs.dumpFiles('./*.{js,css,html,tsx}')).toMatchInlineSnapshot(` @@ -2557,7 +2499,7 @@ test( @import './utilities.css'; @import './generated/ignore-me.css'; `, - 'src/generated/.gitignore': txt` + 'src/generated/.gitignore': ` * !.gitignore `, @@ -2597,7 +2539,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade ./src/index.css') + await exec('npx @tailwindcss/upgrade ./src/index.css') expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` " @@ -2723,7 +2665,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` " @@ -2736,8 +2678,8 @@ test( text-red-500/10 text-red-500/semitransparent text-red-500/transparent - text-red-500/semitransparent - text-red-500/50.50 + text-red-500/50 + text-red-500/50.5 text-red-500/50.50 text-red-500/50% text-red-500/100%" @@ -2809,7 +2751,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` " @@ -2817,7 +2759,7 @@ test( 
--- ./src/input.css --- - @import 'tailwindcss'; + @import 'tailwindcss'; /* The default border color has changed to \`currentcolor\` in Tailwind CSS v4, @@ -2876,8 +2818,8 @@ test( `, }, }, - async ({ exec, root, fs, expect }) => { - await exec('pnpm exec upgrade') + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') let before = await fs.dumpFiles('./src/**/*.{css,html}') expect(before).toMatchInlineSnapshot(` @@ -2913,13 +2855,13 @@ test( `) // Commit the changes - if (isRepoDirty(root)) { + if (isRepoDirty()) { await exec('git add .') await exec('git commit -m "upgrade"') } // Run the upgrade again - let output = await exec('pnpm exec upgrade') + let output = await exec('npx @tailwindcss/upgrade') expect(output).toContain('No changes were made to your repository') let after = await fs.dumpFiles('./src/**/*.{css,html}') @@ -2998,7 +2940,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` " @@ -3026,202 +2968,6 @@ test( }, ) -test( - 'v4 ignores .env files during template migration', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^4", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'src/app.css': css`@import 'tailwindcss';`, - 'src/index.html': html` -
- `, - '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 --- -
- - --- ./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` -
- `, - - // Should be picked up by auto-content detection - 'templates/migrate-me.php': html` -
- `, - - // 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` -
- `, - }, - }, - 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 --- -
- - --- ./src/migrate-me.less --- -
- - --- ./templates/migrate-me.php --- -
- " - `) - }, -) - -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` -
- `, - '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': ` - 'this file should never be truncated', - ]; - `, - }, - }, - async ({ spawn, fs, expect }) => { - let repeatedCandidates = Array.from( - { length: 250 }, - () => '
', - ).join('\n') - - for (let i = 0; i < 100; i++) { - await fs.write( - `src/templates/template-${i}.php`, - ` ${i}];\n`, - ) - } - - let originalKeepFile = await fs.read('src/keep.php') - let originalTemplate = await fs.read('src/templates/template-0.php') - - // NOTE: This test intentionally keeps `npx` instead of `pnpm exec`. It uses - // a `NODE_OPTIONS` hook that overrides `fs.writeFile` to simulate a bad - // write, and `pnpm exec` may write a bin shim on startup which would trip - // that hook before template migration begins. - let process = await spawn('npx @tailwindcss/upgrade --force', { - env: { - NODE_OPTIONS: '--require=./hook.cjs', - }, - }) - - // We're only interested once we start migrating the templates - await process.onStderr((message) => message.includes('Migrating templates')) - - // Wait for the trigger that we are mid-write - await process.onStderr((message) => message === '__TRUNCATED_TARGET__') - - // Kill the process - await process.dispose() - - expect(await fs.read('src/keep.php')).toBe(originalKeepFile) - expect(await fs.read('src/templates/template-0.php')).toBe(originalTemplate) - - for (let [file, contents] of await fs.glob('src/**/*.{html,php,css}')) { - expect(contents.trim(), `${file} should not be empty after interruption`).not.toBe('') - } - }, -) - test( 'upgrades can run in a pnpm workspace', { @@ -3265,7 +3011,7 @@ test( }, }, async ({ root, exec, fs, expect }) => { - let stdout = await exec('pnpm exec upgrade', { + let stdout = await exec('npx @tailwindcss/upgrade', { cwd: path.join(root, 'project-a'), }) @@ -3339,7 +3085,7 @@ test( }, }, async ({ exec, fs, expect }) => { - let stdout = await exec('pnpm exec upgrade') + let stdout = await exec('npx @tailwindcss/upgrade') expect(stdout).not.toContain( 'Running this command will add the dependency to the workspace root', @@ -3371,61 +3117,6 @@ test( }, ) -// https://github.com/tailwindlabs/tailwindcss/issues/20328 -test( - 'ignored files should not be touched when upgrading from a nested directory', - { - fs: { - 'pnpm-workspace.yaml': yaml` - # - packages: - - packages/* - `, - 'package.json': json` - { - "name": "root", - "private": true - } - `, - 'packages/css/package.json': json` - { - "name": "css-pkg", - "private": true, - "devDependencies": { - "tailwindcss": "^4", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'packages/css/src/input.css': css` - @import 'tailwindcss/utilities.css' layer(utilities) source(none); - `, - 'packages/css/src/index.html': html`
Hi
`, - - // 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 - - -

Astro

- - - `, - 'src/styles/global.css': css`@import 'tailwindcss';`, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm astro build') - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - - await fs.expectFileToContain(files[0][0], [candidate`underline`]) - }, -) diff --git a/integrations/vite/bundled-dev.test.ts b/integrations/vite/bundled-dev.test.ts deleted file mode 100644 index 8ba1b6b73..000000000 --- a/integrations/vite/bundled-dev.test.ts +++ /dev/null @@ -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` - - - - -
Hello, world!
- - `, - 'project-a/src/index.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - }, - }, - 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 { - 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(/]*\ssrc="([^"]+)"/g), - ...html.matchAll(/]*\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` - - - - -
Hello, world!
- - `, - ) - - await fs.write('project-b/src/index.html', html` -
- `) - - 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([]) - }, -) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index c346f8e37..ead71b472 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -7,7 +7,6 @@ import { html, js, json, - jsx, retryAssertion, test, ts, @@ -294,113 +293,6 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { }, ) - // https://github.com/tailwindlabs/tailwindcss/issues/20346 - test( - 'dev mode + symlinked `@source` files pick up changes to the real file', - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - - packages/* - `, - 'project-a/package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "repro-package": "workspace:*", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "^7" - } - } - `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` - - - - -
Hello, world!
- - `, - - // 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` -
- Hello, world! -
- `, - }, - }, - 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` -
- Hello, world! -
- `, - ) - - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`content-['v1']`) - expect(styles).toContain(candidate`content-['v2']`) - }) - }, - ) - test( 'watch mode', { @@ -580,352 +472,6 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { }, ) - describe.each([['^6'], ['7.0.8'], ['7.1.12'], ['7.3.1'], ['8.0.0']])( - 'Using Vite %s', - (version) => { - test( - 'external source file changes update the CSS', - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - `, - 'project-a/package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "${version}" - } - } - `, - 'project-a/vite.config.ts': ts` - import fs from 'node:fs' - import path from 'node:path' - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - logLevel: 'info', - }) - `, - 'project-a/index.html': html` - - - - - -
- - - - `, - 'project-a/src/main.ts': jsx`import { classes } from './app'`, - 'project-a/src/app.ts': jsx`export let classes = "content-['project-a/src/app.ts']"`, - 'project-a/src/index.css': css` - @import 'tailwindcss'; - @source '../../project-b/**/*.php'; - `, - 'project-b/src/index.php': html` -
- `, - }, - }, - async ({ root, spawn, fs, expect }) => { - let process = await spawn('pnpm vite dev --debug hmr', { - cwd: path.join(root, 'project-a'), - }) - await process.onStdout((m) => m.includes('ready in')) - - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - await retryAssertion(async () => { - let styles = await fetchStyles(url, '/index.html') - expect(styles).toContain(candidate`content-['project-b/src/index.php']`) - }) - - // Flush all messages so that we can be sure the next messages are - // from the file changes we're about to make - process.flush() - - // Changing an external .php file hot-updates the generated CSS - { - await fs.write( - 'project-b/src/index.php', - txt`
`, - ) - - // On Vite < 7.1, Vite itself hard-invalidates watched files that - // aren't part of the module graph and reloads the page. - // - // On newer versions nothing reloads the page: the CSS hot-updates - // through the regular pipeline because the changed file is a - // watch dependency of the CSS root. - // - // Reloading the page for external template changes is the - // responsibility of the backend integration (e.g. `laravel-vite-plugin`'s `refresh` option, or `vite-plugin-full-reload`). - // - // https://github.com/tailwindlabs/tailwindcss/issues/20411 - if (version === '^6' || version === '7.0.8') { - await process.onStdout((m) => m.includes('page reload') && m.includes('index.php')) - } else { - await process.onStdout((m) => m.includes('hmr update') && m.includes('index.css')) - } - - // Ensure the styles were regenerated with the new content - let styles = await fetchStyles(url, '/index.html') - expect(styles).toContain(candidate`content-['updated:project-b/src/index.php']`) - } - }, - ) - }, - ) - - // https://github.com/tailwindlabs/tailwindcss/issues/20320 - // https://github.com/tailwindlabs/tailwindcss/issues/19903 - test( - 'editing scanned files that Vite can process as modules does not trigger a full reload', - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - `, - 'project-a/package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "^8" - } - } - `, - 'project-a/vite.config.ts': ts` - import fs from 'node:fs' - import path from 'node:path' - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [ - tailwindcss(), - { - // A plugin that processes a custom file type into a JS - // module, similar to e.g. \`.vue\` or \`.svelte\` files - name: 'custom-file-type', - transform(code, id) { - if (id.endsWith('.custom')) { - return { code: 'export default ' + JSON.stringify(code), map: null } - } - }, - }, - { - // Log all HMR payloads to a file so the test can assert on - // them - name: 'hmr-wiretap', - configureServer(server) { - let logFile = path.resolve('hmr.log') - fs.writeFileSync(logFile, '') - for (let environment of Object.values(server.environments)) { - let send = environment.hot.send.bind(environment.hot) - environment.hot.send = (payload) => { - fs.appendFileSync(logFile, JSON.stringify(payload) + '\\n') - return send(payload) - } - } - }, - }, - ], - }) - `, - 'project-a/index.html': html` - - - - - -
- - - - `, - '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` -
- `, - 'project-a/src/comp-a.custom': html` -
- `, - 'project-a/src/comp-b.custom': html` -
- `, - 'project-a/src/lazy.tsx': jsx` - export default function Lazy() { - return
- } - `, - '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` -
- `, - }, - }, - 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
- } - `, - ) - - 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`
`, - ) - - 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`
`, - ) - - await retryAssertion(async () => { - let log = await fs.read('project-a/hmr.log') - expect(log.split('"type":"update"').length).toBeGreaterThan(updates) - }) - - let styles = await fetchStyles(url, '/index.html') - expect(styles).toContain(candidate`content-['updated:project-b/src/index.php']`) - } - }, - ) - test( `source(none) disables looking at the module graph`, { @@ -1178,92 +724,12 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { async ({ root, fs, exec, expect }) => { await expect(() => exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }), - ).rejects.toThrow('The `source(../i-do-not-exist)` does not exist') + ).rejects.toThrowError('The `source(../i-do-not-exist)` does not exist') let files = await fs.glob('project-a/dist/**/*.css') expect(files).toHaveLength(0) }, ) - - test( - 'source(…) and `@source` are relative to the file they are in', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "^7" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - - - - - `, - 'index.css': css` @import './project-a/src/index.css'; `, - - 'project-a/src/index.css': css` - /* Run auto-content detection in ../../project-b */ - @import 'tailwindcss/utilities' source('../../project-b'); - - /* Explicitly using node_modules in the @source allows git ignored folders */ - @source '../../project-c'; - `, - - // Project A is the current folder, but we explicitly configured - // `source(project-b)`, therefore project-a should not be included in - // the output. - 'project-a/src/index.html': html` -
- `, - - // Project B is the configured `source(…)`, therefore auto source - // detection should include known extensions and folders in the output. - 'project-b/src/index.html': html` -
- `, - - // Project C should apply auto source detection, therefore known - // extensions and folders should be included in the output. - 'project-c/src/index.html': html` -
- `, - }, - }, - async ({ fs, exec, spawn, root, expect }) => { - await exec('pnpm vite build', { cwd: root }) - - let content = await fs.dumpFiles('./dist/assets/*.css') - - expect(content).not.toContain(candidate`content-['project-a/src/index.html']`) - expect(content).toContain(candidate`content-['project-b/src/index.html']`) - expect(content).toContain(candidate`content-['project-c/src/index.html']`) - }, - ) }) test( @@ -1432,14 +898,6 @@ test( `, 'src/index.js': js`import Plotly from 'plotly.js/lib/core'`, - - // `plotly.js` pulls in `es5-ext`, which has a build script. As of pnpm - // v10, build scripts only run when explicitly allowed. - 'pnpm-workspace.yaml': yaml` - # - allowBuilds: - es5-ext: true - `, }, }, async ({ exec, expect, fs }) => { @@ -1487,7 +945,7 @@ test( -
Hello, world!
+
Hello, world!
`, 'src/index.css': css` @@ -1499,16 +957,16 @@ test( async ({ exec, expect, fs }) => { await exec('pnpm vite build') + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + // Should not be minified when optimize is disabled - expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-.css')) - .toMatchInlineSnapshot(` - " - --- dist/assets/index-.css --- - .focus\\:text-\\[black\\]:focus { - color: black; - } - " - `) + let content = await fs.read(filename) + expect(content).toContain('.hover\\:flex {') + expect(content).toContain('&:hover {') + expect(content).toContain('@media (hover: hover) {') + expect(content).toContain('display: flex;') }, ) @@ -1542,7 +1000,7 @@ test( -
Hello, world!
+
Hello, world!
`, 'src/index.css': css` @@ -1554,173 +1012,14 @@ test( async ({ exec, expect, fs }) => { await exec('pnpm vite build') - expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-.css')) - .toMatchInlineSnapshot(` - " - --- dist/assets/index-.css --- - @media (hover: hover) { - .hover\\:text-\\[black\\]:hover { - color: #000; - } - } - " - `) - }, -) - -test( - `the plugin works when using the environment API`, - { - fs: { - 'package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^7" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - plugins: [tailwindcss()], - builder: {}, - environments: { - server: { - build: { - cssMinify: false, - emitAssets: true, - rollupOptions: { input: './src/server.ts' }, - }, - }, - }, - }) - `, - // Has to exist or the build fails - 'index.html': html` -
- `, - 'src/server.ts': js` - // Import the stylesheet in the server build - import a from './index.css?url' - console.log(a) - `, - 'src/index.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - }, - }, - async ({ root, fs, exec, expect }) => { - await exec('pnpm vite build', { cwd: root }) - let files = await fs.glob('dist/**/*.css') expect(files).toHaveLength(1) let [filename] = files[0] - await fs.expectFileToContain(filename, [candidate`content-['index.html']`]) - }, -) - -// https://github.com/tailwindlabs/tailwindcss/issues/17532 -test( - 'deleting a file should not crash Vite', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "6.2.5" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - -
- - - `, - '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` - - - - `, - }, - }, - async ({ fs, spawn, expect }) => { - let process = await spawn('pnpm vite dev') - await process.onStdout((m) => m.includes('ready in')) - - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - await retryAssertion(async () => { - let [main, css] = await Promise.all([ - fetch(`${url}/src/main.js`).then((r) => r.text()), - fetch(`${url}/src/index.css`).then((r) => r.text()), - ]) - - expect(main).toContain('/src/assets/icon.svg?import&url') - expect(main).toContain('/src/index.css') - expect(css).toContain('.fill-red-500') - }) - - process.flush() - - let unexpectedError = process.onStderr((m) => { - return /error|crash|panic/i.test(m) - }) - - await fs.write( - 'src/main.js', - js` - import './index.css' - document.querySelector('#app').innerHTML = 'Hello World' - `, - ) - - await fs.delete('src/assets/icon.svg') - - let result = await Promise.race([ - unexpectedError.then(() => 'stderr'), - new Promise((resolve) => setTimeout(() => resolve('timeout'), 500)), - ]) - - expect(result).toBe('timeout') - - await retryAssertion(async () => { - let response = await fetch(`${url}/src/index.css?t=${Date.now()}`) - expect(response.status).toBe(200) - }) + // Should be optimized but not minified + let content = await fs.read(filename) + expect(content).toContain('@media (hover: hover) {') + expect(content).toContain('.hover\\:flex:hover {') + expect(content).toContain('display: flex;') }, ) diff --git a/integrations/vite/nuxt.test.ts b/integrations/vite/nuxt.test.ts index 20f367020..a6ac545b9 100644 --- a/integrations/vite/nuxt.test.ts +++ b/integrations/vite/nuxt.test.ts @@ -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: { @@ -18,18 +7,18 @@ const SETUP = { "type": "module", "dependencies": { "@tailwindcss/vite": "workspace:^", - "nuxt": "3.16.0", + "nuxt": "3.14.0", "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', diff --git a/integrations/vite/preact.test.ts b/integrations/vite/preact.test.ts deleted file mode 100644 index 8e4deec2b..000000000 --- a/integrations/vite/preact.test.ts +++ /dev/null @@ -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` - - - - - -
- - - - `, - 'src/main.tsx': ts` - import { render } from 'preact' - import { App } from './app' - - render(, document.getElementById('app')!) - `, - 'src/app.tsx': ts` - import { useState } from 'preact/hooks' - - export function App() { - const [count, setCount] = useState(0) - return ( - - ) - } - `, - '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 ( - - ) - } - `, - ) - - 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') - } - }, -) diff --git a/integrations/vite/react-router.test.ts b/integrations/vite/react-router.test.ts index 98b0c928d..5b9574fd6 100644 --- a/integrations/vite/react-router.test.ts +++ b/integrations/vite/react-router.test.ts @@ -102,101 +102,13 @@ test('dev mode', { fs: WORKSPACE }, async ({ fs, spawn, expect }) => { }) }) -test( - // cf. https://github.com/remix-run/react-router/blob/00cb4d7b310663b2e84152700c05d3b503005e83/integration/vite-hmr-hdr-test.ts#L311-L318 - 'dev mode, editing a server-only loader dependency triggers HDR instead of a full reload', - { - fs: { - ...WORKSPACE, - 'package.json': json` - { - "type": "module", - "dependencies": { - "@react-router/dev": "^7", - "@react-router/node": "^7", - "@react-router/serve": "^7", - "@tailwindcss/vite": "workspace:^", - "@types/node": "^20", - "@types/react-dom": "^19", - "@types/react": "^19", - "isbot": "^5", - "react-dom": "^19", - "react-router": "^7", - "react": "^19", - "tailwindcss": "workspace:^", - "vite": "^7" - } - } - `, - 'app/routes/home.tsx': ts` - import type { Route } from './+types/home' - import { direct } from '../direct-hdr-dep' - - export async function loader() { - return { message: direct } - } - - export default function Home({ loaderData }: Route.ComponentProps) { - return ( -
-

{loaderData.message}

- -
- ) - } - `, - 'app/direct-hdr-dep.ts': ts` export const direct = 'HDR: 0' `, - }, - }, - async ({ fs, spawn, expect }) => { - let process = await spawn('pnpm react-router dev') - - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - // check initial state - await retryAssertion(async () => { - let html = await (await fetch(url)).text() - expect(html).toContain('HDR: 0') - - let css = await fetchStyles(url) - expect(css).toContain(candidate`font-bold`) - }) - - // Flush stdout so we only see messages triggered by the edit below. - process.flush() - - // Edit the server-only module. The client environment watches this file - // but it only exists in the server module graph. Without the fix, the - // Tailwind CSS plugin would trigger a full page reload on the client - // instead of letting react-router handle HDR. - await fs.write('app/direct-hdr-dep.ts', ts` export const direct = 'HDR: 1' `) - - // check update - await retryAssertion(async () => { - let html = await (await fetch(url)).text() - expect(html).toContain('HDR: 1') - - let css = await fetchStyles(url) - expect(css).toContain(candidate`font-bold`) - }) - - // Assert the client receives an HMR update (not a full page reload). - await process.onStdout((m) => m.includes('(client) hmr update')) - }, -) - test('build mode', { fs: WORKSPACE }, async ({ spawn, exec, expect }) => { await exec('pnpm react-router build') let process = await spawn('pnpm react-router-serve ./build/server/index.js') let url = '' await process.onStdout((m) => { - let match = /\[react-router-serve\]\s*(http.*) \/?/.exec(m) + let match = /\[react-router-serve\]\s*(http.*)\ \/?/.exec(m) if (match) url = match[1] return url != '' }) diff --git a/integrations/vite/resolvers.test.ts b/integrations/vite/resolvers.test.ts index fc9aca5c7..f8e389a7e 100644 --- a/integrations/vite/resolvers.test.ts +++ b/integrations/vite/resolvers.test.ts @@ -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` - - - - -
Hello, world!
- - `, - '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` - - - - -
Hello, world!
- - `, - '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` - - - - -
Hello, world!
- - `, - '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` - - - - -
Hello, world!
- - `, - '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` - - - - - -
Hello, world!
- - - `, - '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` - - - - - -
Hello, world!
- - - `, - '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` - - - - - - - - - `, - '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` - - - - - - - `, - '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, '-.css'), - ).toMatchInlineSnapshot(` - " - --- ./dist/assets/index-.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` - - - - - - - `, - '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, '-.css'), - ).toMatchInlineSnapshot(` - " - --- ./dist/assets/index-.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( diff --git a/integrations/vite/source-maps.test.ts b/integrations/vite/source-maps.test.ts index fe26d9a7c..0e2594690 100644 --- a/integrations/vite/source-maps.test.ts +++ b/integrations/vite/source-maps.test.ts @@ -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` - - -
Hello, world!
- - `, - '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') - }, -) diff --git a/integrations/vite/ssr.test.ts b/integrations/vite/ssr.test.ts index a1d660816..75442cfa2 100644 --- a/integrations/vite/ssr.test.ts +++ b/integrations/vite/ssr.test.ts @@ -1,72 +1,168 @@ -import { describe } from 'vitest' import { candidate, css, html, json, test, ts } from '../utils' -describe.each([['^5.3'], ['^6.0'], ['^7'], ['^8']])('Using Vite %s', (version) => { - test( - `SSR build`, - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "${version}" - } +const WORKSPACE = { + 'index.html': html` + +
+ + + `, + 'src/index.css': css`@import 'tailwindcss';`, + 'src/index.ts': ts` + import './index.css' + + document.querySelector('#app').innerHTML = \` +
Hello, world!
+ \` + `, + 'server.ts': ts` + import css from './src/index.css?url' + + document.querySelector('#app').innerHTML = \` + +
Hello, world!
+ \` + `, +} + +test( + 'Vite 5', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "_comment": "This test uses Vite 5.3 on purpose. Do not upgrade it to Vite 6.", + "devDependencies": { + "vite": "^5.3" } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' - export default defineConfig({ - build: { - cssMinify: false, - ssrEmitAssets: true, - }, - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - -
- - - `, - 'src/index.css': css`@import 'tailwindcss';`, - 'src/index.ts': ts` - import './index.css' - - document.querySelector('#app').innerHTML = \` -
Hello, world!
- \` - `, - 'server.ts': ts` - import css from './src/index.css?url' - - document.querySelector('#app').innerHTML = \` - -
Hello, world!
- \` - `, - }, + export default defineConfig({ + build: { + cssMinify: false, + ssrEmitAssets: true, + }, + plugins: [tailwindcss()], + }) + `, + ...WORKSPACE, }, - async ({ fs, exec, expect }) => { - await exec('pnpm vite build --ssr server.ts') + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build --ssr server.ts') - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] - await fs.expectFileToContain(filename, [ - candidate`underline`, - candidate`m-2`, - candidate`overline`, - candidate`m-3`, - ]) + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`m-2`, + candidate`overline`, + candidate`m-3`, + ]) + }, +) + +test( + `Vite 6`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^6.0" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { + cssMinify: false, + ssrEmitAssets: true, + }, + plugins: [tailwindcss()], + }) + `, + ...WORKSPACE, }, - ) -}) + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build --ssr server.ts') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`m-2`, + candidate`overline`, + candidate`m-3`, + ]) + }, +) + +test( + `Vite 7`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { + cssMinify: false, + ssrEmitAssets: true, + }, + plugins: [tailwindcss()], + }) + `, + ...WORKSPACE, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build --ssr server.ts') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`m-2`, + candidate`overline`, + candidate`m-3`, + ]) + }, +) diff --git a/integrations/vite/svelte.test.ts b/integrations/vite/svelte.test.ts index e186c3f85..107494207 100644 --- a/integrations/vite/svelte.test.ts +++ b/integrations/vite/svelte.test.ts @@ -101,7 +101,7 @@ test( await fs.expectFileToContain(files[0][0], [ candidate`underline`, '.global{color:var(--color-green-500,oklch(72.3% .219 149.579));animation:2s ease-in-out infinite globalKeyframes}', - /\.local.svelte-.*\{color:var\(--color-red-500,oklch\(63\.7% \.237 25\.331\)\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/, + /\.local.svelte-.*\{color:var\(--color-red-500\,oklch\(63\.7% \.237 25\.331\)\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/, /@keyframes globalKeyframes\{/, /@keyframes svelte-.*-localKeyframes\{/, ]) diff --git a/integrations/vite/sveltekit.test.ts b/integrations/vite/sveltekit.test.ts deleted file mode 100644 index 92e855da9..000000000 --- a/integrations/vite/sveltekit.test.ts +++ /dev/null @@ -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` - - - - %sveltekit.head% - - -
%sveltekit.body%
- - - `, - 'src/routes/+layout.svelte': html` - - - - `, - 'src/routes/+page.svelte': html` -
Hello, world!
- `, - '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`]) - }, -) diff --git a/integrations/vite/vue.test.ts b/integrations/vite/vue.test.ts index b04f57c6b..3fea0db9a 100644 --- a/integrations/vite/vue.test.ts +++ b/integrations/vite/vue.test.ts @@ -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` - - - - `, - ]), - ) - - 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) => ``, - ).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` - - - -
- - - - `, - '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` - - - - `, - ...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` - - - - - - -
- - - - `, - '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` - - `, - - // 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` - - `, - }, - }, - 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` - - `, - ) - - 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') - }, -) diff --git a/integrations/vitest.config.mts b/integrations/vitest.config.ts similarity index 100% rename from integrations/vitest.config.mts rename to integrations/vitest.config.ts diff --git a/integrations/webpack/loader.test.ts b/integrations/webpack/loader.test.ts deleted file mode 100644 index 9c3d870af..000000000 --- a/integrations/webpack/loader.test.ts +++ /dev/null @@ -1,495 +0,0 @@ -import { css, html, js, json, test } from '../utils' - -test( - '@tailwindcss/webpack loader (build)', - { - 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') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .flex { - display: flex; - } - " - `) - }, -) - -test( - '@tailwindcss/webpack loader (watch)', - { - 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') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - }, - }, - async ({ fs, spawn, exec, expect }) => { - // Generate the initial build so output CSS files exist on disk - await exec('pnpm webpack --mode=development') - - let process = await spawn('pnpm webpack --mode=development --watch') - await process.onStdout((m) => m.includes('compiled successfully in')) - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .flex { - display: flex; - } - " - `) - - // Add a new Tailwind class to the HTML file - await fs.write('src/index.html', html` -
- `) - await process.onStdout((m) => m.includes('compiled successfully in')) - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .flex { - display: flex; - } - .underline { - text-decoration-line: underline; - } - " - `) - }, -) - -test( - '@tailwindcss/webpack loader with @apply', - { - 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') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.css': css` - @import 'tailwindcss/theme'; - - .btn { - @apply flex items-center px-4 py-2 rounded-md; - } - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - :root, :host { - --spacing: 0.25rem; - --radius-md: 0.375rem; - } - .btn { - display: flex; - align-items: center; - border-radius: var(--radius-md); - padding-inline: calc(var(--spacing) * 4); - padding-block: calc(var(--spacing) * 2); - } - " - `) - }, -) - -test( - '@tailwindcss/webpack loader with optimization', - { - 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') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - { - loader: '@tailwindcss/webpack', - options: { - optimize: true, - }, - }, - ], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .flex{display:flex} - " - `) - }, -) - -test( - '@tailwindcss/webpack loader with CSS @import', - { - 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') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import './custom.css'; - @import 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - 'src/custom.css': css` - /**/ - @utility custom-util { - color: var(--color-red-500); - } - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - :root, :host { - --color-red-500: oklch(63.7% 0.237 25.331); - } - .flex { - display: flex; - } - .custom-util { - color: var(--color-red-500); - } - " - `) - }, -) - -test( - '@tailwindcss/webpack loader with @plugin', - { - 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') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/utilities'; - @plugin './plugin.js'; - `, - 'src/plugin.js': js` - export default function ({ addUtilities }) { - addUtilities({ - '.custom-underline': { - 'border-bottom': '1px solid green', - }, - }) - } - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .custom-underline { - border-bottom: 1px solid green; - } - " - `) - }, -) - -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']) - }, -) diff --git a/package.json b/package.json index 7d41a469e..2810b0d7c 100644 --- a/package.json +++ b/package.json @@ -48,17 +48,23 @@ }, "license": "MIT", "devDependencies": { - "@playwright/test": "^1.62.1", + "@playwright/test": "^1.56.1", "@types/node": "catalog:", - "postcss": "catalog:", + "postcss": "8.5.6", "postcss-import": "^16.1.1", "prettier": "catalog:", - "prettier-plugin-embed": "^0.5.1", + "prettier-plugin-embed": "^0.5.0", "prettier-plugin-organize-imports": "^4.3.0", - "tsup": "^8.5.1", - "turbo": "^2.10.8", - "typescript": "^5.9.3", - "vitest": "^4.1.10" + "tsup": "^8.5.0", + "turbo": "^2.5.4", + "typescript": "^5.5.4", + "vitest": "^2.0.5" }, - "packageManager": "pnpm@11.9.0" + "packageManager": "pnpm@9.6.0", + "pnpm": { + "patchedDependencies": { + "lightningcss@1.30.2": "patches/lightningcss@1.30.2.patch", + "@parcel/watcher@2.5.1": "patches/@parcel__watcher@2.5.1.patch" + } + } } diff --git a/packages/@tailwindcss-browser/README.md b/packages/@tailwindcss-browser/README.md index 5f532607d..7d21bd883 100644 --- a/packages/@tailwindcss-browser/README.md +++ b/packages/@tailwindcss-browser/README.md @@ -13,10 +13,10 @@

- Build Status + Build Status Total Downloads - Latest Release - License + Latest Release + License

--- @@ -29,8 +29,8 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-browser/package.json b/packages/@tailwindcss-browser/package.json index 70af78c1e..31f1d01ec 100644 --- a/packages/@tailwindcss-browser/package.json +++ b/packages/@tailwindcss-browser/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/browser", - "version": "4.3.3", + "version": "4.1.16", "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.4", + "listhen": "^1.9.0", "tailwindcss": "workspace:*" } } diff --git a/packages/@tailwindcss-browser/playwright.config.ts b/packages/@tailwindcss-browser/playwright.config.ts index c6059dd82..c3ad05b91 100644 --- a/packages/@tailwindcss-browser/playwright.config.ts +++ b/packages/@tailwindcss-browser/playwright.config.ts @@ -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: { diff --git a/packages/@tailwindcss-browser/tests/ui.spec.ts b/packages/@tailwindcss-browser/tests/ui.spec.ts index 241322daa..0b4484c6f 100644 --- a/packages/@tailwindcss-browser/tests/ui.spec.ts +++ b/packages/@tailwindcss-browser/tests/ui.spec.ts @@ -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, } } diff --git a/packages/@tailwindcss-browser/vitest.config.mts b/packages/@tailwindcss-browser/vitest.config.ts similarity index 100% rename from packages/@tailwindcss-browser/vitest.config.mts rename to packages/@tailwindcss-browser/vitest.config.ts diff --git a/packages/@tailwindcss-cli/README.md b/packages/@tailwindcss-cli/README.md index 5f532607d..7d21bd883 100644 --- a/packages/@tailwindcss-cli/README.md +++ b/packages/@tailwindcss-cli/README.md @@ -13,10 +13,10 @@

- Build Status + Build Status Total Downloads - Latest Release - License + Latest Release + License

--- @@ -29,8 +29,8 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-cli/package.json b/packages/@tailwindcss-cli/package.json index 7fa5b5500..d07bea635 100644 --- a/packages/@tailwindcss-cli/package.json +++ b/packages/@tailwindcss-cli/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/cli", - "version": "4.3.3", + "version": "4.1.16", "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.18.3", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "workspace:*" diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index ffef2ae8b..794b54ae3 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -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(fn: () => T): Promise { 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>) { - 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>) { // 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>) { let previous = { css: '', optimizedCss: '', - output: null as string | null, } async function write( @@ -235,36 +182,19 @@ export async function handle(args: Result>) { output += `\n` output += map.inline } else if (typeof args['--map'] === 'string') { - let basePath = - args['--output'] && args['--output'] !== '-' - ? path.dirname(path.resolve(args['--output'])) - : process.cwd() - - let mapPath = path.resolve(args['--map']) - - let relativePath = path.relative(basePath, mapPath) - - output += `\n` - output += map.comment(relativePath) - DEBUG && I.start('Write source map') await outputFile(args['--map'], map.raw) DEBUG && I.end('Write source map') } } - 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 +203,6 @@ export async function handle(args: Result>) { 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 +229,6 @@ export async function handle(args: Result>) { 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 +236,12 @@ export async function handle(args: Result>) { } let [compiler, scanner] = await handleError(() => createCompiler(input, I)) - let cleanupWatchers: (() => Promise)[] = [] // 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)[] = [] 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 +253,36 @@ export async function handle(args: Result>) { // 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 +292,11 @@ export async function handle(args: Result>) { @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 +304,7 @@ export async function handle(args: Result>) { // 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 @@ -405,22 +321,22 @@ export async function handle(args: Result>) { if (args['--map']) { DEBUG && I.start('Build Source Map') - compiledMap = toSourceMap(compiler.buildSourceMap()) + compiledMap = compiler.buildSourceMap() as any DEBUG && I.end('Build Source Map') } } // 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 } @@ -430,7 +346,7 @@ export async function handle(args: Result>) { if (args['--map']) { DEBUG && I.start('Build Source Map') - compiledMap = toSourceMap(compiler.buildSourceMap()) + compiledMap = compiler.buildSourceMap() as any DEBUG && I.end('Build Source Map') } } @@ -438,48 +354,13 @@ export async function handle(args: Result>) { 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 +398,10 @@ export async function handle(args: Result>) { 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 { - 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 +463,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 +497,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, pollInterval: number) { - let disposed = false - let timer: ReturnType | 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))] } diff --git a/packages/@tailwindcss-cli/src/commands/build/utils.ts b/packages/@tailwindcss-cli/src/commands/build/utils.ts index ecdd96726..b41a74358 100644 --- a/packages/@tailwindcss-cli/src/commands/build/utils.ts +++ b/packages/@tailwindcss-cli/src/commands/build/utils.ts @@ -13,19 +13,10 @@ export function drainStdin() { } export async function outputFile(file: string, contents: string) { - // Check for special files like `/dev/stdout` or pipes. We don't want to read from these as that - // will hang the process until the file descriptors are closed. - let isSpecialFile = await fs - .stat(file) - .then((stats) => stats.isCharacterDevice() || stats.isFIFO()) - .catch(() => false) - - if (!isSpecialFile) { - try { - let currentContents = await fs.readFile(file, 'utf8') - if (currentContents === contents) return // Skip writing the file - } catch {} - } + try { + let currentContents = await fs.readFile(file, 'utf8') + if (currentContents === contents) return // Skip writing the file + } catch {} // Ensure the parent directories exist await fs.mkdir(path.dirname(file), { recursive: true }) diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts b/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts deleted file mode 100644 index 4392380a5..000000000 --- a/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import path from 'node:path' -import { PassThrough, Readable } from 'node:stream' -import { fileURLToPath } from 'node:url' -import { describe, expect, test } from 'vitest' -import { runCommandLine, streamStdin } from '.' -import { normalizeWindowsSeparators } from '../../utils/test-helpers' - -let css = normalizeWindowsSeparators( - path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'fixtures/input.css'), -) - -describe('runCommandLine', { timeout: 30_000 }, () => { - test('canonicalizes, collapses, and sorts candidate groups from positional arguments', async () => { - let result = await runCommandLine({ - argv: ['--css', css, 'py-3 p-1 px-3'], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result).toEqual({ - exitCode: 0, - stdout: 'p-3', - stderr: '', - }) - }) - - test('falls back to the default tailwind import when --css is omitted', async () => { - let result = await runCommandLine({ - argv: ['py-3 p-1 px-3'], - cwd: path.dirname(css), - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result).toEqual({ - exitCode: 0, - stdout: 'p-3', - stderr: '', - }) - }) - - test('canonicalizes, collapses, and sorts multiple groups from stdin lines', async () => { - let result = await runCommandLine({ - argv: ['--css', css], - stdin: '[display:_flex_] py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2 focus:hover:p-3 hover:p-1 py-3\n', - stdinIsTTY: false, - stdoutIsTTY: false, - }) - - expect(result).toEqual({ - exitCode: 0, - stdout: 'flex p-3\nm-2 py-3 hover:p-1 focus:hover:p-3', - stderr: '', - }) - }) - - test('collapses equivalent candidates', async () => { - let result = await runCommandLine({ - argv: ['--css', css, 'mt-2 mr-2 mb-2 ml-2'], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result).toEqual({ - exitCode: 0, - stdout: 'm-2', - stderr: '', - }) - }) - - test('renders json output for processed candidate groups', async () => { - let result = await runCommandLine({ - argv: ['--css', css, '--format', 'json', 'py-3 p-1 px-3'], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result.exitCode).toBe(0) - expect(JSON.parse(result.stdout)).toEqual([ - { - input: 'py-3 p-1 px-3', - output: 'p-3', - changed: true, - }, - ]) - expect(result.stderr).toBe('') - }) - - test('splits candidate lists with segment-aware spacing', async () => { - let result = await runCommandLine({ - argv: ['--css', css, '--format', 'json', "content-['hello world'] p-1"], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result.exitCode).toBe(0) - expect(JSON.parse(result.stdout)).toEqual([ - { - input: "content-['hello world'] p-1", - output: "p-1 content-['hello_world']", - changed: true, - }, - ]) - expect(result.stderr).toBe('') - }) - - test('shows a usage error when no candidate groups are provided', async () => { - let result = await runCommandLine({ - argv: ['--css', css], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result.exitCode).toBe(1) - expect(result.stderr).toBe('No candidate groups provided') - expect(result.stdout).toContain('Usage:') - }) - - test('streams canonicalized output line by line', async () => { - let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n') - let { stream: output, collect: collectOutput } = createOutput() - - await streamStdin({ css, cwd: path.dirname(css), format: 'text', input, output }) - - expect(collectOutput()).toBe('p-3\nm-2\n') - }) - - test('streams empty lines as empty lines', async () => { - let input = Readable.from('py-3 p-1 px-3\n\nmt-2 mr-2 mb-2 ml-2\n') - let { stream: output, collect: collectOutput } = createOutput() - - await streamStdin({ css, cwd: path.dirname(css), format: 'text', input, output }) - - expect(collectOutput()).toBe('p-3\n\nm-2\n') - }) - - test('streams json output when requested', async () => { - let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n') - let { stream: output, collect: collectOutput } = createOutput() - - await streamStdin({ css, cwd: path.dirname(css), format: 'json', input, output }) - - expect(JSON.parse(collectOutput())).toEqual([ - { - input: 'py-3 p-1 px-3', - output: 'p-3', - changed: true, - }, - { - input: 'mt-2 mr-2 mb-2 ml-2', - output: 'm-2', - changed: true, - }, - ]) - }) - - test('streams jsonl output when requested', async () => { - let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n') - let { stream: output, collect: collectOutput } = createOutput() - - await streamStdin({ css, cwd: path.dirname(css), format: 'jsonl', input, output }) - - expect(collectOutput()).toBe( - '{"input":"py-3 p-1 px-3","output":"p-3","changed":true}\n' + - '{"input":"mt-2 mr-2 mb-2 ml-2","output":"m-2","changed":true}\n', - ) - }) -}) - -function createOutput() { - let stream = new PassThrough() - let chunks: Buffer[] = [] - stream.on('data', (chunk) => chunks.push(chunk)) - return { stream, collect: () => Buffer.concat(chunks).toString() } -} diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/fixtures/input.css b/packages/@tailwindcss-cli/src/commands/canonicalize/fixtures/input.css deleted file mode 100644 index d4b507858..000000000 --- a/packages/@tailwindcss-cli/src/commands/canonicalize/fixtures/input.css +++ /dev/null @@ -1 +0,0 @@ -@import 'tailwindcss'; diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts b/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts deleted file mode 100644 index 5aecfe51e..000000000 --- a/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import fs from 'node:fs/promises' -import path from 'node:path' -import { createInterface } from 'node:readline' -import type { Readable, Writable } from 'node:stream' -import { compare } from '../../../../tailwindcss/src/utils/compare' -import { segment } from '../../../../tailwindcss/src/utils/segment' -import { args, type Arg } from '../../utils/args' -import { help } from '../help' - -const css = String.raw -export type OutputFormat = 'text' | 'json' | 'jsonl' - -export interface CandidateGroupResult { - input: string - output: string - changed: boolean -} - -export interface RunCommandLineOptions { - argv?: string[] - cwd?: string - stdin?: string | null - stdinIsTTY?: boolean - stdoutIsTTY?: boolean -} - -export interface RunCommandLineResult { - exitCode: number - stdout: string - stderr: string -} - -export function usage() { - return 'tailwindcss canonicalize [classes...]' -} - -function usageWithCss() { - return 'tailwindcss canonicalize --css input.css [classes...]' -} - -function usageWithStream() { - return 'tailwindcss canonicalize --stream [--css input.css]' -} - -export function options() { - return { - '--css': { - type: 'string', - description: - 'CSS entry file used to load the Tailwind design system (defaults to `@import "tailwindcss";`)', - }, - '--format': { - type: 'string', - description: 'Output format', - default: 'text', - values: ['text', 'json', 'jsonl'], - }, - '--stream': { - type: 'boolean', - description: 'Read candidate groups from stdin line by line and write results to stdout', - default: false, - }, - } satisfies Arg -} - -const sharedOptions = { - '--help': { - type: 'boolean', - description: 'Display usage information', - alias: '-h', - default: false, - }, -} satisfies Arg - -export async function runCommandLine({ - argv = process.argv.slice(2), - cwd = process.cwd(), - stdin = null, - stdinIsTTY = process.stdin.isTTY, - stdoutIsTTY = process.stdout.isTTY, -}: RunCommandLineOptions = {}): Promise { - try { - let flags = args( - { - ...options(), - ...sharedOptions, - }, - argv, - ) - - let format = parseFormat(flags['--format'] ?? 'text') - - if ((stdoutIsTTY && argv.length === 0) || flags['--help']) { - return { - exitCode: 0, - stdout: helpMessage() ?? '', - stderr: '', - } - } - - if (flags['--stream']) { - await streamStdin({ - css: flags['--css'], - cwd, - format, - input: process.stdin, - output: process.stdout, - }) - return { exitCode: 0, stdout: '', stderr: '' } - } - - let inputs = flags._.length > 0 ? flags._ : await readCandidateGroups({ stdin, stdinIsTTY }) - - if (inputs.length === 0) { - return usageError('No candidate groups provided') - } - - let results = await processCandidateGroups({ - css: flags['--css'], - cwd, - inputs, - }) - - return { - exitCode: 0, - stdout: formatCandidateResults(results, format), - stderr: '', - } - } catch (error) { - return { - exitCode: 1, - stdout: '', - stderr: error instanceof Error ? error.message : String(error), - } - } -} - -export async function streamStdin({ - css: cssFile, - cwd, - format, - input, - output, -}: { - css: string | null - cwd: string - format: OutputFormat - input: Readable - output: Writable -}): Promise { - let designSystem = await loadDesignSystem(cssFile, cwd) - let rl = createInterface({ input }) - let first = true - - if (format === 'json') { - output.write('[') - } - - for await (let line of rl) { - let result = createCandidateGroupResult(designSystem, line) - - switch (format) { - case 'text': { - output.write(result.output + '\n') - break - } - - case 'jsonl': { - output.write(JSON.stringify(result) + '\n') - break - } - - case 'json': { - if (first) { - output.write('\n') - } else { - output.write(',\n') - } - - output.write(indent(JSON.stringify(result, null, 2), 2)) - first = false - break - } - } - } - - if (format === 'json') { - output.write(first ? ']' : '\n]') - } -} - -export function readCandidateGroups({ - stdin, - stdinIsTTY, -}: { - stdin: string | null - stdinIsTTY: boolean -}) { - if (stdin !== null) { - return Promise.resolve(splitCandidateGroups(stdin)) - } - - if (stdinIsTTY) { - return Promise.resolve([]) - } - - return drainStdin().then(splitCandidateGroups) -} - -export async function drainStdin() { - return new Promise((resolve, reject) => { - let result = '' - - process.stdin.on('data', (chunk) => { - result += chunk - }) - - process.stdin.on('end', () => resolve(result)) - process.stdin.on('error', (err) => reject(err)) - }) -} - -export async function processCandidateGroups({ - css, - cwd = process.cwd(), - inputs, -}: { - css: string | null - cwd?: string - inputs: string[] -}): Promise { - let designSystem = await loadDesignSystem(css, cwd) - - return inputs.map((input) => createCandidateGroupResult(designSystem, input)) -} - -export function formatCandidateResults(results: CandidateGroupResult[], format: OutputFormat) { - switch (format) { - case 'json': - return JSON.stringify(results, null, 2) - case 'jsonl': - return results.map((result) => JSON.stringify(result)).join('\n') - case 'text': - return results.map((result) => result.output).join('\n') - } -} - -function helpMessage() { - return help({ - render: false, - usage: [usage(), usageWithCss(), usageWithStream()], - options: { - ...options(), - ...sharedOptions, - }, - }) -} - -async function loadDesignSystem(cssFile: string | null, cwd: string) { - if (cssFile === null) { - return __unstable__loadDesignSystem( - css` - @import 'tailwindcss'; - `, - { base: cwd }, - ) - } - - let resolvedCssFile = path.resolve(cwd, cssFile) - let content = await fs.readFile(resolvedCssFile, 'utf8') - return __unstable__loadDesignSystem(content, { - base: path.dirname(resolvedCssFile), - }) -} - -function splitCandidateGroups(input: string) { - return input - .split(/\r?\n/g) - .map((line) => line.trim()) - .filter((line) => line.length > 0) -} - -function parseFormat(input: string): OutputFormat { - if (input === 'text' || input === 'json' || input === 'jsonl') { - return input - } - - throw new Error(`Invalid value for --format: ${input}`) -} - -function usageError(message: string): RunCommandLineResult { - return { - exitCode: 1, - stdout: helpMessage() ?? '', - stderr: message, - } -} - -function splitCandidates(input: string) { - let trimmedInput = input.trim() - if (trimmedInput.length === 0) return [] - - return segment(trimmedInput, ' ') - .map((candidate) => candidate.trim()) - .filter((candidate) => candidate.length > 0) -} - -function canonicalize( - designSystem: Awaited>, - input: string, -) { - let candidates = splitCandidates(input) - candidates = designSystem.canonicalizeCandidates(candidates, { - collapse: true, - logicalToPhysical: true, - }) - return defaultSort(designSystem.getClassOrder(candidates)).join(' ') -} - -function createCandidateGroupResult( - designSystem: Awaited>, - input: string, -): CandidateGroupResult { - let output = canonicalize(designSystem, input) - - return { - input, - output, - changed: output !== input, - } -} - -function indent(input: string, size: number) { - let prefix = ' '.repeat(size) - return input - .split('\n') - .map((line) => prefix + line) - .join('\n') -} - -function defaultSort(entries: [string, bigint | null][]) { - return entries - .slice() - .sort(([candidateA, orderA], [candidateZ, orderZ]) => { - if (orderA === orderZ) return compare(candidateA, candidateZ) - if (orderA === null) return -1 - if (orderZ === null) return 1 - return bigSign(orderA - orderZ) || compare(candidateA, candidateZ) - }) - .map(([candidate]) => candidate) -} - -function bigSign(value: bigint) { - if (value > 0n) { - return 1 - } else if (value === 0n) { - return 0 - } else { - return -1 - } -} diff --git a/packages/@tailwindcss-cli/src/commands/help/index.ts b/packages/@tailwindcss-cli/src/commands/help/index.ts index b557d03e0..a28132062 100644 --- a/packages/@tailwindcss-cli/src/commands/help/index.ts +++ b/packages/@tailwindcss-cli/src/commands/help/index.ts @@ -3,34 +3,30 @@ import type { Arg } from '../../utils/args' import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer' export function help({ - render = true, invalid, usage, options, }: { - render?: boolean invalid?: string usage?: string[] options?: Arg }) { // Available terminal width - let width = process.stdout.columns ?? 80 - let lines: string[] = [] - let writeLine = render ? println : (value = '') => void lines.push(value) + let width = process.stdout.columns // Render header - writeLine(header()) + println(header()) // Render the invalid command if (invalid) { - writeLine() - writeLine(`${pc.dim('Invalid command:')} ${invalid}`) + println() + println(`${pc.dim('Invalid command:')} ${invalid}`) } // Render usage if (usage && usage.length > 0) { - writeLine() - writeLine(pc.dim('Usage:')) + println() + println(pc.dim('Usage:')) for (let [idx, example] of usage.entries()) { // Split the usage example into the command and its options. This allows // us to wrap the options based on the available width of the terminal. @@ -69,7 +65,7 @@ export function help({ // tailwindcss other [--watch] [options...] // ``` if (lines.length > 1 && idx !== 0) { - writeLine() + println() } // Print the usage examples based on available width of the terminal. @@ -90,9 +86,9 @@ export function help({ // ``` // // > Note how the second line is indented to align with the first line. - writeLine(indent(`${command}${lines.shift()}`)) + println(indent(`${command}${lines.shift()}`)) for (let line of lines) { - writeLine(indent(line, command.length)) + println(indent(line, command.length)) } } } @@ -135,8 +131,8 @@ export function help({ maxOptionLength = Math.max(maxOptionLength, option.length) } - writeLine() - writeLine(pc.dim('Options:')) + println() + println(pc.dim('Options:')) // The minimum amount of dots between the option and the description. let minimumGap = 8 @@ -164,19 +160,15 @@ export function help({ ) // Print the option, the spacer dots and the start of the description. - writeLine( + println( indent(`${pc.blue(option)} ${pc.dim(pc.gray('\u00B7')).repeat(dotCount)} ${lines.shift()}`), ) // Print the remaining lines of the description, indenting them to align // with the start of the description. for (let line of lines) { - writeLine(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`)) + println(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`)) } } } - - if (!render) { - return lines.join('\n') - } } diff --git a/packages/@tailwindcss-cli/src/index.ts b/packages/@tailwindcss-cli/src/index.ts index 1543f7d13..41807f174 100644 --- a/packages/@tailwindcss-cli/src/index.ts +++ b/packages/@tailwindcss-cli/src/index.ts @@ -3,80 +3,46 @@ import { args, type Arg } from './utils/args' import * as build from './commands/build' -import * as canonicalize from './commands/canonicalize' import { help } from './commands/help' const sharedOptions = { '--help': { type: 'boolean', description: 'Display usage information', alias: '-h' }, } satisfies Arg -function buildUsage(command = 'tailwindcss') { - return `${command} [--input input.css] [--output output.css] [--watch] [--poll=ms] [options…]` -} +const flags = args({ + ...build.options(), + ...sharedOptions, +}) +const command = flags._[0] -function rootHelp({ invalid }: { invalid?: string } = {}) { +// Right now we don't support any sub-commands. Let's show the help message +// instead. +if (command) { help({ - invalid, - usage: [buildUsage('tailwindcss'), buildUsage('tailwindcss build'), canonicalize.usage()], - commands: { - build: 'Build your CSS', - canonicalize: 'Canonicalize candidate lists', - }, + invalid: command, + usage: ['tailwindcss [options]'], options: { ...build.options(), ...sharedOptions }, }) + process.exit(1) } -async function run() { - let argv = process.argv.slice(2) - let command = argv[0] - let rootFlags = args({ ...build.options(), ...sharedOptions }, argv) - - if (command === 'build') { - let flags = args({ ...build.options(), ...sharedOptions }, argv.slice(1)) - - if ((process.stdout.isTTY && argv.length === 1) || flags['--help']) { - help({ - usage: [buildUsage('tailwindcss build')], - options: { ...build.options(), ...sharedOptions }, - }) - process.exit(0) - } - - await build.handle(flags) - return - } - - if (command === 'canonicalize') { - let result = await canonicalize.runCommandLine({ argv: argv.slice(1) }) - - if (result.stdout.length > 0) { - process.stdout.write(`${result.stdout}\n`) - } - - if (result.stderr.length > 0) { - process.stderr.write(`${result.stderr}\n`) - } - - process.exitCode = result.exitCode - return - } - - if ((process.stdout.isTTY && command === undefined) || rootFlags['--help']) { - rootHelp() - process.exit(0) - } - - if (command && !command.startsWith('-')) { - rootHelp({ invalid: command }) - process.exit(1) - } - - let flags = args({ - ...build.options(), - ...sharedOptions, +// Display main help message if no command is being used. +// +// E.g.: +// +// - `tailwindcss` // should show the help message +// +// E.g.: implicit `build` command +// +// - `tailwindcss -o output.css` // should run the build command, not show the help message +// - `tailwindcss > output.css` // should run the build command, not show the help message +if ((process.stdout.isTTY && process.argv[2] === undefined) || flags['--help']) { + help({ + usage: ['tailwindcss [--input input.css] [--output output.css] [--watch] [options…]'], + options: { ...build.options(), ...sharedOptions }, }) - - await build.handle(flags) + process.exit(0) } -await run() +// Handle the build command +build.handle(flags) diff --git a/packages/@tailwindcss-cli/src/utils/args.test.ts b/packages/@tailwindcss-cli/src/utils/args.test.ts index 249fb23eb..51d2c4747 100644 --- a/packages/@tailwindcss-cli/src/utils/args.test.ts +++ b/packages/@tailwindcss-cli/src/utils/args.test.ts @@ -17,38 +17,6 @@ it('should be possible to parse a single argument', () => { `) }) -it('should only return the last value for duplicate arguments', () => { - expect( - args( - { - '--output': { type: 'string', description: 'Output file' }, - }, - ['--output', 'output.css', '--output', 'override.css'], - ), - ).toMatchInlineSnapshot(` - { - "--output": "override.css", - "_": [], - } - `) -}) - -it('uses last value when flag with "-" is supplied multiple times', () => { - let result = args( - { - '--output': { type: 'string', description: 'Output file', alias: '-o' }, - }, - ['--output', 'output.css', '--output', '-'], - ) - - expect(result).toMatchInlineSnapshot(` - { - "--output": "-", - "_": [], - } - `) -}) - it('should fallback to the default value if no flag is passed', () => { expect( args( @@ -153,34 +121,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, - "_": [], - } - `) -}) diff --git a/packages/@tailwindcss-cli/src/utils/args.ts b/packages/@tailwindcss-cli/src/utils/args.ts index 88b306b05..1306aa2f2 100644 --- a/packages/@tailwindcss-cli/src/utils/args.ts +++ b/packages/@tailwindcss-cli/src/utils/args.ts @@ -77,17 +77,9 @@ export function args(options: T, argv = process.argv.slice( let parsed = parse(argv) for (let key in parsed) { - let value = parsed[key] - - if (key !== '_' && Array.isArray(value)) { - value = value[value.length - 1] + if (parsed[key] === '__IO_DEFAULT_VALUE__') { + parsed[key] = '-' } - - if (value === '__IO_DEFAULT_VALUE__') { - value = '-' - } - - parsed[key] = value } let result: { _: string[]; [key: string]: unknown } = { diff --git a/packages/@tailwindcss-cli/src/utils/renderer.test.ts b/packages/@tailwindcss-cli/src/utils/renderer.test.ts index 7acef4551..b9950655f 100644 --- a/packages/@tailwindcss-cli/src/utils/renderer.test.ts +++ b/packages/@tailwindcss-cli/src/utils/renderer.test.ts @@ -1,29 +1,29 @@ import path from 'node:path' import { describe, expect, it } from 'vitest' import { relative, wordWrap } from './renderer' -import { normalizeWindowsSeparators } from './test-helpers' +import { normalizeWindowsSeperators } from './test-helpers' describe('relative', () => { it('should print an absolute path relative to the current working directory', () => { - expect(normalizeWindowsSeparators(relative(path.resolve('index.css')))).toMatchInlineSnapshot( + expect(normalizeWindowsSeperators(relative(path.resolve('index.css')))).toMatchInlineSnapshot( `"./index.css"`, ) }) it('should prefer the shortest value by default', () => { // Shortest between absolute and relative paths - expect(normalizeWindowsSeparators(relative('index.css'))).toMatchInlineSnapshot(`"index.css"`) + expect(normalizeWindowsSeperators(relative('index.css'))).toMatchInlineSnapshot(`"index.css"`) }) it('should be possible to override the current working directory', () => { - expect(normalizeWindowsSeparators(relative('../utils/index.css', '..'))).toMatchInlineSnapshot( + expect(normalizeWindowsSeperators(relative('../utils/index.css', '..'))).toMatchInlineSnapshot( `"./utils/index.css"`, ) }) it('should be possible to always prefer the relative path', () => { expect( - normalizeWindowsSeparators( + normalizeWindowsSeperators( relative('index.css', process.cwd(), { preferAbsoluteIfShorter: false }), ), ).toMatchInlineSnapshot(`"./index.css"`) diff --git a/packages/@tailwindcss-cli/src/utils/renderer.ts b/packages/@tailwindcss-cli/src/utils/renderer.ts index da817dda7..6b0098586 100644 --- a/packages/@tailwindcss-cli/src/utils/renderer.ts +++ b/packages/@tailwindcss-cli/src/utils/renderer.ts @@ -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 diff --git a/packages/@tailwindcss-cli/src/utils/test-helpers.ts b/packages/@tailwindcss-cli/src/utils/test-helpers.ts index 30f4fc777..8e61b0a13 100644 --- a/packages/@tailwindcss-cli/src/utils/test-helpers.ts +++ b/packages/@tailwindcss-cli/src/utils/test-helpers.ts @@ -1,5 +1,5 @@ import path from 'node:path' -export function normalizeWindowsSeparators(p: string) { +export function normalizeWindowsSeperators(p: string) { return path.sep === '\\' ? p.replaceAll('\\', '/') : p } diff --git a/packages/@tailwindcss-node/README.md b/packages/@tailwindcss-node/README.md index 5f532607d..7d21bd883 100644 --- a/packages/@tailwindcss-node/README.md +++ b/packages/@tailwindcss-node/README.md @@ -13,10 +13,10 @@

- Build Status + Build Status Total Downloads - Latest Release - License + Latest Release + License

--- @@ -29,8 +29,8 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index 0eced7366..78f3bb439 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/node", - "version": "4.3.3", + "version": "4.1.16", "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", + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", "lightningcss": "catalog:", - "magic-string": "^1.1.0", + "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "workspace:*" } diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index e5f8782c8..5ce6299af 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -63,9 +63,10 @@ function createCompileOptions({ } } -async function ensureSourceDetectionRootExists(compiler: { - root: Awaited>['root'] -}) { +async function ensureSourceDetectionRootExists( + compiler: { root: Awaited>['root'] }, + base: string, +) { // Verify if the `source(…)` path exists (until the glob pattern starts) if (compiler.root && compiler.root !== 'none') { let globSymbols = /[*{]/ @@ -79,27 +80,25 @@ async function ensureSourceDetectionRootExists(compiler: { } let exists = await fsPromises - .stat(path.resolve(compiler.root.base, basePath.join('/'))) + .stat(path.resolve(base, basePath.join('/'))) .then((stat) => stat.isDirectory()) .catch(() => false) if (!exists) { - throw new Error( - `The \`source(${compiler.root.pattern})\` does not exist or is not a directory.`, - ) + throw new Error(`The \`source(${compiler.root.pattern})\` does not exist`) } } } export async function compileAst(ast: AstNode[], options: CompileOptions) { let compiler = await _compileAst(ast, createCompileOptions(options)) - await ensureSourceDetectionRootExists(compiler) + await ensureSourceDetectionRootExists(compiler, options.base) return compiler } export async function compile(css: string, options: CompileOptions) { let compiler = await _compile(css, createCompileOptions(options)) - await ensureSourceDetectionRootExists(compiler) + await ensureSourceDetectionRootExists(compiler, options.base) return compiler } @@ -166,6 +165,17 @@ async function loadStylesheet( onDependency(resolvedPath) + if (typeof globalThis.__tw_readFile === 'function') { + let file = await globalThis.__tw_readFile(resolvedPath, 'utf-8') + if (file) { + return { + path: resolvedPath, + base: path.dirname(resolvedPath), + content: file, + } + } + } + let file = await fsPromises.readFile(resolvedPath, 'utf-8') return { path: resolvedPath, @@ -194,10 +204,7 @@ async function importModule(path: string): Promise { } } -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), diff --git a/packages/@tailwindcss-node/src/esm-cache.loader.mts b/packages/@tailwindcss-node/src/esm-cache.loader.mts index 96d297a29..edcfc8d77 100644 --- a/packages/@tailwindcss-node/src/esm-cache.loader.mts +++ b/packages/@tailwindcss-node/src/esm-cache.loader.mts @@ -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}`, } } diff --git a/packages/@tailwindcss-node/src/index.cts b/packages/@tailwindcss-node/src/index.cts index 9f2788933..4bca0a5e1 100644 --- a/packages/@tailwindcss-node/src/index.cts +++ b/packages/@tailwindcss-node/src/index.cts @@ -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'))) } diff --git a/packages/@tailwindcss-node/src/index.ts b/packages/@tailwindcss-node/src/index.ts index 8eff1466a..a5e7bf5b4 100644 --- a/packages/@tailwindcss-node/src/index.ts +++ b/packages/@tailwindcss-node/src/index.ts @@ -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'))) } diff --git a/packages/@tailwindcss-node/src/instrumentation.test.ts b/packages/@tailwindcss-node/src/instrumentation.test.ts index 102c7ed8e..1f06796fd 100644 --- a/packages/@tailwindcss-node/src/instrumentation.test.ts +++ b/packages/@tailwindcss-node/src/instrumentation.test.ts @@ -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() diff --git a/packages/@tailwindcss-node/src/instrumentation.ts b/packages/@tailwindcss-node/src/instrumentation.ts index e454e6744..d27858d88 100644 --- a/packages/@tailwindcss-node/src/instrumentation.ts +++ b/packages/@tailwindcss-node/src/instrumentation.ts @@ -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(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() } } diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts index 2f0d39589..6e60f8b87 100644 --- a/packages/@tailwindcss-node/src/optimize.ts +++ b/packages/@tailwindcss-node/src/optimize.ts @@ -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 } diff --git a/packages/@tailwindcss-node/src/require-cache.ts b/packages/@tailwindcss-node/src/require-cache.ts deleted file mode 100644 index ea0562019..000000000 --- a/packages/@tailwindcss-node/src/require-cache.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function clearRequireCache(files: string[]) { - for (let key of files) { - delete require.cache[key] - } -} diff --git a/packages/@tailwindcss-node/src/source-maps.test.ts b/packages/@tailwindcss-node/src/source-maps.test.ts deleted file mode 100644 index 96ff8f05c..000000000 --- a/packages/@tailwindcss-node/src/source-maps.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expect, it } from 'vitest' -import { toSourceMap } from './source-maps' - -it('should emit source maps', () => { - let map = toSourceMap('{"version":3,"sources":[],"names":[],"mappings":""}') - - expect(map.comment('app.css.map')).toBe('/*# sourceMappingURL=app.css.map */\n') -}) diff --git a/packages/@tailwindcss-node/src/source-maps.ts b/packages/@tailwindcss-node/src/source-maps.ts index 58b79cb25..3f02efb24 100644 --- a/packages/@tailwindcss-node/src/source-maps.ts +++ b/packages/@tailwindcss-node/src/source-maps.ts @@ -6,7 +6,6 @@ export type { DecodedSource, DecodedSourceMap } export interface SourceMap { readonly raw: string readonly inline: string - comment(url: string): string } function serializeSourceMap(map: DecodedSourceMap): string { @@ -45,16 +44,16 @@ function serializeSourceMap(map: DecodedSourceMap): string { export function toSourceMap(map: DecodedSourceMap | string): SourceMap { let raw = typeof map === 'string' ? map : serializeSourceMap(map) - function comment(url: string) { - return `/*# sourceMappingURL\x3d${url} */\n` - } - return { raw, get inline() { - let inlined = Buffer.from(raw, 'utf-8').toString('base64') - return comment(`data:application/json;base64,${inlined}`) + let tmp = '' + + tmp += '/*# sourceMappingURL=data:application/json;base64,' + tmp += Buffer.from(raw, 'utf-8').toString('base64') + tmp += ' */\n' + + return tmp }, - comment, } } diff --git a/packages/@tailwindcss-node/src/urls.ts b/packages/@tailwindcss-node/src/urls.ts index 61695a9d1..d6bc22926 100644 --- a/packages/@tailwindcss-node/src/urls.ts +++ b/packages/@tailwindcss-node/src/urls.ts @@ -154,7 +154,7 @@ function skipUrlReplacer(rawUrl: string, aliases?: string[]) { return ( isExternalUrl(rawUrl) || isDataUrl(rawUrl) || - !rawUrl[0].match(/[.a-zA-Z0-9_]/) || + !rawUrl[0].match(/[\.a-zA-Z0-9_]/) || functionCallRE.test(rawUrl) ) } diff --git a/packages/@tailwindcss-node/tsconfig.json b/packages/@tailwindcss-node/tsconfig.json index d2131cbeb..6ae022f65 100644 --- a/packages/@tailwindcss-node/tsconfig.json +++ b/packages/@tailwindcss-node/tsconfig.json @@ -1,6 +1,3 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { - "verbatimModuleSyntax": false, - }, } diff --git a/packages/@tailwindcss-postcss/README.md b/packages/@tailwindcss-postcss/README.md index 3facf2887..f824c2a06 100644 --- a/packages/@tailwindcss-postcss/README.md +++ b/packages/@tailwindcss-postcss/README.md @@ -13,10 +13,10 @@

- Build Status + Build Status Total Downloads - Latest Release - License + Latest Release + License

--- @@ -29,11 +29,11 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. --- @@ -44,14 +44,14 @@ If you're interested in contributing to Tailwind CSS, please read our [contribut You can use the `base` option (defaults to the current working directory) to change the directory in which the plugin searches for source files: ```js -import tailwindcss from '@tailwindcss/postcss' +import tailwindcss from "@tailwindcss/postcss" export default { - plugins: [ - tailwindcss({ - base: path.resolve(__dirname, './path'), - }), - ], + plugins: [ + tailwindcss({ + base: path.resolve(__dirname, "./path) + }) + ] } ``` @@ -109,3 +109,18 @@ export default { ], } ``` + +You may also pass options to `optimize` to enable Lighting CSS but prevent minification: + +```js +import tailwindcss from '@tailwindcss/postcss' + +export default { + plugins: [ + tailwindcss({ + // Enables Lightning CSS but disables minification + optimize: { minify: false }, + }), + ], +} +``` diff --git a/packages/@tailwindcss-postcss/package.json b/packages/@tailwindcss-postcss/package.json index 891578509..6f83e7ae3 100644 --- a/packages/@tailwindcss-postcss/package.json +++ b/packages/@tailwindcss-postcss/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/postcss", - "version": "4.3.3", + "version": "4.1.16", "description": "PostCSS plugin for Tailwind CSS, a utility-first CSS framework for rapidly building custom user interfaces", "license": "MIT", "repository": { @@ -24,27 +24,22 @@ }, "exports": { ".": { - "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" - }, - "require": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" } }, "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "workspace:*", "@tailwindcss/oxide": "workspace:*", - "postcss": "catalog:", + "postcss": "^8.4.41", "tailwindcss": "workspace:*" }, "devDependencies": { "@types/node": "catalog:", "@types/postcss-import": "14.0.3", - "dedent": "catalog:", + "dedent": "1.7.0", "internal-example-plugin": "workspace:*", "postcss-import": "^16.1.1" } diff --git a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap index 05087fa39..8fdc0dbf7 100644 --- a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap +++ b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap @@ -1,8 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` -" -@layer properties { +"@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-font-weight: initial; @@ -12,10 +11,8 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` @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-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", - monospace; + --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-black: #000; --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); @@ -44,7 +41,7 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` -webkit-text-size-adjust: 100%; tab-size: 4; line-height: 1.5; - 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; @@ -110,7 +107,7 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` border-collapse: collapse; } - :-moz-focusring:where(:not(iframe)) { + :-moz-focusring { outline: auto; } @@ -305,6 +302,5 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` @property --tw-font-weight { syntax: "*"; inherits: false -} -" +}" `; diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index b393cc382..593ef70cf 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -4,7 +4,6 @@ import { tmpdir } from 'node:os' import path from 'path' import postcss from 'postcss' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { pretty } from '../../tailwindcss/src/test-utils/run' import tailwindcss from './index' // We give this file path to PostCSS for processing. @@ -20,16 +19,6 @@ function inputCssFilePath() { const css = dedent -async function run(plugin: any, from: string, input: string): Promise { - let ast = postcss.parse(input) - for (let runner of plugin.plugins) { - if (runner.Once) { - await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } }) - } - } - return ast.toString() -} - test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => { let processor = postcss([ tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), @@ -37,7 +26,7 @@ test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => { let result = await processor.process(`@import 'tailwindcss'`, { from: inputCssFilePath() }) - expect(pretty(result.css)).toMatchSnapshot() + expect(result.css.trim()).toMatchSnapshot() // Check for dependency messages expect(result.messages).toContainEqual({ @@ -54,7 +43,7 @@ test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => { }) expect(result.messages).toContainEqual({ type: 'dir-dependency', - dir: expect.stringMatching(/example-project[/|\\]src$/g), + dir: expect.stringMatching(/example-project[\/|\\]src$/g), glob: expect.stringMatching(/^\*\*\/\*/g), parent: expect.any(String), plugin: expect.any(String), @@ -83,9 +72,8 @@ test('output is optimized by Lightning CSS', async () => { { from: inputCssFilePath() }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @layer utilities { + expect(result.css.trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo { color: #000; } @@ -93,8 +81,7 @@ test('output is optimized by Lightning CSS', async () => { .bar { color: red; } - } - " + }" `) }) @@ -113,12 +100,10 @@ test('@apply can be used without emitting the theme in the CSS file', async () = { from: inputCssFilePath() }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .foo { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".foo { color: var(--color-red-500, oklch(63.7% .237 25.331)); - } - " + }" `) }) @@ -135,7 +120,7 @@ describe('processing without specifying a base path', () => { afterEach(() => unlink(filepath)) test('the current working directory is used by default', async () => { - using spy = vi.spyOn(process, 'cwd') + const spy = vi.spyOn(process, 'cwd') spy.mockReturnValue(dir) let processor = postcss([tailwindcss({ optimize: { minify: false } })]) @@ -155,18 +140,6 @@ describe('processing without specifying a base path', () => { }) }) -test('fallback to `base` directory when `result.opts.from` is not provided', async () => { - using _ = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - let processor = postcss([ - tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), - ]) - - let result = await processor.process(`@import 'tailwindcss'`) - - expect(result.css.length).toBeGreaterThan(0) -}) - describe('plugins', () => { test('local CJS plugin', async () => { let processor = postcss([ @@ -181,9 +154,8 @@ describe('plugins', () => { { from: inputCssFilePath() }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .underline { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { text-decoration-line: underline; } @@ -195,8 +167,7 @@ describe('plugins', () => { .hocus\\:underline:focus, .hocus\\:underline:hover { text-decoration-line: underline; - } - " + }" `) }) @@ -213,9 +184,8 @@ describe('plugins', () => { { from: `${__dirname}/fixtures/another-project/input.css` }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .underline { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { text-decoration-line: underline; } @@ -227,8 +197,7 @@ describe('plugins', () => { .hocus\\:underline:focus, .hocus\\:underline:hover { text-decoration-line: underline; - } - " + }" `) }) @@ -245,9 +214,8 @@ describe('plugins', () => { { from: inputCssFilePath() }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .underline { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { text-decoration-line: underline; } @@ -259,8 +227,7 @@ describe('plugins', () => { .hocus\\:underline:focus, .hocus\\:underline:hover { text-decoration-line: underline; - } - " + }" `) }) }) @@ -283,12 +250,10 @@ test('bail early when Tailwind is not used', async () => { // didn't use `@tailwind utilities` we didn't scan for utilities. expect(result.css).not.toContain('.underline {') - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .custom-css { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".custom-css { color: red; - } - " + }" `) }) @@ -310,14 +275,12 @@ test('handle CSS when only using a `@reference` (we should not bail early)', asy { from: inputCssFilePath() }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @media (min-width: 48rem) { + expect(result.css.trim()).toMatchInlineSnapshot(` + "@media (min-width: 48rem) { .foo { bar: baz; } - } - " + }" `) }) @@ -337,12 +300,10 @@ test('handle CSS when using a `@variant` using variants that do not rely on the { from: inputCssFilePath() }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .foo[data-is-hoverable] { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".foo[data-is-hoverable] { bar: baz; - } - " + }" `) }) @@ -377,29 +338,23 @@ test('runs `Once` plugins in the right order', async () => { { from: inputCssFilePath() }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .custom-css { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".custom-css { color: red; - } - " + }" `) - expect(pretty(before)).toMatchInlineSnapshot(` - " - @theme { + expect(before).toMatchInlineSnapshot(` + "@theme { --color-red-500: red; } .custom-css { color: theme(--color-red-500); - } - " + }" `) - expect(pretty(after)).toMatchInlineSnapshot(` - " - .custom-css { + expect(after).toMatchInlineSnapshot(` + ".custom-css { color: red; - } - " + }" `) }) @@ -424,7 +379,7 @@ describe('concurrent builds', () => { afterEach(() => rm(dir, { recursive: true, force: true })) test('does experience a race-condition when calling the plugin two times for the same change', async () => { - using spy = vi.spyOn(process, 'cwd') + const spy = vi.spyOn(process, 'cwd') spy.mockReturnValue(dir) let from = path.join(dir, 'index.css') @@ -432,7 +387,17 @@ describe('concurrent builds', () => { let plugin = tailwindcss({ optimize: { minify: false } }) - let result = await run(plugin, from, input) + async function run(input: string): Promise { + let ast = postcss.parse(input) + for (let runner of (plugin as any).plugins) { + if (runner.Once) { + await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } }) + } + } + return ast.toString() + } + + let result = await run(input) expect(result).toContain('.underline') @@ -448,51 +413,14 @@ describe('concurrent builds', () => { `, ) - let promise1 = run(plugin, from, input) - let promise2 = run(plugin, from, input) + let promise1 = run(input) + let promise2 = run(input) expect(await promise1).toContain('.red') expect(await promise2).toContain('.red') }) }) -test('rebuilds when the input CSS changes even if the `from` file on disk did not', async () => { - let dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss')) - // The `from` file exists on disk with a stable mtime. The input CSS is handed - // to the plugin directly (as a preprocessor like Sass would produce it), so it - // can change without `from`'s mtime ever changing. - await writeFile(path.join(dir, 'index.css'), '') - - let from = path.join(dir, 'index.css') - let plugin = tailwindcss({ base: dir, optimize: { minify: false } }) - - let first = await run( - plugin, - from, - css` - @tailwind utilities; - .first { - @apply underline; - } - `, - ) - let second = await run( - plugin, - from, - css` - @tailwind utilities; - .second { - @apply line-through; - } - `, - ) - - expect(first).toContain('.first') - expect(second).toContain('.second') - - await rm(dir, { recursive: true, force: true }) -}) - test('does not register the input file as a dependency, even if it is passed in as relative path', async () => { let processor = postcss([ tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), @@ -500,12 +428,10 @@ test('does not register the input file as a dependency, even if it is passed in let result = await processor.process(`@tailwind utilities`, { from: './input.css' }) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .underline { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { text-decoration-line: underline; - } - " + }" `) // Check for dependency messages diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index a4a8d9326..2ff4405b6 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -20,7 +20,6 @@ const DEBUG = env.DEBUG interface CacheEntry { mtimes: Map - inputCss: string compiler: null | ReturnType scanner: null | Scanner tailwindCssAst: AstNode[] @@ -35,7 +34,6 @@ function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOp if (cache.has(key)) return cache.get(key)! let entry = { mtimes: new Map(), - inputCss: '', compiler: null, scanner: null, @@ -117,7 +115,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } let context = getContextFromCache(postcss, inputFile, opts) - let inputBasePath = inputFile ? path.dirname(path.resolve(inputFile)) : base + let inputBasePath = path.dirname(path.resolve(inputFile)) // Whether this is the first build or not, if it is, then we can // optimize the build by not creating the compiler until we need it. @@ -196,22 +194,12 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { rebuildStrategy = 'full' context.mtimes.set(file, changedTime) } - - // The mtimes above only track files on disk. When the input CSS is - // generated by another tool (e.g. Sass) and passed to `process()`, - // it can change without any tracked file's mtime changing, so also - // rebuild when the input CSS itself differs from the last build. - let inputCss = root.source?.input.css ?? root.toString() - if (context.inputCss !== inputCss) { - rebuildStrategy = 'full' - context.inputCss = inputCss - } } DEBUG && I.end('Register full rebuild paths') if ( rebuildStrategy === 'full' && - // We can reuse the compiler if it was created during the + // We can re-use the compiler if it was created during the // initial build. If it wasn't, we need to create a new one. !isInitialBuild ) { diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts index fd90c483e..d2f72f664 100644 --- a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts @@ -4,7 +4,6 @@ import postcss from 'postcss' import atImport from 'postcss-import' import { describe, expect, test } from 'vitest' import fixRelativePathsPlugin from '.' -import { pretty } from '../../../tailwindcss/src/test-utils/run' describe('fixRelativePathsPlugin', () => { test('rewrites @source and @plugin to be relative to the initial css file', async () => { @@ -15,13 +14,11 @@ describe('fixRelativePathsPlugin', () => { let result = await processor.process(css, { from: cssPath }) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @source "../../example-project/src/**/*.ts"; + expect(result.css.trim()).toMatchInlineSnapshot(` + "@source "../../example-project/src/**/*.ts"; @source "!../../example-project/src/**/*.ts"; @plugin "../../example-project/src/plugin.js"; - @plugin "../../example-project/src/what\\"s-this.js"; - " + @plugin "../../example-project/src/what\\"s-this.js";" `) }) @@ -33,13 +30,11 @@ describe('fixRelativePathsPlugin', () => { let result = await processor.process(css, { from: cssPath }) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @plugin "/absolute/paths"; + expect(result.css.trim()).toMatchInlineSnapshot(` + "@plugin "/absolute/paths"; @plugin "C:\\Program Files\\HAL 9000"; @plugin "\\\\Media\\Pictures\\Worth\\1000 words"; - @plugin "some-node-dep"; - " + @plugin "some-node-dep";" `) }) @@ -51,16 +46,13 @@ describe('fixRelativePathsPlugin', () => { let result = await processor.process(css, { from: cssPath }) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @plugin './plugin-in-sibling.ts'; + expect(result.css.trim()).toMatchInlineSnapshot(` + "@plugin './plugin-in-sibling.ts'; @plugin '../plugin-in-sibling.ts'; @plugin 'plugin-in-sibling'; - @plugin './plugin-in-root.ts'; @plugin '../plugin-in-root.ts'; - @plugin 'plugin-in-root'; - " + @plugin 'plugin-in-root';" `) }) }) diff --git a/packages/@tailwindcss-postcss/tsconfig.json b/packages/@tailwindcss-postcss/tsconfig.json index d2131cbeb..6ae022f65 100644 --- a/packages/@tailwindcss-postcss/tsconfig.json +++ b/packages/@tailwindcss-postcss/tsconfig.json @@ -1,6 +1,3 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { - "verbatimModuleSyntax": false, - }, } diff --git a/packages/@tailwindcss-standalone/package.json b/packages/@tailwindcss-standalone/package.json index 24f8c93c7..4260c6708 100644 --- a/packages/@tailwindcss-standalone/package.json +++ b/packages/@tailwindcss-standalone/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/standalone", - "version": "4.3.3", + "version": "4.1.16", "private": true, "description": "Standalone CLI for Tailwind CSS", "license": "MIT", @@ -27,23 +27,23 @@ "dependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/cli": "workspace:*", - "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/typography": "^0.5.20", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.19", "detect-libc": "1.0.3", - "enhanced-resolve": "catalog:", + "enhanced-resolve": "^5.18.3", "tailwindcss": "workspace:*" }, "__notes": "These binary packages must be included so Bun can build the CLI for all supported platforms. We also rely on Lightning CSS and Parcel being patched so Bun can statically analyze the executables.", "devDependencies": { - "@parcel/watcher-darwin-arm64": "catalog:", - "@parcel/watcher-darwin-x64": "catalog:", - "@parcel/watcher-linux-arm64-glibc": "catalog:", - "@parcel/watcher-linux-arm64-musl": "catalog:", - "@parcel/watcher-linux-x64-glibc": "catalog:", - "@parcel/watcher-linux-x64-musl": "catalog:", - "@parcel/watcher-win32-x64": "catalog:", - "@types/bun": "^1.3.14", - "bun": "^1.3.14", + "@parcel/watcher-darwin-arm64": "^2.5.1", + "@parcel/watcher-darwin-x64": "^2.5.1", + "@parcel/watcher-linux-arm64-glibc": "^2.5.1", + "@parcel/watcher-linux-arm64-musl": "^2.5.1", + "@parcel/watcher-linux-x64-glibc": "^2.5.1", + "@parcel/watcher-linux-x64-musl": "^2.5.1", + "@parcel/watcher-win32-x64": "^2.5.1", + "@types/bun": "^1.3.0", + "bun": "^1.3.0", "lightningcss-darwin-arm64": "catalog:", "lightningcss-darwin-x64": "catalog:", "lightningcss-linux-arm64-gnu": "catalog:", diff --git a/packages/@tailwindcss-standalone/scripts/build.ts b/packages/@tailwindcss-standalone/scripts/build.ts index 0aee26435..f37bdfd20 100644 --- a/packages/@tailwindcss-standalone/scripts/build.ts +++ b/packages/@tailwindcss-standalone/scripts/build.ts @@ -1,3 +1,4 @@ +import { $ } from 'bun' import { createHash } from 'node:crypto' import { mkdir, readFile, writeFile } from 'node:fs/promises' import * as path from 'node:path' @@ -5,107 +6,76 @@ import { fileURLToPath } from 'node:url' const __dirname = fileURLToPath(new URL('.', import.meta.url)) -// Workaround for Bun binary downloads failing on Windows CI when -// USERPROFILE is passed through by Turborepo. -// -// Unfortunately, setting this at runtime doesn't appear to work so we have to -// spawn a new process without the env var. -if (process.env.NESTED_BUILD !== '1' && process.env.USERPROFILE && process.env.USERPROFILE !== '') { - let result = await Bun.$`bun ${fileURLToPath(import.meta.url)}`.env({ - USERPROFILE: '', - NESTED_BUILD: '1', - }) +async function buildForPlatform(triple: string, outfile: string) { + // We wrap this in a retry because occasionally the atomic rename fails for some reason + for (let i = 0; i < 5; ++i) { + try { + let cmd = $`bun build --compile --target=${triple} ./src/index.ts --outfile=${outfile} --env inline` - process.exit(result.exitCode) + // This env var is used by our patched versions of Lightning CSS and Parcel Watcher to + // statically bundle the proper binaries for musl vs glibc + cmd = cmd.env({ + PLATFORM_LIBC: triple.includes('-musl') ? 'musl' : 'glibc', + }) + + return await cmd + } catch (err) { + if (i < 5) continue + + throw new Error(`Failed to build for platform ${triple}`, { cause: err }) + } + } } -// We use baseline builds for all x64 platforms to ensure compatibility with -// older hardware. -let builds: { target: Bun.Build.Target; name: string }[] = [ - { name: 'tailwindcss-linux-arm64', target: 'bun-linux-arm64' }, - { name: 'tailwindcss-linux-arm64-musl', target: 'bun-linux-arm64-musl' }, - // @ts-expect-error: Either the types are wrong or the runtime needs to be updated - // to accept a `-glibc` at the end like the types suggest. - { name: 'tailwindcss-linux-x64', target: 'bun-linux-x64-baseline' }, - { name: 'tailwindcss-linux-x64-musl', target: 'bun-linux-x64-baseline-musl' }, - { name: 'tailwindcss-macos-arm64', target: 'bun-darwin-arm64' }, - { name: 'tailwindcss-macos-x64', target: 'bun-darwin-x64-baseline' }, - { name: 'tailwindcss-windows-x64.exe', target: 'bun-windows-x64-baseline' }, -] +async function build(triple: string, file: string) { + let start = process.hrtime.bigint() -let summary: { target: Bun.Build.Target; name: string; sum: string }[] = [] + let outfile = path.resolve(__dirname, `../dist/${file}`) -// Build platform binaries and checksum them. -let start = process.hrtime.bigint() -for (let { target, name } of builds) { - let outfile = path.resolve(__dirname, `../dist/${name}`) + await buildForPlatform(triple, outfile) - let result = await Bun.build({ - entrypoints: ['./src/index.ts'], - target: 'node', - minify: { - whitespace: false, - syntax: true, - identifiers: false, - keepNames: true, - }, - - define: { - // This ensures only necessary binaries are bundled for linux targets - // It reduces binary size since no runtime selection is necessary - 'process.env.PLATFORM_LIBC': JSON.stringify(target.includes('-musl') ? 'musl' : 'glibc'), - - // This prevents the WASI build from being bundled with the binary - 'process.env.NAPI_RS_FORCE_WASI': JSON.stringify(''), - - // This simplifies the Oxide loading code a small amount - 'process.env.NAPI_RS_NATIVE_LIBRARY_PATH': JSON.stringify(''), - }, - - compile: { - target, - outfile, - - // Disable .env loading - autoloadDotenv: false, - - // Disable bunfig.toml loading - autoloadBunfig: false, - }, - - plugins: [ - { - name: 'tailwindcss-plugin', - setup(build) { - build.onLoad({ filter: /tailwindcss-oxide\.wasi\.cjs$/ }, async (args) => { - return { contents: '' } - }) - }, - }, - ], - }) - - let entry = result.outputs.find((output) => output.kind === 'entry-point') - if (!entry) throw new Error(`Build failed for ${target}`) + await new Promise((resolve) => setTimeout(resolve, 100)) let content = await readFile(outfile) + let sum = createHash('sha256').update(content).digest('hex') - summary.push({ - target, - name, - sum: createHash('sha256').update(content).digest('hex'), - }) + let elapsed = process.hrtime.bigint() - start + + return { + triple, + file, + sum, + elapsed, + } } await mkdir(path.resolve(__dirname, '../dist'), { recursive: true }) +// Build platform binaries and checksum them. We use baseline builds for all x64 platforms to ensure +// compatibility with older hardware. +let results = await Promise.all([ + build('bun-linux-arm64', './tailwindcss-linux-arm64'), + build('bun-linux-arm64-musl', './tailwindcss-linux-arm64-musl'), + + build('bun-linux-x64-baseline', './tailwindcss-linux-x64'), + build('bun-linux-x64-musl-baseline', './tailwindcss-linux-x64-musl'), + + build('bun-darwin-arm64', './tailwindcss-macos-arm64'), + build('bun-darwin-x64-baseline', './tailwindcss-macos-x64'), + + build('bun-windows-x64-baseline', './tailwindcss-windows-x64.exe'), +]) + // Write the checksums to a file let sumsFile = path.resolve(__dirname, '../dist/sha256sums.txt') -let sums = summary.map(({ name, sum }) => `${sum} ./${name}`) +let sums = results.map(({ file, sum }) => `${sum} ${file}`) + +console.table( + results.map(({ triple, sum, elapsed }) => ({ + triple, + sum, + elapsed: `${(Number(elapsed) / 1e6).toFixed(0)}ms`, + })), +) await writeFile(sumsFile, sums.join('\n') + '\n') - -console.table(summary.map(({ target, sum }) => ({ target, sum }))) - -let elapsed = process.hrtime.bigint() - start -console.log(`Build completed in ${(Number(elapsed) / 1e6).toFixed(0)}ms`) diff --git a/packages/@tailwindcss-standalone/src/index.ts b/packages/@tailwindcss-standalone/src/index.ts index 23a928f8b..be85729bb 100644 --- a/packages/@tailwindcss-standalone/src/index.ts +++ b/packages/@tailwindcss-standalone/src/index.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs' import { createRequire } from 'node:module' import packageJson from 'tailwindcss/package.json' @@ -60,6 +61,15 @@ globalThis.__tw_load = async (id) => { } } globalThis.__tw_version = packageJson.version +globalThis.__tw_readFile = async (path, encoding) => { + // When reading a file from the `$bunfs`, we need to use the synchronous + // `readFileSync` API + let isEmbeddedFileBase = path.includes('/$bunfs/root') || path.includes(':/~BUN/root') + if (!isEmbeddedFileBase) { + return + } + return fs.readFileSync(path, encoding) +} // We use a plugin to make sure that the JS APIs are bundled with the standalone // CLI and can be imported inside configs and plugins @@ -75,10 +85,12 @@ Bun.plugin({ 'tailwindcss/plugin': await import('tailwindcss/plugin'), 'tailwindcss/plugin.js': await import('tailwindcss/plugin'), 'tailwindcss/package.json': await import('tailwindcss/package.json'), - 'tailwindcss/lib/util/flattenColorPalette': - await import('tailwindcss/lib/util/flattenColorPalette'), - 'tailwindcss/lib/util/flattenColorPalette.js': - await import('tailwindcss/lib/util/flattenColorPalette'), + 'tailwindcss/lib/util/flattenColorPalette': await import( + 'tailwindcss/lib/util/flattenColorPalette' + ), + 'tailwindcss/lib/util/flattenColorPalette.js': await import( + 'tailwindcss/lib/util/flattenColorPalette' + ), 'tailwindcss/defaultTheme': await import('tailwindcss/defaultTheme'), 'tailwindcss/defaultTheme.js': await import('tailwindcss/defaultTheme'), } diff --git a/packages/@tailwindcss-standalone/src/types.d.ts b/packages/@tailwindcss-standalone/src/types.d.ts index 0b5deb6ea..3212ac5b6 100644 --- a/packages/@tailwindcss-standalone/src/types.d.ts +++ b/packages/@tailwindcss-standalone/src/types.d.ts @@ -6,5 +6,6 @@ declare module '*.css' { declare var __tw_version: string | undefined declare var __tw_resolve: undefined | ((id: string, base?: string) => string | false) declare var __tw_readFile: - undefined | ((path: string, encoding: BufferEncoding) => Promise) + | undefined + | ((path: string, encoding: BufferEncoding) => Promise) declare var __tw_load: undefined | ((path: string) => Promise) diff --git a/packages/@tailwindcss-turbopack/README.md b/packages/@tailwindcss-turbopack/README.md deleted file mode 100644 index ee54b4d03..000000000 --- a/packages/@tailwindcss-turbopack/README.md +++ /dev/null @@ -1,99 +0,0 @@ -

- - - - - Tailwind CSS - - -

- -

- A utility-first CSS framework for rapidly building custom user interfaces. -

- -

- Build Status - Total Downloads - Latest Release - License -

- ---- - -## Documentation - -For full documentation, visit [tailwindcss.com](https://tailwindcss.com). - -## Community - -For help, discussion about best practices, or feature ideas: - -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) - -## Contributing - -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. - ---- - -## @tailwindcss/turbopack - -A Turbopack loader for Tailwind CSS v4. - -## Installation - -```sh -npm install @tailwindcss/turbopack -``` - -### Usage - -```javascript -// next.config.js -module.exports = { - turbopack: { - rules: { - '*.css': { - loaders: ['@tailwindcss/turbopack'], - as: '*.css', - }, - }, - }, -} -``` - -Then create a CSS file that imports Tailwind: - -```css -/* src/index.css */ -@import 'tailwindcss'; -``` - -### Options - -#### `base` - -The base directory to scan for class candidates. Defaults to the current working directory. - -```javascript -{ - loader: '@tailwindcss/turbopack', - options: { - base: process.cwd(), - }, -} -``` - -#### `optimize` - -Whether to optimize and minify the output CSS. Defaults to `true` in production mode. - -```javascript -{ - loader: '@tailwindcss/turbopack', - options: { - optimize: true, // or { minify: true } - }, -} -``` diff --git a/packages/@tailwindcss-turbopack/package.json b/packages/@tailwindcss-turbopack/package.json deleted file mode 100644 index abdbbe90e..000000000 --- a/packages/@tailwindcss-turbopack/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@tailwindcss/turbopack", - "version": "4.3.3", - "description": "A Turbopack loader for Tailwind CSS v4.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/tailwindlabs/tailwindcss.git", - "directory": "packages/@tailwindcss-turbopack" - }, - "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", - "homepage": "https://tailwindcss.com", - "scripts": { - "build": "tsup-node", - "dev": "pnpm run build -- --watch" - }, - "files": [ - "dist/" - ], - "publishConfig": { - "provenance": true, - "access": "public" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "workspace:*", - "@tailwindcss/oxide": "workspace:*", - "tailwindcss": "workspace:*" - }, - "devDependencies": { - "@types/node": "catalog:", - "webpack": "catalog:" - } -} diff --git a/packages/@tailwindcss-turbopack/src/index.cts b/packages/@tailwindcss-turbopack/src/index.cts deleted file mode 100644 index dbdded748..000000000 --- a/packages/@tailwindcss-turbopack/src/index.cts +++ /dev/null @@ -1,5 +0,0 @@ -import tailwindLoader from './index.ts' - -// CommonJS export for webpack loaders - must be the function directly -// @ts-ignore -export = tailwindLoader diff --git a/packages/@tailwindcss-turbopack/src/index.ts b/packages/@tailwindcss-turbopack/src/index.ts deleted file mode 100644 index a70f28871..000000000 --- a/packages/@tailwindcss-turbopack/src/index.ts +++ /dev/null @@ -1,287 +0,0 @@ -import QuickLRU from '@alloc/quick-lru' -import { - compile, - env, - Features, - Instrumentation, - normalizePath, - optimize, - Polyfills, -} from '@tailwindcss/node' -import { clearRequireCache } from '@tailwindcss/node/require-cache' -import { Scanner } from '@tailwindcss/oxide' -import fs from 'node:fs' -import path from 'node:path' -import type { LoaderContext } from 'webpack' - -const DEBUG = env.DEBUG - -export interface LoaderOptions { - /** - * The base directory to scan for class candidates. - * - * Defaults to the current working directory. - */ - base?: string - - /** - * Optimize and minify the output CSS. - */ - optimize?: boolean | { minify?: boolean } -} - -interface CacheEntry { - mtimes: Map - compiler: null | Awaited> - scanner: null | Scanner - candidates: Set - fullRebuildPaths: string[] -} - -const cache = new QuickLRU({ maxSize: 50 }) - -function getCacheKey(resourceId: string, opts: LoaderOptions): string { - return `${resourceId}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}` -} - -function getContextFromCache(resourceId: string, opts: LoaderOptions): CacheEntry { - let key = getCacheKey(resourceId, opts) - if (cache.has(key)) return cache.get(key)! - let entry: CacheEntry = { - mtimes: new Map(), - compiler: null, - scanner: null, - candidates: new Set(), - fullRebuildPaths: [], - } - cache.set(key, entry) - return entry -} - -export default async function tailwindLoader( - this: LoaderContext, - source: string, -): Promise { - let callback = this.async() - let options = this.getOptions() ?? {} - let inputFile = this.resourcePath - let resourceId = this.resource - let base = options.base ?? process.cwd() - let shouldOptimize = options.optimize ?? process.env.NODE_ENV === 'production' - let isCSSModuleFile = inputFile.endsWith('.module.css') - - using I = new Instrumentation() - - DEBUG && I.start(`[@tailwindcss/turbopack] ${path.relative(base, inputFile)}`) - - // Bail out early if this is guaranteed to be a non-Tailwind CSS file. - { - DEBUG && I.start('Quick bail check') - let canBail = !/@(import|reference|theme|variant|config|plugin|apply|tailwind)\b/.test(source) - if (canBail) { - DEBUG && I.end('Quick bail check') - DEBUG && I.end(`[@tailwindcss/turbopack] ${path.relative(base, inputFile)}`) - callback(null, source) - return - } - DEBUG && I.end('Quick bail check') - } - - try { - let context = getContextFromCache(resourceId, options) - let inputBasePath = path.dirname(path.resolve(inputFile)) - - // Whether this is the first build or not - let isInitialBuild = context.compiler === null - - async function createCompiler() { - DEBUG && I.start('Setup compiler') - if (context.fullRebuildPaths.length > 0 && !isInitialBuild) { - clearRequireCache(context.fullRebuildPaths) - } - - context.fullRebuildPaths = [] - - DEBUG && I.start('Create compiler') - let compiler = await compile(source, { - from: inputFile, - base: inputBasePath, - shouldRewriteUrls: true, - onDependency: (depPath) => context.fullRebuildPaths.push(depPath), - // In CSS Module files, we have to disable the `@property` polyfill since these will - // emit global `*` rules which are considered to be non-pure and will cause builds - // to fail. - polyfills: isCSSModuleFile ? Polyfills.All ^ Polyfills.AtProperty : Polyfills.All, - }) - DEBUG && I.end('Create compiler') - - DEBUG && I.end('Setup compiler') - return compiler - } - - // Setup the compiler if it doesn't exist yet - context.compiler ??= await createCompiler() - - // Early exit if no Tailwind features are used - if (context.compiler.features === Features.None) { - DEBUG && I.end(`[@tailwindcss/turbopack] ${path.relative(base, inputFile)}`) - callback(null, source) - return - } - - let rebuildStrategy: 'full' | 'incremental' = 'incremental' - - // Track file modification times to CSS files - DEBUG && I.start('Register full rebuild paths') - { - // Report dependencies for config files, plugins, etc. - for (let file of context.fullRebuildPaths) { - this.addDependency(path.resolve(file)) - } - - let files = [...context.fullRebuildPaths, inputFile] - - for (let file of files) { - let changedTime: number | null = null - try { - changedTime = fs.statSync(file)?.mtimeMs ?? null - } catch { - // File might not exist - } - - if (changedTime === null) { - if (file === inputFile) { - rebuildStrategy = 'full' - } - continue - } - - let prevTime = context.mtimes.get(file) - if (prevTime === changedTime) continue - - rebuildStrategy = 'full' - context.mtimes.set(file, changedTime) - } - } - DEBUG && I.end('Register full rebuild paths') - - if (rebuildStrategy === 'full' && !isInitialBuild) { - context.compiler = await createCompiler() - } - - let compiler = context.compiler - - // Check if we need to process this file at all - if ( - !( - compiler.features & - (Features.AtApply | Features.JsPluginCompat | Features.ThemeFunction | Features.Utilities) - ) - ) { - DEBUG && I.end(`[@tailwindcss/turbopack] ${path.relative(base, inputFile)}`) - callback(null, source) - return - } - - // Setup or update scanner if needed - if (context.scanner === null || rebuildStrategy === 'full') { - DEBUG && I.start('Setup scanner') - let sources = (() => { - // Disable auto source detection - if (compiler.root === 'none') { - return [] - } - - // No root specified, use the base directory - if (compiler.root === null) { - return [{ base, pattern: '**/*', negated: false }] - } - - // Use the specified root - return [{ ...compiler.root, negated: false }] - })().concat(compiler.sources) - - context.scanner = new Scanner({ sources }) - DEBUG && I.end('Setup scanner') - } - - // Scan for candidates if utilities are used - if (compiler.features & Features.Utilities) { - DEBUG && I.start('Scan for candidates') - for (let candidate of context.scanner.scan()) { - context.candidates.add(candidate) - } - DEBUG && I.end('Scan for candidates') - - DEBUG && I.start('Register dependency messages') - // Add all found files as direct dependencies - let resolvedInputFile = path.resolve(base, inputFile) - for (let file of context.scanner.files) { - let absolutePath = path.resolve(file) - // The CSS file cannot be a dependency of itself - if (absolutePath === resolvedInputFile) { - continue - } - this.addDependency(absolutePath) - } - - // Register context dependencies for glob patterns - for (let glob of context.scanner.globs) { - // Skip negated patterns - if (glob.pattern[0] === '!') continue - - // Avoid adding a dependency on the base directory itself - if (glob.pattern === '*' && base === glob.base) { - continue - } - - this.addContextDependency(path.resolve(glob.base)) - } - - // Validate that source(...) paths are directories - let root = compiler.root - if (root !== 'none' && root !== null) { - let basePath = normalizePath(path.resolve(root.base, root.pattern)) - try { - let stats = fs.statSync(basePath) - if (!stats.isDirectory()) { - throw new Error( - `The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`, - ) - } - } catch (err) { - if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { - throw err - } - // Directory doesn't exist yet, which is fine - } - } - DEBUG && I.end('Register dependency messages') - } - - DEBUG && I.start('Build utilities') - let css = compiler.build([...context.candidates]) - DEBUG && I.end('Build utilities') - - // Optionally optimize the output - let result = css - if (shouldOptimize) { - DEBUG && I.start('Optimization') - let optimized = optimize(css, { - minify: typeof shouldOptimize === 'object' ? shouldOptimize.minify : true, - }) - result = optimized.code - DEBUG && I.end('Optimization') - } - - DEBUG && I.end(`[@tailwindcss/turbopack] ${path.relative(base, inputFile)}`) - callback(null, result) - } catch (error) { - // Clear the cache entry on error to force a full rebuild next time - let key = getCacheKey(resourceId, options) - cache.delete(key) - - DEBUG && I.end(`[@tailwindcss/turbopack] ${path.relative(base, inputFile)}`) - callback(error as Error) - } -} diff --git a/packages/@tailwindcss-turbopack/tsconfig.json b/packages/@tailwindcss-turbopack/tsconfig.json deleted file mode 100644 index d2131cbeb..000000000 --- a/packages/@tailwindcss-turbopack/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "compilerOptions": { - "verbatimModuleSyntax": false, - }, -} diff --git a/packages/@tailwindcss-turbopack/tsup.config.ts b/packages/@tailwindcss-turbopack/tsup.config.ts deleted file mode 100644 index 998eebe4f..000000000 --- a/packages/@tailwindcss-turbopack/tsup.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from 'tsup' - -export default defineConfig([ - { - format: ['esm'], - clean: true, - minify: true, - cjsInterop: true, - dts: true, - entry: ['src/index.ts'], - }, - { - format: ['cjs'], - minify: true, - cjsInterop: true, - dts: true, - entry: ['src/index.cts'], - }, -]) diff --git a/packages/@tailwindcss-upgrade/README.md b/packages/@tailwindcss-upgrade/README.md index 5f532607d..7d21bd883 100644 --- a/packages/@tailwindcss-upgrade/README.md +++ b/packages/@tailwindcss-upgrade/README.md @@ -13,10 +13,10 @@

- Build Status + Build Status Total Downloads - Latest Release - License + Latest Release + License

--- @@ -29,8 +29,8 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json index e960d7677..e9ef280a1 100644 --- a/packages/@tailwindcss-upgrade/package.json +++ b/packages/@tailwindcss-upgrade/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/upgrade", - "version": "4.3.3", + "version": "4.1.16", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -29,24 +29,26 @@ "dependencies": { "@tailwindcss/node": "workspace:*", "@tailwindcss/oxide": "workspace:*", - "dedent": "catalog:", - "enhanced-resolve": "catalog:", - "globby": "^16.2.2", - "jiti": "^2.7.0", + "braces": "^3.0.3", + "dedent": "1.7.0", + "enhanced-resolve": "^5.18.3", + "globby": "^15.0.0", + "jiti": "^2.0.0-beta.3", "mri": "^1.2.0", "picocolors": "^1.1.1", - "postcss": "catalog:", + "postcss": "^8.4.41", "postcss-import": "^16.1.1", - "postcss-selector-parser": "^7.1.4", + "postcss-selector-parser": "^7.1.0", "prettier": "catalog:", - "semver": "^7.8.5", + "semver": "^7.7.3", "tailwindcss": "workspace:*", - "tree-sitter": "^0.25.1", + "tree-sitter": "^0.22.4", "tree-sitter-typescript": "^0.23.2" }, "devDependencies": { + "@types/braces": "^3.0.5", "@types/node": "catalog:", "@types/postcss-import": "^14.0.3", - "@types/semver": "^7.8.0" + "@types/semver": "^7.7.1" } } diff --git a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts index 6a451333e..24afe56b8 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts @@ -237,21 +237,16 @@ async function migrateTheme( prevSectionKey = sectionKey } - let property = keyPathToCssProperty(key) - - if (property !== null) { - if ( - !property.startsWith('default-') && - resetNamespaces.has(key[0]) && - resetNamespaces.get(key[0]) === false - ) { - resetNamespaces.set(key[0], true) - let ns = keyPathToCssProperty([key[0]]) - if (ns !== null) { - themeSection.push(` ${escape(`--${ns}`)}-*: initial;`) - } + if (resetNamespaces.has(key[0]) && resetNamespaces.get(key[0]) === false) { + resetNamespaces.set(key[0], true) + let property = keyPathToCssProperty([key[0]]) + if (property !== null) { + themeSection.push(` ${escape(`--${property}`)}-*: initial;`) } + } + let property = keyPathToCssProperty(key) + if (property !== null) { themeSection.push(` ${escape(`--${property}`)}: ${value};`) } } @@ -416,8 +411,6 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { 'presets', 'prefix', // Prefix is handled in the dedicated prefix migrator 'corePlugins', - 'future', - 'experimental', ] if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) { @@ -432,29 +425,6 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return false } - // If there are unknown "future" flags we should bail - if (unresolvedConfig.future && unresolvedConfig.future !== 'all') { - let knownFlags = [ - 'hoverOnlyWhenSupported', - 'respectDefaultRingColorOpacity', - 'disableColorOpacityUtilitiesByDefault', - 'relativeContentPathsByDefault', - ] - - if (Object.keys(unresolvedConfig.future).some((key) => !knownFlags.includes(key))) { - return false - } - } - - // If there are unknown "experimental" flags we should bail - if (unresolvedConfig.experimental && unresolvedConfig.experimental !== 'all') { - let knownFlags = ['generalizedModifiers'] - - if (Object.keys(unresolvedConfig.experimental).some((key) => !knownFlags.includes(key))) { - return false - } - } - // Only migrate the config file if all top-level theme keys are allowed to be // migrated if (theme && typeof theme === 'object') { diff --git a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-postcss.ts b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-postcss.ts index e16f52287..b44d1ea61 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-postcss.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-postcss.ts @@ -2,7 +2,6 @@ import fs from 'node:fs/promises' import path from 'node:path' import { pkg } from '../../utils/packages' import { highlight, info, relative, success, warn } from '../../utils/renderer' -import { writeFileSafely } from '../../utils/write-file-safely' // Migrates simple PostCSS setups. This is to cover non-dynamic config files // similar to the ones we have all over our docs: @@ -51,7 +50,7 @@ export async function migratePostCSSConfig(base: string) { ranMigration = true if (result) { - await writeFileSafely( + await fs.writeFile( packageJsonPath, JSON.stringify({ ...packageJson, postcss: result?.json }, null, 2), ) @@ -77,7 +76,7 @@ export async function migratePostCSSConfig(base: string) { ranMigration = true if (result) { - await writeFileSafely(jsonConfigPath, JSON.stringify(result.json, null, 2)) + await fs.writeFile(jsonConfigPath, JSON.stringify(result.json, null, 2)) didMigrate = true didAddPostcssClient = result.didAddPostcssClient @@ -137,16 +136,16 @@ async function migratePostCSSJSConfig(configPath: string): Promise<{ didRemovePostCSSImport: boolean } | null> { function isTailwindCSSPlugin(line: string) { - return /['"]?tailwindcss['"]?: ?\{\}/.test(line) + return /['"]?tailwindcss['"]?\: ?\{\}/.test(line) } function isPostCSSImportPlugin(line: string) { - return /['"]?postcss-import['"]?: ?\{\}/.test(line) + return /['"]?postcss-import['"]?\: ?\{\}/.test(line) } function isAutoprefixerPlugin(line: string) { - return /['"]?autoprefixer['"]?: ?\{\}/.test(line) + return /['"]?autoprefixer['"]?\: ?\{\}/.test(line) } function isTailwindCSSNestingPlugin(line: string) { - return /['"]tailwindcss\/nesting['"]: ?(\{\}|['"]postcss-nesting['"])/.test(line) + return /['"]tailwindcss\/nesting['"]\: ?(\{\}|['"]postcss-nesting['"])/.test(line) } info('Migrating PostCSS configuration…') @@ -207,7 +206,7 @@ async function migratePostCSSJSConfig(configPath: string): Promise<{ newLines.push(line) } } - await writeFileSafely(configPath, newLines.join('\n')) + await fs.writeFile(configPath, newLines.join('\n')) return { didAddPostcssClient, didRemoveAutoprefixer, didRemovePostCSSImport } } diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts b/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts index c7058df54..c9af6a7a3 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts @@ -4,14 +4,11 @@ import postcss, { type Result } from 'postcss' import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' import { segment } from '../../../../tailwindcss/src/utils/segment' import { Stylesheet, type StylesheetConnection } from '../../stylesheet' -import { gitRoot } from '../../utils/git' import { error, highlight, relative } from '../../utils/renderer' import { resolveCssId } from '../../utils/resolve' -export async function analyze(stylesheets: Stylesheet[], { base }: { base: string }) { - // Use the project's git root, instead of the `base` to ensure all - // `.gitignore` rules are being used. - let isIgnored = await isGitIgnored({ cwd: gitRoot(base) ?? base }) +export async function analyze(stylesheets: Stylesheet[]) { + let isIgnored = await isGitIgnored() let processingQueue: (() => Promise)[] = [] let stylesheetsByFile = new DefaultMap((file) => { // We don't want to process ignored files (like node_modules) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts index 9f6a92008..d0b722c9c 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts @@ -13,11 +13,6 @@ async function migrate(input: string, config: Config = {}) { let designSystem = await __unstable__loadDesignSystem( css` @import 'tailwindcss'; - - /* TODO(perf): Only here to speed up the tests */ - @theme { - --*: initial; - } `, { base: __dirname }, ) @@ -61,6 +56,21 @@ it('should append `!` to each utility, when using `!important`', async () => { `) }) +// TODO: Handle SCSS syntax +it.skip('should append `!` to each utility, when using `#{!important}`', async () => { + expect( + await migrate(css` + .foo { + @apply flex flex-col #{!important}; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex! flex-col!; + }" + `) +}) + it('should move the legacy `!` prefix, to the new `!` postfix notation', async () => { expect( await migrate(css` @@ -75,44 +85,35 @@ it('should move the legacy `!` prefix, to the new `!` postfix notation', async ( `) }) -it( - 'should apply all candidate migration when migrating with a config', - { timeout: 10_000 }, - async () => { - async function migrateWithPrefix(input: string) { - return postcss() - .use( - migrateAtApply({ - designSystem: await __unstable__loadDesignSystem( - css` - @import 'tailwindcss' prefix(tw); +it('should apply all candidate migration when migrating with a config', async () => { + async function migrateWithPrefix(input: string) { + return postcss() + .use( + migrateAtApply({ + designSystem: await __unstable__loadDesignSystem( + css` + @import 'tailwindcss' prefix(tw); + `, + { base: __dirname }, + ), + userConfig: { + prefix: 'tw_', + }, + }), + ) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) + } - /* TODO(perf): Only here to speed up the tests */ - @theme { - --*: initial; - } - `, - { base: __dirname }, - ), - userConfig: { - prefix: 'tw_', - }, - }), - ) - .process(input, { from: expect.getState().testPath }) - .then((result) => result.css) - } - - expect( - await migrateWithPrefix(css` - .foo { - @apply !tw_flex [color:--my-color] tw_bg-gradient-to-t; - } - `), - ).toMatchInlineSnapshot(` + expect( + await migrateWithPrefix(css` + .foo { + @apply !tw_flex [color:--my-color] tw_bg-gradient-to-t; + } + `), + ).toMatchInlineSnapshot(` ".foo { @apply tw:flex! tw:text-(--my-color) tw:bg-linear-to-t; }" `) - }, -) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts index e115951d3..ba1d06947 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts @@ -39,7 +39,7 @@ async function migrate( .use(sortBuckets()) .use(formatNodes()) .process(stylesheet.root!, { from: expect.getState().testPath }) - .then((result) => `\n${result.css.trim()}\n`) + .then((result) => result.css) } it('should migrate simple `@layer utilities` to `@utility`', async () => { @@ -52,11 +52,9 @@ it('should migrate simple `@layer utilities` to `@utility`', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; - } - " + }" `) }) @@ -71,15 +69,13 @@ it('should split multiple selectors in separate utilities', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; } @utility bar { color: red; - } - " + }" `) }) @@ -103,16 +99,14 @@ it('should merge `@utility` with the same name', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; font-weight: bold; } .bar { color: blue; - } - " + }" `) }) @@ -155,8 +149,7 @@ it('should leave non-class utilities alone', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { /* 2. */ /* 2.1. */ color: red; @@ -189,58 +182,7 @@ it('should leave non-class utilities alone', async () => { font-weight: bold; } } - } - " - `) -}) - -it('should not convert empty classes into `@utility` since that would make them invalid', async () => { - expect( - await migrate(css` - @layer utilities { - .good { - color: red; - } - - .bad-empty { - } - - /* Comment above */ - .bad-with-comment-above { - } - - .bad-with-comment-inside { - /* Comment inside */ - } - - .bad-with-comment-below { - } - /* Comment below */ - } - `), - ).toMatchInlineSnapshot(` - " - @utility good { - color: red; - } - - @layer utilities { - .bad-empty { - } - - /* Comment above */ - .bad-with-comment-above { - } - - .bad-with-comment-inside { - /* Comment inside */ - } - - .bad-with-comment-below { - } - /* Comment below */ - } - " + }" `) }) @@ -262,8 +204,7 @@ it('should migrate simple `@layer utilities` with nesting to `@utility`', async } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; &:hover { @@ -273,8 +214,7 @@ it('should migrate simple `@layer utilities` with nesting to `@utility`', async &:focus { color: green; } - } - " + }" `) }) @@ -292,15 +232,13 @@ it('should migrate multiple simple `@layer utilities` to `@utility`', async () = } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; } @utility bar { color: blue; - } - " + }" `) }) @@ -322,8 +260,7 @@ it('should not migrate Rules inside of Rules to a `@utility`', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; } @@ -333,8 +270,7 @@ it('should not migrate Rules inside of Rules to a `@utility`', async () => { .baz { color: green; } - } - " + }" `) }) @@ -350,13 +286,11 @@ it('should invert at-rules to make them migrate-able', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { @media (min-width: 640px) { color: red; } - } - " + }" `) }) @@ -380,8 +314,7 @@ it('should migrate at-rules with multiple utilities and invert them', async () = } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { @media (min-width: 640px) { color: red; } @@ -391,8 +324,7 @@ it('should migrate at-rules with multiple utilities and invert them', async () = @media (min-width: 640px) { color: blue; } - } - " + }" `) }) @@ -424,8 +356,7 @@ it('should migrate deeply nested at-rules with multiple utilities and invert the } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { @media (min-width: 640px) { color: red; } @@ -453,8 +384,7 @@ it('should migrate deeply nested at-rules with multiple utilities and invert the } } } - } - " + }" `) }) @@ -468,13 +398,11 @@ it('should migrate classes with pseudo elements', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { &::-webkit-scrollbar { display: none; } - } - " + }" `) }) @@ -488,13 +416,11 @@ it('should migrate classes with attribute selectors', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { &[data-checked=''] { display: none; } - } - " + }" `) }) @@ -508,13 +434,11 @@ it('should migrate classes with element selectors', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { & main { display: none; } - } - " + }" `) }) @@ -528,13 +452,11 @@ it('should migrate classes attached to an element selector', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { &main { display: none; } - } - " + }" `) }) @@ -548,13 +470,11 @@ it('should migrate classes with id selectors', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { &#main { display: none; } - } - " + }" `) }) @@ -568,8 +488,7 @@ it('should migrate classes with another attached class', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { &.main { display: none; } @@ -579,8 +498,7 @@ it('should migrate classes with another attached class', async () => { &.no-scrollbar { display: none; } - } - " + }" `) }) @@ -594,8 +512,7 @@ it('should migrate a selector with multiple classes to multiple @utility definit } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { & .bar:hover .baz:focus { display: none; } @@ -611,8 +528,7 @@ it('should migrate a selector with multiple classes to multiple @utility definit .foo .bar:hover &:focus { display: none; } - } - " + }" `) }) @@ -632,8 +548,7 @@ it('should merge `@utility` definitions with the same name', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility step { + "@utility step { counter-increment: step; &:before { @@ -641,8 +556,7 @@ it('should merge `@utility` definitions with the same name', async () => { @apply ml-[-41px]; content: counter(step); } - } - " + }" `) }) @@ -656,8 +570,7 @@ it('should not migrate nested classes inside a `:not(…)`', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { & .bar:not(.qux):has(.baz) { display: none; } @@ -673,8 +586,7 @@ it('should not migrate nested classes inside a `:not(…)`', async () => { .foo .bar:not(.qux):has(&) { display: none; } - } - " + }" `) }) @@ -708,8 +620,7 @@ it('should migrate advanced combinations', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { @media (width >= 100px) { @supports (display: none) { & .bar:not(.qux):has(.baz) { @@ -748,8 +659,7 @@ it('should migrate advanced combinations', async () => { } } } - } - " + }" `) }) @@ -767,15 +677,12 @@ describe('comments', () => { } `), ).toMatchInlineSnapshot(` - " - /* Start of utilities: */ - + "/* Start of utilities: */ @utility foo { /* Utility #1 */ /* Declarations: */ color: red; - } - " + }" `) }) @@ -792,9 +699,7 @@ describe('comments', () => { } `), ).toMatchInlineSnapshot(` - " - /* Start of utilities: */ - + "/* Start of utilities: */ @utility foo { /* Foo & Bar */ & .bar { @@ -802,15 +707,13 @@ describe('comments', () => { color: red; } } - @utility bar { /* Foo & Bar */ .foo & { /* Declarations: */ color: red; } - } - " + }" `) }) @@ -830,9 +733,7 @@ describe('comments', () => { } `), ).toMatchInlineSnapshot(` - " - /* Start of utilities: */ - + "/* Start of utilities: */ @utility foo { /* Mobile only */ @media (width <= 640px) { @@ -840,8 +741,7 @@ describe('comments', () => { /* Declarations: */ color: red; } - } - " + }" `) }) @@ -880,9 +780,7 @@ describe('comments', () => { /* After */ `), ).toMatchInlineSnapshot(` - " - /* Tailwind Utilities: */ - + "/* Tailwind Utilities: */ @utility no-scrollbar { /* Chrome, Safari and Opera */ /* Second comment */ @@ -909,8 +807,7 @@ describe('comments', () => { .after { /* Inside */ } - /* After */ - " + /* After */" `) }) }) @@ -935,8 +832,7 @@ it('should not lose attribute selectors', async () => { } `), ).toMatchInlineSnapshot(` - " - @layer components { + "@layer components { #TableOfContents { .toc a { @apply block max-w-full truncate py-1 pl-2 hover:font-medium hover:no-underline; @@ -949,8 +845,7 @@ it('should not lose attribute selectors', async () => { } } } - } - " + }" `) }) @@ -968,13 +863,11 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { /* Utility #1 */ /* Declarations: */ color: red; - } - " + }" `) }) @@ -991,13 +884,11 @@ describe('layered stylesheets', () => { layers: ['components'], }), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { /* Utility #1 */ /* Declarations: */ color: red; - } - " + }" `) }) @@ -1014,13 +905,11 @@ describe('layered stylesheets', () => { layers: ['foo'], }), ).toMatchInlineSnapshot(` - " - /* Utility #1 */ + "/* Utility #1 */ .foo { /* Declarations: */ color: red; - } - " + }" `) }) @@ -1040,8 +929,7 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { /* Utility #1 */ /* Declarations: */ color: red; @@ -1049,8 +937,7 @@ describe('layered stylesheets', () => { #main { color: red; - } - " + }" `) }) @@ -1091,8 +978,7 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { @layer utilities { @layer utilities { /* Utility #1 */ @@ -1125,8 +1011,7 @@ describe('layered stylesheets', () => { #secondary { color: red; - } - " + }" `) }) @@ -1143,13 +1028,11 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " - @import 'thing'; + "@import 'thing'; @utility foo { color: red; - } - " + }" `) }) @@ -1166,13 +1049,11 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " - @charset "utf-8"; + "@charset "utf-8"; @utility foo { color: red; - } - " + }" `) }) }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts index 6bcb72d47..9b03e9d16 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts @@ -1,4 +1,4 @@ -import { type AtRule, type Comment, type Container, type Plugin, type Rule } from 'postcss' +import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss' import SelectorParser from 'postcss-selector-parser' import { segment } from '../../../../tailwindcss/src/utils/segment' import { Stylesheet } from '../../stylesheet' @@ -43,7 +43,6 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { walk(atRule, (node) => { if (node.type !== 'rule') return - if (isEmpty(node)) return // Find all the classes in the selector SelectorParser((selectors) => { @@ -185,18 +184,6 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { toRemove.push(node.parent?.nodes.at(i) as Comment) } } - - let commentsAfter: Comment[] = [] - for (let i = idx + 1; i < (node.parent?.nodes.length ?? 0); i++) { - if (node.parent?.nodes.at(i)?.type === 'rule') { - commentsAfter = [] - break - } - if (node.parent?.nodes.at(i)?.type === 'comment') { - commentsAfter.push(node.parent?.nodes.at(i) as Comment) - } - } - toRemove.push(...commentsAfter) } for (let node of toRemove) { node.remove() @@ -244,12 +231,8 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { } } - // Remove empty rules from `@utility` clones and empty wrapper at-rules - // from the default `@layer` clone. - if ( - (node.type === 'atrule' && node.nodes?.length === 0) || - (clone !== defaultsAtRule && node.type === 'rule' && node.nodes?.length === 0) - ) { + // Remove empty rules + if ((node.type === 'rule' || node.type === 'atrule') && node.nodes?.length === 0) { node.remove() } @@ -278,11 +261,6 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { // Remove empty clones entirely if (clone.nodes?.length === 0) { clones.splice(idx, 1) - } else if (clone === defaultsAtRule) { - let first = clone.nodes?.[0] - if (first) { - first.raws.before = first.raws.before?.replace(/\n\s*\n/g, '\n') - } } } @@ -338,17 +316,3 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { }, } } - -function isEmpty(node: Container): boolean { - // We have no body - if (node.nodes === undefined) return false - - // We have a body, but no nodes - if (node.nodes.length === 0) return true - - // We have nodes, but they are only comments which we consider empty - if (node.nodes.every((child) => child.type === 'comment')) return true - - // Not empty - return false -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts index 7cfb16c75..2061f27d4 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts @@ -4,22 +4,10 @@ import * as versions from '../../utils/version' import { migrateCandidate } from './migrate' vi.spyOn(versions, 'isMajor').mockReturnValue(true) -const css = String.raw - describe('is-safe-migration', async () => { - let designSystem = await __unstable__loadDesignSystem( - css` - @import 'tailwindcss'; - - /* TODO(perf): Only here to speed up the tests */ - @theme { - --*: initial; - --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - } - `, - { base: __dirname }, - ) + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) test.each([ [`let notBorder = !border \n`, '!border'], @@ -48,10 +36,6 @@ describe('is-safe-migration', async () => { [`
\n`, 'shadow'], [`
\n`, 'shadow'], [`
\n`, 'shadow'], - [`
\n`, 'flex-grow'], - [`
\n`, 'flex-shrink'], - [`
\n`, 'flex-shrink'], - [`
\n`, 'flex-shrink'], // Next.js Image placeholder cases [``, 'blur'], @@ -95,7 +79,7 @@ describe('is-safe-migration', async () => { 'outline', ], [ - // As the last argument, but there is technically another `"` on the same line + // As the last argument, but there is techinically another `"` on the same line `function foo({ size = "1.25rem", digit, outline }): { return "foo" }`, 'outline', ], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts index 2c09f299f..fa7ee7f4a 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts @@ -66,38 +66,6 @@ export function isSafeMigration( } } - let currentLineBeforeCandidate = '' - for (let i = location.start - 1; i >= 0; i--) { - let char = location.contents.at(i)! - if (char === '\n') { - break - } - currentLineBeforeCandidate = char + currentLineBeforeCandidate - } - let currentLineAfterCandidate = '' - for (let i = location.end; i < location.contents.length; i++) { - let char = location.contents.at(i)! - if (char === '\n') { - break - } - currentLineAfterCandidate += char - } - - // Inline `style="..."` attributes can contain CSS property names that look - // like valid utility candidates, such as `flex-grow`. - { - let ranges = inlineStyleAttributeValueRanges.get(location.contents) - - for (let i = 0; i < ranges.length; i += 2) { - let start = ranges[i] - let end = ranges[i + 1] - - if (location.start >= start && location.end <= end) { - return false - } - } - } - let [candidate] = parseCandidate(rawCandidate, designSystem) // If we can't parse the candidate, then it's not a candidate at all. However, @@ -155,6 +123,23 @@ export function isSafeMigration( } } + let currentLineBeforeCandidate = '' + for (let i = location.start - 1; i >= 0; i--) { + let char = location.contents.at(i)! + if (char === '\n') { + break + } + currentLineBeforeCandidate = char + currentLineBeforeCandidate + } + let currentLineAfterCandidate = '' + for (let i = location.end; i < location.contents.length; i++) { + let char = location.contents.at(i)! + if (char === '\n') { + break + } + currentLineAfterCandidate += char + } + // Heuristic: Require the candidate to be inside quotes let isQuoteBeforeCandidate = isMiddleOfString(currentLineBeforeCandidate) let isQuoteAfterCandidate = isMiddleOfString(currentLineAfterCandidate) @@ -233,14 +218,6 @@ const BACKSLASH = 0x5c const DOUBLE_QUOTE = 0x22 const SINGLE_QUOTE = 0x27 const BACKTICK = 0x60 -const TAB = 0x09 -const NEWLINE = 0x0a -const FORM_FEED = 0x0c -const CARRIAGE_RETURN = 0x0d -const SPACE = 0x20 -const SLASH = 0x2f -const EQUALS = 0x3d -const GREATER_THAN = 0x3e function isMiddleOfString(line: string): boolean { let currentQuote: number | null = null @@ -271,97 +248,3 @@ function isMiddleOfString(line: string): boolean { return currentQuote !== null } - -const inlineStyleAttributeValueRanges = new DefaultMap((source: string) => { - let ranges: number[] = [] - let offset = 0 - - while (true) { - let tagStart = source.indexOf('<', offset) - if (tagStart === -1) return ranges - - let tagEnd = source.indexOf('>', tagStart + 1) - if (tagEnd === -1) return ranges - - offset = tagEnd + 1 - - for (let i = tagStart + 1; i < tagEnd; i++) { - let char = source.charCodeAt(i) - - if ( - char === SPACE || - char === TAB || - char === NEWLINE || - char === CARRIAGE_RETURN || - char === FORM_FEED - ) { - continue - } - - let start = i - while (i < tagEnd) { - let char = source.charCodeAt(i) - if ( - char === SPACE || - char === TAB || - char === NEWLINE || - char === CARRIAGE_RETURN || - char === FORM_FEED || - char === EQUALS || - char === GREATER_THAN || - char === SLASH - ) { - break - } - - i++ - } - - let attribute = source.slice(start, i).toLowerCase() - if (attribute !== 'style' && attribute !== ':style') continue - - while (i < tagEnd) { - let char = source.charCodeAt(i) - if ( - char !== SPACE && - char !== TAB && - char !== NEWLINE && - char !== CARRIAGE_RETURN && - char !== FORM_FEED - ) { - break - } - - i++ - } - - if (source[i] !== '=') continue - - i++ - while (i < tagEnd) { - let char = source.charCodeAt(i) - if ( - char !== SPACE && - char !== TAB && - char !== NEWLINE && - char !== CARRIAGE_RETURN && - char !== FORM_FEED - ) { - break - } - - i++ - } - - let quote = source[i] - if (quote !== '"' && quote !== "'") continue - - let valueStart = i + 1 - let valueEnd = source.indexOf(quote, valueStart) - if (valueEnd === -1 || valueEnd > tagEnd) break - - ranges.push(valueStart, valueEnd) - i = valueEnd - } - } -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts index 258ba1b33..58efb0421 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts @@ -16,8 +16,8 @@ export function migrateArbitraryVariants( rawCandidate: string, ): string { let designSystem = prepareDesignSystemStorage(baseDesignSystem) - let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY].get(null) - let variants = designSystem.storage[PRE_COMPUTED_VARIANTS_KEY].get(null) + let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] + let variants = designSystem.storage[PRE_COMPUTED_VARIANTS_KEY] for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { // We are only interested in the variants @@ -35,11 +35,7 @@ export function migrateArbitraryVariants( if (typeof targetSignature !== 'string') continue let foundVariants = variants.get(targetSignature) - if (foundVariants.length === 0) continue - - // The variant is already in a canonical form, e.g.: `min-lg` and `lg` - // produce the same CSS, but both are valid so we keep them as-is. - if (foundVariants.includes(targetString)) continue + if (foundVariants.length !== 1) continue let foundVariant = foundVariants[0] let parsedVariant = designSystem.parseVariant(foundVariant) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts index 2b088aa2e..614b4017b 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts @@ -59,7 +59,7 @@ const THEME_KEYS = new Map([ ['backdrop-blur-sm', '--backdrop-blur-sm'], ['backdrop-blur-xs', '--backdrop-blur-xs'], - ['ring', '--default-ring-width'], + ['ring', '--ring-width'], ['ring-3', '--ring-width-3'], ]) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts index e566e7d6e..a80aa75e0 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts @@ -211,10 +211,10 @@ function substituteFunctionsInValue( fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path) if (replacement === null) return - { - let idx = ctx.index - 1 + if (ctx.parent) { + let idx = ctx.parent.nodes.indexOf(node) - 1 while (idx !== -1) { - let previous = ctx.siblings[idx] + let previous = ctx.parent.nodes[idx] // Skip the space separator if (previous.kind === 'separator' && previous.value.trim() === '') { idx -= 1 diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts index 976c63a83..2b39094d4 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts @@ -9,7 +9,6 @@ import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' import { spliceChangesIntoString, type StringChange } from '../../utils/splice-changes-into-string' -import { writeFileSafely } from '../../utils/write-file-safely' import { extractRawCandidates } from './candidates' import { isSafeMigration } from './is-safe-migration' import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection' @@ -120,18 +119,12 @@ export default async function migrateContents( return spliceChangesIntoString(contents, changes) } -export async function migrate( - designSystem: DesignSystem, - userConfig: Config | null, - file: string, -): Promise { +export async function migrate(designSystem: DesignSystem, userConfig: Config | null, file: string) { let fullPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file) let contents = await fs.readFile(fullPath, 'utf-8') - let migrated = await migrateContents(designSystem, userConfig, contents, extname(file)) - if (migrated === contents) return false // Nothing changed - if (migrated.trim() === '') return false // Emptied out, something went horribly wrong - - await writeFileSafely(fullPath, migrated) - return true + await fs.writeFile( + fullPath, + await migrateContents(designSystem, userConfig, contents, extname(file)), + ) } diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index d304bbe1f..2cfbab371 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { Scanner } from '@tailwindcss/oxide' -import { globby, isGitIgnored } from 'globby' +import { globby } from 'globby' import fs from 'node:fs/promises' import path from 'node:path' import pc from 'picocolors' @@ -19,11 +19,10 @@ import { prepareConfig } from './codemods/template/prepare-config' import { help } from './commands/help' import { Stylesheet } from './stylesheet' import { args, type Arg } from './utils/args' -import { gitRoot, isRepoDirty } from './utils/git' +import { isRepoDirty } from './utils/git' import { pkg } from './utils/packages' import { eprintln, error, header, highlight, info, relative, success } from './utils/renderer' import * as version from './utils/version' -import { writeFileSafely } from './utils/write-file-safely' const options = { '--config': { type: 'string', description: 'Path to the configuration file', alias: '-c' }, @@ -43,9 +42,6 @@ if (flags['--help']) { async function run() { let base = process.cwd() - // Use the project's git root, instead of the `base` to ensure all - // `.gitignore` rules are being used. - let isIgnored = await isGitIgnored({ cwd: gitRoot(base) ?? base }) eprintln(header()) eprintln() @@ -99,7 +95,6 @@ async function run() { info('Searching for CSS files in the current directory and its subdirectories…') files = await globby(['**/*.css'], { - cwd: base, absolute: true, gitignore: true, // gitignore: true will first search for all .gitignore including node_modules folders, this makes the initial search much faster @@ -127,7 +122,7 @@ async function run() { // Analyze the stylesheets try { - await analyzeStylesheets(stylesheets, { base }) + await analyzeStylesheets(stylesheets) } catch (e: any) { error(`${e?.message ?? e}`, { prefix: '↳ ' }) } @@ -201,13 +196,6 @@ async function run() { } } - // Prime the compiler/designSystem cache against the pre-migration AST - // so the later template-migration phase sees the same v4 state. After - // this point, `sheet.root` may be mutated by `migrateStylesheet`. - if (!version.isMajor(3) && sheet.isTailwindRoot) { - await Promise.all([sheet.compiler(), sheet.designSystem()]) - } - await migrateStylesheet(sheet, { newPrefix: config?.newPrefix ?? null, designSystem: config?.designSystem ?? (await sheet.designSystem()), @@ -262,7 +250,7 @@ async function run() { for (let sheet of stylesheets) { if (!sheet.file) continue - await writeFileSafely(sheet.file, sheet.root.toString()) + await fs.writeFile(sheet.file, sheet.root.toString()) if (sheet.isTailwindRoot) { success(`Migrated stylesheet: ${highlight(relative(sheet.file, base))}`, { prefix: '↳ ' }) @@ -305,8 +293,6 @@ async function run() { let designSystem = await sheet.designSystem() if (!designSystem) continue - let config = configBySheet.get(sheet) - // Figure out the source files to migrate let sources = (() => { // Disable auto source detection @@ -314,123 +300,43 @@ async function run() { return [] } - // No root specified + // No root specified, use the base directory if (compiler.root === null) { - // When coming from Tailwind CSS v3, we have to use the - // `config.sources` (which came from `config.content` originally) - if (version.isMajor(3)) { - if (config?.sources) { - return config.sources.map((source) => ({ ...source, negated: false })) - } - - // When we don't have any sources, then we have to fallback to no - // sources at all. We cannot fallback to the `**/*` pattern. - return [] - } - - // When we are upgrading a Tailwind CSS v4 and up version, we use - // the default `**/*` pattern. All custom `@source` directives will - // be attached later as sources. return [{ base, pattern: '**/*', negated: false }] } // Use the specified root return [{ ...compiler.root, negated: false }] })().concat(compiler.sources) + + let config = configBySheet.get(sheet) let scanner = new Scanner({ sources }) let filesToMigrate = [] - - let ignoredPaths = new Set() - for (let file of scanner.files) { - file = await fs.realpath(file).catch(() => file) // Ensure we are dealing with the real path, not symlinks if (file.endsWith('.css')) continue - - // When a file is git ignored, then we don't want to migrate it even - // if it was listed in the `config.content` array or part of any - // `@source` directives. - // - // We can make this an option later to explicitly allow this, but - // this should be the default. This guarantees that: - // - // 1. Files coming from node_modules aren't touched - // 2. Generated files aren't changed (the source should update, not the target) - // 3. You can see all the changes that happened - try { - if (isIgnored(file)) { - let culprit = file - - // To prevent print all ignored files, we can also walk up the - // parent tree and log those instead _if_ they are: - // - // 1. Also git ignored - // 2. Are not going outside of the current repo - let parent = path.dirname(file) - do { - try { - if (isIgnored(parent)) { - culprit = parent - } - } catch { - // Escaping the current repo - break - } - - let nextParent = path.dirname(parent) - if (nextParent === parent) break - parent = nextParent - } while (parent) - - if (ignoredPaths.has(culprit)) continue // Already logged, skip - ignoredPaths.add(culprit) - - if (culprit === file) { - info(`Git ignored, skipping: ${highlight(relative(culprit, base))}`, { - prefix: '↳ ', - }) - } else { - info(`Git ignored folder, skipping: ${highlight(relative(culprit, base))}`, { - prefix: '↳ ', - }) - } - - continue - } - } catch (err) { - info(`Outside repository, skipping: ${highlight(relative(file, base))}`, { - prefix: '↳ ', - }) - // Skip this file when we run into errors. E.g.: when the current - // file is not part of the current git repo it will throw an error. - continue - } - if (seenFiles.has(file)) continue seenFiles.add(file) filesToMigrate.push(file) } // Migrate each file - let changes = 0 await Promise.allSettled( - filesToMigrate.map(async (file) => { - let changed = await migrateTemplate(designSystem, config?.userConfig ?? null, file) - if (changed) { - changes++ - info(`Migrated ${highlight(relative(file, base))}`, { prefix: '↳ ' }) - } - }), + filesToMigrate.map((file) => + migrateTemplate(designSystem, config?.userConfig ?? null, file), + ), ) if (config?.configFilePath) { success( - `Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))} (${changes} file${changes === 1 ? '' : 's'} changed)`, + `Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))}`, { prefix: '↳ ' }, ) } else { success( - `Migrated templates for: ${highlight(relative(sheet.file ?? '', base))} (${changes} file${changes === 1 ? '' : 's'} changed)`, - { prefix: '↳ ' }, + `Migrated templates for: ${highlight(relative(sheet.file ?? '', base))}`, + { + prefix: '↳ ', + }, ) } } diff --git a/packages/@tailwindcss-upgrade/src/stylesheet.ts b/packages/@tailwindcss-upgrade/src/stylesheet.ts index 573413262..42cd1f3c6 100644 --- a/packages/@tailwindcss-upgrade/src/stylesheet.ts +++ b/packages/@tailwindcss-upgrade/src/stylesheet.ts @@ -265,26 +265,23 @@ export class Stylesheet { return false } - #compiler?: Promise> | null> - #designSystem?: Promise> | null> - async compiler(): Promise> | null> { if (!this.isTailwindRoot) return null if (!this.file) return null - return (this.#compiler ??= compileAst(postCssAstToCssAst(this.root), { + return compileAst(postCssAstToCssAst(this.root), { base: path.dirname(this.file), onDependency() {}, - })) + }) } async designSystem(): Promise> | null> { if (!this.isTailwindRoot) return null if (!this.file) return null - return (this.#designSystem ??= __unstable__loadDesignSystem(this.root.toString(), { + return __unstable__loadDesignSystem(this.root.toString(), { base: path.dirname(this.file), - })) + }) } [util.inspect.custom]() { diff --git a/packages/@tailwindcss-upgrade/src/utils/git.ts b/packages/@tailwindcss-upgrade/src/utils/git.ts index 38edf21cb..d716fbb8a 100644 --- a/packages/@tailwindcss-upgrade/src/utils/git.ts +++ b/packages/@tailwindcss-upgrade/src/utils/git.ts @@ -1,20 +1,8 @@ import { execSync } from 'node:child_process' -export function gitRoot(cwd?: string): string | null { +export function isRepoDirty() { try { - return execSync('git rev-parse --show-toplevel', { - encoding: 'utf-8', - cwd, - stdio: ['ignore', 'pipe', 'ignore'], - }).trim() - } catch { - return null - } -} - -export function isRepoDirty(cwd?: string) { - try { - let stdout = execSync('git status --porcelain', { encoding: 'utf-8', cwd }) + let stdout = execSync('git status --porcelain', { encoding: 'utf-8' }) return stdout.trim() !== '' } catch (error) { // If it's not a git repository we don't know if it's dirty or not. But we diff --git a/packages/@tailwindcss-upgrade/src/utils/write-file-safely.ts b/packages/@tailwindcss-upgrade/src/utils/write-file-safely.ts deleted file mode 100644 index 2ff99f9e7..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/write-file-safely.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { randomUUID } from 'node:crypto' -import fs from 'node:fs/promises' -import path from 'node:path' - -export async function writeFileSafely(file: string, contents: string) { - // Resolve symlinks so the rename replaces the actual target file rather than - // replacing the symlink itself with a regular file. - let realFile = await fs.realpath(file).catch(() => file) - - // Start by creating a new file in the current directory that is guaranteed to - // be unique (via `uuid`). We can embed the `process.id` in case we need to - // debug things later. - // - // While we can write this to a more global `/tmp` folder, I want to be 100% - // sure that we are on the same file system (same drive) so the rename - // operation is atomic. Once the file is written, we will rename the file. If - // this fails, the old file is still intact, if it works we have an updated - // file. - // - // If this still causes problems (but it will slow things down): - // 1. We could make sure that we inherit the file permissions - // 2. Use an explicit fsync to force a flush to disk - let temporaryFile = path.join( - path.dirname(realFile), - `.${path.basename(realFile)}.tailwind-upgrade.${process.pid}.${randomUUID()}.tmp`, - ) - - // Write file uses the `w` flag by default, which is defined as: - // > Open file for writing. The file is created (if it does not exist) or truncated (if it exists). - // > https://nodejs.org/api/fs.html#file-system-flags - // - // Which means that if this function is actively running, and you cancel the - // process at the wrong time, then the truncated files are present. Since all - // these migrations happen in parallel, multiple files are open and available - // to be written to, it could mean in multiple truncated files. - // - // Writing to a temp file first means that if the process is cancelled at this - // point, that the old original file is still correct. - // - // The rename part should be atomic (especially because we guarantee it to be - // on the same file system) so this either succeeds or doesn't happen. - try { - await fs.writeFile(temporaryFile, contents, 'utf8') - await fs.rename(temporaryFile, realFile) - } catch (error) { - await fs.unlink(temporaryFile).catch(() => {}) - throw error - } -} diff --git a/packages/@tailwindcss-vite/README.md b/packages/@tailwindcss-vite/README.md index 1a5d25c73..53e03ab47 100644 --- a/packages/@tailwindcss-vite/README.md +++ b/packages/@tailwindcss-vite/README.md @@ -13,10 +13,10 @@

- Build Status + Build Status Total Downloads - Latest Release - License + Latest Release + License

--- @@ -29,11 +29,11 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. --- diff --git a/packages/@tailwindcss-vite/package.json b/packages/@tailwindcss-vite/package.json index 821704a1d..5d591854d 100644 --- a/packages/@tailwindcss-vite/package.json +++ b/packages/@tailwindcss-vite/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/vite", - "version": "4.3.3", + "version": "4.1.16", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -37,6 +37,6 @@ "vite": "catalog:" }, "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" + "vite": "^5.2.0 || ^6 || ^7" } } diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 8fc660eaa..ab9635d10 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -11,13 +11,12 @@ import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' import fs from 'node:fs/promises' import path from 'node:path' -import type { Environment, InternalResolveOptions, Plugin, ResolvedConfig } from 'vite' -import * as vite from 'vite' +import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' const DEBUG = env.DEBUG const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/ const COMMON_JS_PROXY_RE = /\?commonjs-proxy/ -const INLINE_STYLE_ID_RE = /[?&]index=\d+\.css$/ +const INLINE_STYLE_ID_RE = /[?&]index\=\d+\.css$/ export type PluginOptions = { /** @@ -26,143 +25,31 @@ export type PluginOptions = { optimize?: boolean | { minify?: boolean } } -function createCustomResolver( - resolvers: ((id: string, importer: string) => Promise)[], - filter = (_path: string) => true, -) { - return async (id: string, base: string) => { - // The resolver expects an `importer` file. We don't really know where the - // current `id` was imported from, but Vite will essentially do a - // `path.dirname(importer)` so it doesn't really matter. - // - // It does matter that this is a file, otherwise we would go up a directory, - // which means that we would be resolving files from a parent folder first, - // instead of the current folder we are in. - let importer = path.resolve(base, '__placeholder__.ts') - - for (let resolver of resolvers) { - let resolved = await resolver(id, importer) - - // If we didn't resolve, we don't have to bail immediately, but we can try - // the next resolver - if (!resolved) continue - - if (resolved === id) continue - - // Looks like a relative file, let's resolve it to an absolute path - if (resolved[0] === '.') resolved = path.resolve(base, resolved) - - // Must adhere to additional filters (e.g.: must be a .css file) - if (!filter(resolved)) continue - - // If it's not an absolute path, then we don't really know how to read - // the file from disk. - if (!path.isAbsolute(resolved)) continue - - return resolved - } - } -} - export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { + let servers: ViteDevServer[] = [] let config: ResolvedConfig | null = null - let rootsByEnv = new DefaultMap>((env: string) => new Map()) let isSSR = false let shouldOptimize = true let minify = true - function createRoot(env: Environment | null, id: string) { - type ResolveFn = (id: string, base: string) => Promise - - let customCssResolver: ResolveFn - let customJsResolver: ResolveFn - - if (!env) { - // Older, pre-environment Vite API - // TODO: Can we drop this?? - let cssResolver = config!.createResolver({ - ...config!.resolve, - extensions: ['.css'], - mainFields: ['style'], - conditions: ['style', 'development|production'], - tryIndex: false, - preferRelative: true, - }) - - let jsResolver = config!.createResolver(config!.resolve) - - customCssResolver = createCustomResolver( - [ - (id, importer) => cssResolver(id, importer, true, isSSR), - (id, importer) => cssResolver(id, importer, false, isSSR), - ], - (path) => path.endsWith('.css'), - ) - customJsResolver = createCustomResolver( - [ - (id, importer) => jsResolver(id, importer, true, isSSR), - (id, importer) => jsResolver(id, importer, false, isSSR), - ], - (path) => !path.endsWith('.css'), - ) - } else { - type ResolveIdFn = ( - environment: Environment, - id: string, - importer?: string, - aliasOnly?: boolean, - ) => Promise - - // There are cases where Environment API is available, - // but `createResolver` is still overridden (for example astro v5) - // - // Copied as-is from vite, because this function is not a part of public API - // - // TODO: Remove this function and pre-environment code when Vite < 7 is no longer supported - function createBackCompatIdResolver( - config: ResolvedConfig, - options?: Partial, - ): ResolveIdFn { - const compatResolve = config.createResolver(options) - let resolve: ResolveIdFn - return async (environment, id, importer, aliasOnly) => { - if (environment.name === 'client' || environment.name === 'ssr') { - return compatResolve(id, importer, aliasOnly, environment.name === 'ssr') - } - resolve ??= vite.createIdResolver(config, options) - return resolve(environment, id, importer, aliasOnly) - } - } - - // Newer Vite versions - let cssResolver = createBackCompatIdResolver(env.config, { - ...env.config.resolve, - extensions: ['.css'], - mainFields: ['style'], - conditions: ['style', 'development|production'], - tryIndex: false, - preferRelative: true, - }) - - let jsResolver = createBackCompatIdResolver(env.config, env.config.resolve) - - customCssResolver = createCustomResolver( - [ - (id, importer) => cssResolver(env, id, importer, true), - (id, importer) => cssResolver(env, id, importer, false), - ], - (path) => path.endsWith('.css'), - ) - customJsResolver = createCustomResolver( - [ - (id, importer) => jsResolver(env, id, importer, true), - (id, importer) => jsResolver(env, id, importer, false), - ], - (path) => !path.endsWith('.css'), - ) + let roots: DefaultMap = new DefaultMap((id) => { + let cssResolver = config!.createResolver({ + ...config!.resolve, + extensions: ['.css'], + mainFields: ['style'], + conditions: ['style', 'development|production'], + tryIndex: false, + preferRelative: true, + }) + function customCssResolver(id: string, base: string) { + return cssResolver(id, base, true, isSSR) } + let jsResolver = config!.createResolver(config!.resolve) + function customJsResolver(id: string, base: string) { + return jsResolver(id, base, true, isSSR) + } return new Root( id, config!.root, @@ -172,7 +59,7 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { customCssResolver, customJsResolver, ) - } + }) return [ { @@ -180,6 +67,10 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { name: '@tailwindcss/vite:scan', enforce: 'pre', + configureServer(server) { + servers.push(server) + }, + async configResolved(_config) { config = _config isSSR = config.build.ssr !== false && config.build.ssr !== undefined @@ -206,35 +97,23 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { name: '@tailwindcss/vite:generate:serve', apply: 'serve', enforce: 'pre', - transform: { - filter: { - id: { - exclude: [/\/\.vite\//, SPECIAL_QUERY_RE, COMMON_JS_PROXY_RE], - include: [/\.css(?:\?.*)?$/, /&lang\.css/, INLINE_STYLE_ID_RE], - }, - }, - async handler(src, id) { - if (!isPotentialCssRootFile(id)) return - using I = new Instrumentation() - DEBUG && I.start('[@tailwindcss/vite] Generate CSS (serve)') + async transform(src, id, options) { + if (!isPotentialCssRootFile(id)) return - let roots = rootsByEnv.get(this.environment?.name ?? 'default') - let root = roots.get(id) - if (!root) { - root ??= createRoot(this.environment ?? null, id) - roots.set(id, root) - } + using I = new Instrumentation() + DEBUG && I.start('[@tailwindcss/vite] Generate CSS (serve)') - let result = await root.generate(src, (file) => this.addWatchFile(file), I) - if (!result) { - roots.delete(id) - return - } + let root = roots.get(id) - DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)') - return result - }, + let result = await root.generate(src, (file) => this.addWatchFile(file), I) + if (!result) { + roots.delete(id) + return src + } + + DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)') + return result }, }, @@ -243,44 +122,32 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { name: '@tailwindcss/vite:generate:build', apply: 'build', enforce: 'pre', - transform: { - filter: { - id: { - exclude: [/\/\.vite\//, SPECIAL_QUERY_RE, COMMON_JS_PROXY_RE], - include: [/\.css(?:\?.*)?$/, /&lang\.css/, INLINE_STYLE_ID_RE], - }, - }, - async handler(src, id) { - if (!isPotentialCssRootFile(id)) return - using I = new Instrumentation() - DEBUG && I.start('[@tailwindcss/vite] Generate CSS (build)') + async transform(src, id) { + if (!isPotentialCssRootFile(id)) return - let roots = rootsByEnv.get(this.environment?.name ?? 'default') - let root = roots.get(id) - if (!root) { - root ??= createRoot(this.environment ?? null, id) - roots.set(id, root) - } + using I = new Instrumentation() + DEBUG && I.start('[@tailwindcss/vite] Generate CSS (build)') - let result = await root.generate(src, (file) => this.addWatchFile(file), I) - if (!result) { - roots.delete(id) - return - } - DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)') + let root = roots.get(id) - if (shouldOptimize) { - DEBUG && I.start('[@tailwindcss/vite] Optimize CSS') - result = optimize(result.code, { - minify, - map: result.map, - }) - DEBUG && I.end('[@tailwindcss/vite] Optimize CSS') - } + let result = await root.generate(src, (file) => this.addWatchFile(file), I) + if (!result) { + roots.delete(id) + return src + } + DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)') - return result - }, + if (shouldOptimize) { + DEBUG && I.start('[@tailwindcss/vite] Optimize CSS') + result = optimize(result.code, { + minify, + map: result.map, + }) + DEBUG && I.end('[@tailwindcss/vite] Optimize CSS') + } + + return result }, }, ] satisfies Plugin[] @@ -292,15 +159,13 @@ function getExtension(id: string) { } function isPotentialCssRootFile(id: string) { - if (id.includes('/.vite/')) return false - - // Don't intercept special static asset resources - if (SPECIAL_QUERY_RE.test(id)) return false - if (COMMON_JS_PROXY_RE.test(id)) return false - + if (id.includes('/.vite/')) return let extension = getExtension(id) - let isCssFile = extension === 'css' || id.includes('&lang.css') || id.match(INLINE_STYLE_ID_RE) - + let isCssFile = + (extension === 'css' || id.includes('&lang.css') || id.match(INLINE_STYLE_ID_RE)) && + // Don't intercept special static asset resources + !SPECIAL_QUERY_RE.test(id) && + !COMMON_JS_PROXY_RE.test(id) return isCssFile } @@ -372,7 +237,7 @@ class Root { let inputPath = idToPath(this.id) function addWatchFile(file: string) { - // Don't watch the input file since it's already a dependency and causes + // Don't watch the input file since it's already a dependency anc causes // issues with some setups (e.g. Qwik). if (file === inputPath) { return @@ -382,7 +247,7 @@ class Root { // crash Vite. We work around this for now by ignoring updates to them. // // https://github.com/tailwindlabs/tailwindcss/issues/16877 - if (/[#?].*\.svg$/.test(file)) { + if (/[\#\?].*\.svg$/.test(file)) { return } _addWatchFile(file) @@ -442,11 +307,7 @@ class Root { if ( !( this.compiler.features & - (Features.AtApply | - Features.JsPluginCompat | - Features.ThemeFunction | - Features.Utilities | - Features.Variants) + (Features.AtApply | Features.JsPluginCompat | Features.ThemeFunction | Features.Utilities) ) ) { return false @@ -464,7 +325,6 @@ class Root { } if (this.compiler.features & Features.Utilities) { - DEBUG && I.start('Register dependency messages') // Watch individual files found via custom `@source` paths for (let file of this.scanner.files) { addWatchFile(file) @@ -501,7 +361,6 @@ class Root { } } } - DEBUG && I.end('Register dependency messages') } DEBUG && I.start('Build CSS') diff --git a/packages/@tailwindcss-webpack/README.md b/packages/@tailwindcss-webpack/README.md deleted file mode 100644 index 56484ed7f..000000000 --- a/packages/@tailwindcss-webpack/README.md +++ /dev/null @@ -1,102 +0,0 @@ -

- - - - - Tailwind CSS - - -

- -

- A utility-first CSS framework for rapidly building custom user interfaces. -

- -

- Build Status - Total Downloads - Latest Release - License -

- ---- - -## Documentation - -For full documentation, visit [tailwindcss.com](https://tailwindcss.com). - -## Community - -For help, discussion about best practices, or feature ideas: - -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) - -## Contributing - -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. - ---- - -## @tailwindcss/webpack - -A webpack loader for Tailwind CSS v4. - -## Installation - -```sh -npm install @tailwindcss/webpack -``` - -### Usage - -```javascript -// webpack.config.js -const MiniCssExtractPlugin = require('mini-css-extract-plugin') - -module.exports = { - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /\.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, -} -``` - -Then create a CSS file that imports Tailwind: - -```css -/* src/index.css */ -@import 'tailwindcss'; -``` - -### Options - -#### `base` - -The base directory to scan for class candidates. Defaults to the current working directory. - -```javascript -{ - loader: '@tailwindcss/webpack', - options: { - base: process.cwd(), - }, -} -``` - -#### `optimize` - -Whether to optimize and minify the output CSS. Defaults to `true` in production mode. - -```javascript -{ - loader: '@tailwindcss/webpack', - options: { - optimize: true, // or { minify: true } - }, -} -``` diff --git a/packages/@tailwindcss-webpack/package.json b/packages/@tailwindcss-webpack/package.json deleted file mode 100644 index dce09429d..000000000 --- a/packages/@tailwindcss-webpack/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@tailwindcss/webpack", - "version": "4.3.3", - "description": "A webpack loader for Tailwind CSS v4.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/tailwindlabs/tailwindcss.git", - "directory": "packages/@tailwindcss-webpack" - }, - "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", - "homepage": "https://tailwindcss.com", - "scripts": { - "build": "tsup-node", - "dev": "pnpm run build -- --watch" - }, - "files": [ - "dist/" - ], - "publishConfig": { - "provenance": true, - "access": "public" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "workspace:*", - "@tailwindcss/oxide": "workspace:*", - "tailwindcss": "workspace:*" - }, - "devDependencies": { - "@types/node": "catalog:", - "webpack": "catalog:" - }, - "peerDependencies": { - "@rspack/core": "^1.0.0 || ^2.0.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } -} diff --git a/packages/@tailwindcss-webpack/src/index.cts b/packages/@tailwindcss-webpack/src/index.cts deleted file mode 100644 index dbdded748..000000000 --- a/packages/@tailwindcss-webpack/src/index.cts +++ /dev/null @@ -1,5 +0,0 @@ -import tailwindLoader from './index.ts' - -// CommonJS export for webpack loaders - must be the function directly -// @ts-ignore -export = tailwindLoader diff --git a/packages/@tailwindcss-webpack/src/index.ts b/packages/@tailwindcss-webpack/src/index.ts deleted file mode 100644 index 6d02f1f0b..000000000 --- a/packages/@tailwindcss-webpack/src/index.ts +++ /dev/null @@ -1,287 +0,0 @@ -import QuickLRU from '@alloc/quick-lru' -import { - compile, - env, - Features, - Instrumentation, - normalizePath, - optimize, - Polyfills, -} from '@tailwindcss/node' -import { clearRequireCache } from '@tailwindcss/node/require-cache' -import { Scanner } from '@tailwindcss/oxide' -import fs from 'node:fs' -import path from 'node:path' -import type { LoaderContext } from 'webpack' - -const DEBUG = env.DEBUG - -export interface LoaderOptions { - /** - * The base directory to scan for class candidates. - * - * Defaults to the current working directory. - */ - base?: string - - /** - * Optimize and minify the output CSS. - */ - optimize?: boolean | { minify?: boolean } -} - -interface CacheEntry { - mtimes: Map - compiler: null | Awaited> - scanner: null | Scanner - candidates: Set - fullRebuildPaths: string[] -} - -const cache = new QuickLRU({ maxSize: 50 }) - -function getCacheKey(resourceId: string, opts: LoaderOptions): string { - return `${resourceId}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}` -} - -function getContextFromCache(resourceId: string, opts: LoaderOptions): CacheEntry { - let key = getCacheKey(resourceId, opts) - if (cache.has(key)) return cache.get(key)! - let entry: CacheEntry = { - mtimes: new Map(), - compiler: null, - scanner: null, - candidates: new Set(), - fullRebuildPaths: [], - } - cache.set(key, entry) - return entry -} - -export default async function tailwindLoader( - this: LoaderContext, - source: string, -): Promise { - let callback = this.async() - let options = this.getOptions() ?? {} - let inputFile = this.resourcePath - let resourceId = this.resource - let base = options.base ?? process.cwd() - let shouldOptimize = options.optimize ?? process.env.NODE_ENV === 'production' - let isCSSModuleFile = inputFile.endsWith('.module.css') - - using I = new Instrumentation() - - DEBUG && I.start(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - - // Bail out early if this is guaranteed to be a non-Tailwind CSS file. - { - DEBUG && I.start('Quick bail check') - let canBail = !/@(import|reference|theme|variant|config|plugin|apply|tailwind)\b/.test(source) - if (canBail) { - DEBUG && I.end('Quick bail check') - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(null, source) - return - } - DEBUG && I.end('Quick bail check') - } - - try { - let context = getContextFromCache(resourceId, options) - let inputBasePath = path.dirname(path.resolve(inputFile)) - - // Whether this is the first build or not - let isInitialBuild = context.compiler === null - - async function createCompiler() { - DEBUG && I.start('Setup compiler') - if (context.fullRebuildPaths.length > 0 && !isInitialBuild) { - clearRequireCache(context.fullRebuildPaths) - } - - context.fullRebuildPaths = [] - - DEBUG && I.start('Create compiler') - let compiler = await compile(source, { - from: inputFile, - base: inputBasePath, - shouldRewriteUrls: true, - onDependency: (depPath) => context.fullRebuildPaths.push(depPath), - // In CSS Module files, we have to disable the `@property` polyfill since these will - // emit global `*` rules which are considered to be non-pure and will cause builds - // to fail. - polyfills: isCSSModuleFile ? Polyfills.All ^ Polyfills.AtProperty : Polyfills.All, - }) - DEBUG && I.end('Create compiler') - - DEBUG && I.end('Setup compiler') - return compiler - } - - // Setup the compiler if it doesn't exist yet - context.compiler ??= await createCompiler() - - // Early exit if no Tailwind features are used - if (context.compiler.features === Features.None) { - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(null, source) - return - } - - let rebuildStrategy: 'full' | 'incremental' = 'incremental' - - // Track file modification times to CSS files - DEBUG && I.start('Register full rebuild paths') - { - // Report dependencies for config files, plugins, etc. - for (let file of context.fullRebuildPaths) { - this.addDependency(path.resolve(file)) - } - - let files = [...context.fullRebuildPaths, inputFile] - - for (let file of files) { - let changedTime: number | null = null - try { - changedTime = fs.statSync(file)?.mtimeMs ?? null - } catch { - // File might not exist - } - - if (changedTime === null) { - if (file === inputFile) { - rebuildStrategy = 'full' - } - continue - } - - let prevTime = context.mtimes.get(file) - if (prevTime === changedTime) continue - - rebuildStrategy = 'full' - context.mtimes.set(file, changedTime) - } - } - DEBUG && I.end('Register full rebuild paths') - - if (rebuildStrategy === 'full' && !isInitialBuild) { - context.compiler = await createCompiler() - } - - let compiler = context.compiler - - // Check if we need to process this file at all - if ( - !( - compiler.features & - (Features.AtApply | Features.JsPluginCompat | Features.ThemeFunction | Features.Utilities) - ) - ) { - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(null, source) - return - } - - // Setup or update scanner if needed - if (context.scanner === null || rebuildStrategy === 'full') { - DEBUG && I.start('Setup scanner') - let sources = (() => { - // Disable auto source detection - if (compiler.root === 'none') { - return [] - } - - // No root specified, use the base directory - if (compiler.root === null) { - return [{ base, pattern: '**/*', negated: false }] - } - - // Use the specified root - return [{ ...compiler.root, negated: false }] - })().concat(compiler.sources) - - context.scanner = new Scanner({ sources }) - DEBUG && I.end('Setup scanner') - } - - // Scan for candidates if utilities are used - if (compiler.features & Features.Utilities) { - DEBUG && I.start('Scan for candidates') - for (let candidate of context.scanner.scan()) { - context.candidates.add(candidate) - } - DEBUG && I.end('Scan for candidates') - - DEBUG && I.start('Register dependency messages') - // Add all found files as direct dependencies - let resolvedInputFile = path.resolve(base, inputFile) - for (let file of context.scanner.files) { - let absolutePath = path.resolve(file) - // The CSS file cannot be a dependency of itself - if (absolutePath === resolvedInputFile) { - continue - } - this.addDependency(absolutePath) - } - - // Register context dependencies for glob patterns - for (let glob of context.scanner.globs) { - // Skip negated patterns - if (glob.pattern[0] === '!') continue - - // Avoid adding a dependency on the base directory itself - if (glob.pattern === '*' && base === glob.base) { - continue - } - - this.addContextDependency(path.resolve(glob.base)) - } - - // Validate that source(...) paths are directories - let root = compiler.root - if (root !== 'none' && root !== null) { - let basePath = normalizePath(path.resolve(root.base, root.pattern)) - try { - let stats = fs.statSync(basePath) - if (!stats.isDirectory()) { - throw new Error( - `The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`, - ) - } - } catch (err) { - if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { - throw err - } - // Directory doesn't exist yet, which is fine - } - } - DEBUG && I.end('Register dependency messages') - } - - DEBUG && I.start('Build utilities') - let css = compiler.build([...context.candidates]) - DEBUG && I.end('Build utilities') - - // Optionally optimize the output - let result = css - if (shouldOptimize) { - DEBUG && I.start('Optimization') - let optimized = optimize(css, { - minify: typeof shouldOptimize === 'object' ? shouldOptimize.minify : true, - }) - result = optimized.code - DEBUG && I.end('Optimization') - } - - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(null, result) - } catch (error) { - // Clear the cache entry on error to force a full rebuild next time - let key = getCacheKey(resourceId, options) - cache.delete(key) - - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(error as Error) - } -} diff --git a/packages/@tailwindcss-webpack/tsconfig.json b/packages/@tailwindcss-webpack/tsconfig.json deleted file mode 100644 index d2131cbeb..000000000 --- a/packages/@tailwindcss-webpack/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "compilerOptions": { - "verbatimModuleSyntax": false, - }, -} diff --git a/packages/@tailwindcss-webpack/tsup.config.ts b/packages/@tailwindcss-webpack/tsup.config.ts deleted file mode 100644 index 998eebe4f..000000000 --- a/packages/@tailwindcss-webpack/tsup.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from 'tsup' - -export default defineConfig([ - { - format: ['esm'], - clean: true, - minify: true, - cjsInterop: true, - dts: true, - entry: ['src/index.ts'], - }, - { - format: ['cjs'], - minify: true, - cjsInterop: true, - dts: true, - entry: ['src/index.cts'], - }, -]) diff --git a/packages/tailwindcss/README.md b/packages/tailwindcss/README.md index 5f532607d..7d21bd883 100644 --- a/packages/tailwindcss/README.md +++ b/packages/tailwindcss/README.md @@ -13,10 +13,10 @@

- Build Status + Build Status Total Downloads - Latest Release - License + Latest Release + License

--- @@ -29,8 +29,8 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index d1288d6fa..a18284c63 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -1,6 +1,6 @@ { "name": "tailwindcss", - "version": "4.3.3", + "version": "4.1.16", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -127,12 +127,12 @@ "utilities.css" ], "devDependencies": { - "@jridgewell/remapping": "^2.3.5", + "@jridgewell/remapping": "^2.3.4", "@tailwindcss/oxide": "workspace:^", "@types/node": "catalog:", - "dedent": "catalog:", + "dedent": "1.7.0", "lightningcss": "catalog:", - "magic-string": "^1.1.0", + "magic-string": "^0.30.21", "source-map-js": "^1.2.1" } } diff --git a/packages/tailwindcss/playwright.config.ts b/packages/tailwindcss/playwright.config.ts index ba8f95d70..c0a123abf 100644 --- a/packages/tailwindcss/playwright.config.ts +++ b/packages/tailwindcss/playwright.config.ts @@ -20,7 +20,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('/')`. */ @@ -36,11 +36,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: { diff --git a/packages/tailwindcss/preflight.css b/packages/tailwindcss/preflight.css index 04e87905c..753e79ef0 100644 --- a/packages/tailwindcss/preflight.css +++ b/packages/tailwindcss/preflight.css @@ -32,13 +32,8 @@ html, tab-size: 4; /* 3 */ font-family: --theme( --default-font-family, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - Roboto, - 'Helvetica Neue', - 'Noto Sans', - Arial, + ui-sans-serif, + system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', @@ -175,7 +170,7 @@ table { Use the modern Firefox focus style for all focusable elements. */ -:-moz-focusring:where(:not(iframe)) { +:-moz-focusring { outline: auto; } diff --git a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap index 9921709d7..70060e1e1 100644 --- a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap @@ -1,8 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`compiling CSS > \`@tailwind utilities\` is replaced by utilities using the default theme 1`] = ` -" -@layer properties { +"@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-shadow: 0 0 #0000; @@ -124,18 +123,14 @@ exports[`compiling CSS > \`@tailwind utilities\` is replaced by utilities using syntax: "*"; inherits: false; initial-value: 0 0 #0000; -} -" +}" `; exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = ` -" -@layer theme { +"@layer theme { :root, :host { - --tw-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"; - --tw-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", - monospace; + --tw-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --tw-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --tw-default-font-family: var(--tw-font-sans); --tw-default-mono-font-family: var(--tw-font-mono); } @@ -160,7 +155,7 @@ exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = ` -webkit-text-size-adjust: 100%; tab-size: 4; line-height: 1.5; - font-family: var(--tw-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(--tw-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(--tw-default-font-feature-settings, normal); font-variation-settings: var(--tw-default-font-variation-settings, normal); -webkit-tap-highlight-color: transparent; @@ -226,7 +221,7 @@ exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = ` border-collapse: collapse; } - :-moz-focusring:where(:not(iframe)) { + :-moz-focusring { outline: auto; } @@ -388,6 +383,5 @@ exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = ` } } -@layer components, utilities; -" +@layer components, utilities;" `; diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index d595e8c01..690b1cb1f 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -306,6 +306,66 @@ exports[`getClassList 1`] = ` "-col-start-11", "-col-start-12", "-col-start-13", + "-end-0", + "-end-0.5", + "-end-1", + "-end-1.5", + "-end-2", + "-end-2.5", + "-end-3", + "-end-3.5", + "-end-4", + "-end-5", + "-end-6", + "-end-7", + "-end-8", + "-end-9", + "-end-10", + "-end-11", + "-end-12", + "-end-14", + "-end-16", + "-end-20", + "-end-24", + "-end-28", + "-end-32", + "-end-36", + "-end-40", + "-end-44", + "-end-48", + "-end-52", + "-end-56", + "-end-60", + "-end-64", + "-end-72", + "-end-80", + "-end-96", + "-end-1/2", + "-end-1/3", + "-end-1/4", + "-end-1/5", + "-end-1/6", + "-end-1/12", + "-end-2/3", + "-end-2/4", + "-end-2/5", + "-end-2/6", + "-end-2/12", + "-end-3/4", + "-end-3/5", + "-end-3/6", + "-end-3/12", + "-end-4/5", + "-end-4/6", + "-end-4/12", + "-end-5/6", + "-end-5/12", + "-end-6/12", + "-end-7/12", + "-end-8/12", + "-end-9/12", + "-end-10/12", + "-end-11/12", "-end-full", "-end-px", "-indent-0", @@ -403,256 +463,8 @@ exports[`getClassList 1`] = ` "-inset-9/12", "-inset-10/12", "-inset-11/12", - "-inset-be-0", - "-inset-be-0.5", - "-inset-be-1", - "-inset-be-1.5", - "-inset-be-2", - "-inset-be-2.5", - "-inset-be-3", - "-inset-be-3.5", - "-inset-be-4", - "-inset-be-5", - "-inset-be-6", - "-inset-be-7", - "-inset-be-8", - "-inset-be-9", - "-inset-be-10", - "-inset-be-11", - "-inset-be-12", - "-inset-be-14", - "-inset-be-16", - "-inset-be-20", - "-inset-be-24", - "-inset-be-28", - "-inset-be-32", - "-inset-be-36", - "-inset-be-40", - "-inset-be-44", - "-inset-be-48", - "-inset-be-52", - "-inset-be-56", - "-inset-be-60", - "-inset-be-64", - "-inset-be-72", - "-inset-be-80", - "-inset-be-96", - "-inset-be-1/2", - "-inset-be-1/3", - "-inset-be-1/4", - "-inset-be-1/5", - "-inset-be-1/6", - "-inset-be-1/12", - "-inset-be-2/3", - "-inset-be-2/4", - "-inset-be-2/5", - "-inset-be-2/6", - "-inset-be-2/12", - "-inset-be-3/4", - "-inset-be-3/5", - "-inset-be-3/6", - "-inset-be-3/12", - "-inset-be-4/5", - "-inset-be-4/6", - "-inset-be-4/12", - "-inset-be-5/6", - "-inset-be-5/12", - "-inset-be-6/12", - "-inset-be-7/12", - "-inset-be-8/12", - "-inset-be-9/12", - "-inset-be-10/12", - "-inset-be-11/12", - "-inset-be-full", - "-inset-be-px", - "-inset-bs-0", - "-inset-bs-0.5", - "-inset-bs-1", - "-inset-bs-1.5", - "-inset-bs-2", - "-inset-bs-2.5", - "-inset-bs-3", - "-inset-bs-3.5", - "-inset-bs-4", - "-inset-bs-5", - "-inset-bs-6", - "-inset-bs-7", - "-inset-bs-8", - "-inset-bs-9", - "-inset-bs-10", - "-inset-bs-11", - "-inset-bs-12", - "-inset-bs-14", - "-inset-bs-16", - "-inset-bs-20", - "-inset-bs-24", - "-inset-bs-28", - "-inset-bs-32", - "-inset-bs-36", - "-inset-bs-40", - "-inset-bs-44", - "-inset-bs-48", - "-inset-bs-52", - "-inset-bs-56", - "-inset-bs-60", - "-inset-bs-64", - "-inset-bs-72", - "-inset-bs-80", - "-inset-bs-96", - "-inset-bs-1/2", - "-inset-bs-1/3", - "-inset-bs-1/4", - "-inset-bs-1/5", - "-inset-bs-1/6", - "-inset-bs-1/12", - "-inset-bs-2/3", - "-inset-bs-2/4", - "-inset-bs-2/5", - "-inset-bs-2/6", - "-inset-bs-2/12", - "-inset-bs-3/4", - "-inset-bs-3/5", - "-inset-bs-3/6", - "-inset-bs-3/12", - "-inset-bs-4/5", - "-inset-bs-4/6", - "-inset-bs-4/12", - "-inset-bs-5/6", - "-inset-bs-5/12", - "-inset-bs-6/12", - "-inset-bs-7/12", - "-inset-bs-8/12", - "-inset-bs-9/12", - "-inset-bs-10/12", - "-inset-bs-11/12", - "-inset-bs-full", - "-inset-bs-px", - "-inset-e-0", - "-inset-e-0.5", - "-inset-e-1", - "-inset-e-1.5", - "-inset-e-2", - "-inset-e-2.5", - "-inset-e-3", - "-inset-e-3.5", - "-inset-e-4", - "-inset-e-5", - "-inset-e-6", - "-inset-e-7", - "-inset-e-8", - "-inset-e-9", - "-inset-e-10", - "-inset-e-11", - "-inset-e-12", - "-inset-e-14", - "-inset-e-16", - "-inset-e-20", - "-inset-e-24", - "-inset-e-28", - "-inset-e-32", - "-inset-e-36", - "-inset-e-40", - "-inset-e-44", - "-inset-e-48", - "-inset-e-52", - "-inset-e-56", - "-inset-e-60", - "-inset-e-64", - "-inset-e-72", - "-inset-e-80", - "-inset-e-96", - "-inset-e-1/2", - "-inset-e-1/3", - "-inset-e-1/4", - "-inset-e-1/5", - "-inset-e-1/6", - "-inset-e-1/12", - "-inset-e-2/3", - "-inset-e-2/4", - "-inset-e-2/5", - "-inset-e-2/6", - "-inset-e-2/12", - "-inset-e-3/4", - "-inset-e-3/5", - "-inset-e-3/6", - "-inset-e-3/12", - "-inset-e-4/5", - "-inset-e-4/6", - "-inset-e-4/12", - "-inset-e-5/6", - "-inset-e-5/12", - "-inset-e-6/12", - "-inset-e-7/12", - "-inset-e-8/12", - "-inset-e-9/12", - "-inset-e-10/12", - "-inset-e-11/12", - "-inset-e-full", - "-inset-e-px", "-inset-full", "-inset-px", - "-inset-s-0", - "-inset-s-0.5", - "-inset-s-1", - "-inset-s-1.5", - "-inset-s-2", - "-inset-s-2.5", - "-inset-s-3", - "-inset-s-3.5", - "-inset-s-4", - "-inset-s-5", - "-inset-s-6", - "-inset-s-7", - "-inset-s-8", - "-inset-s-9", - "-inset-s-10", - "-inset-s-11", - "-inset-s-12", - "-inset-s-14", - "-inset-s-16", - "-inset-s-20", - "-inset-s-24", - "-inset-s-28", - "-inset-s-32", - "-inset-s-36", - "-inset-s-40", - "-inset-s-44", - "-inset-s-48", - "-inset-s-52", - "-inset-s-56", - "-inset-s-60", - "-inset-s-64", - "-inset-s-72", - "-inset-s-80", - "-inset-s-96", - "-inset-s-1/2", - "-inset-s-1/3", - "-inset-s-1/4", - "-inset-s-1/5", - "-inset-s-1/6", - "-inset-s-1/12", - "-inset-s-2/3", - "-inset-s-2/4", - "-inset-s-2/5", - "-inset-s-2/6", - "-inset-s-2/12", - "-inset-s-3/4", - "-inset-s-3/5", - "-inset-s-3/6", - "-inset-s-3/12", - "-inset-s-4/5", - "-inset-s-4/6", - "-inset-s-4/12", - "-inset-s-5/6", - "-inset-s-5/12", - "-inset-s-6/12", - "-inset-s-7/12", - "-inset-s-8/12", - "-inset-s-9/12", - "-inset-s-10/12", - "-inset-s-11/12", - "-inset-s-full", - "-inset-s-px", "-inset-x-0", "-inset-x-0.5", "-inset-x-1", @@ -927,76 +739,6 @@ exports[`getClassList 1`] = ` "-mb-80", "-mb-96", "-mb-px", - "-mbe-0", - "-mbe-0.5", - "-mbe-1", - "-mbe-1.5", - "-mbe-2", - "-mbe-2.5", - "-mbe-3", - "-mbe-3.5", - "-mbe-4", - "-mbe-5", - "-mbe-6", - "-mbe-7", - "-mbe-8", - "-mbe-9", - "-mbe-10", - "-mbe-11", - "-mbe-12", - "-mbe-14", - "-mbe-16", - "-mbe-20", - "-mbe-24", - "-mbe-28", - "-mbe-32", - "-mbe-36", - "-mbe-40", - "-mbe-44", - "-mbe-48", - "-mbe-52", - "-mbe-56", - "-mbe-60", - "-mbe-64", - "-mbe-72", - "-mbe-80", - "-mbe-96", - "-mbe-px", - "-mbs-0", - "-mbs-0.5", - "-mbs-1", - "-mbs-1.5", - "-mbs-2", - "-mbs-2.5", - "-mbs-3", - "-mbs-3.5", - "-mbs-4", - "-mbs-5", - "-mbs-6", - "-mbs-7", - "-mbs-8", - "-mbs-9", - "-mbs-10", - "-mbs-11", - "-mbs-12", - "-mbs-14", - "-mbs-16", - "-mbs-20", - "-mbs-24", - "-mbs-28", - "-mbs-32", - "-mbs-36", - "-mbs-40", - "-mbs-44", - "-mbs-48", - "-mbs-52", - "-mbs-56", - "-mbs-60", - "-mbs-64", - "-mbs-72", - "-mbs-80", - "-mbs-96", - "-mbs-px", "-me-0", "-me-0.5", "-me-1", @@ -1497,76 +1239,6 @@ exports[`getClassList 1`] = ` "-scroll-mb-80", "-scroll-mb-96", "-scroll-mb-px", - "-scroll-mbe-0", - "-scroll-mbe-0.5", - "-scroll-mbe-1", - "-scroll-mbe-1.5", - "-scroll-mbe-2", - "-scroll-mbe-2.5", - "-scroll-mbe-3", - "-scroll-mbe-3.5", - "-scroll-mbe-4", - "-scroll-mbe-5", - "-scroll-mbe-6", - "-scroll-mbe-7", - "-scroll-mbe-8", - "-scroll-mbe-9", - "-scroll-mbe-10", - "-scroll-mbe-11", - "-scroll-mbe-12", - "-scroll-mbe-14", - "-scroll-mbe-16", - "-scroll-mbe-20", - "-scroll-mbe-24", - "-scroll-mbe-28", - "-scroll-mbe-32", - "-scroll-mbe-36", - "-scroll-mbe-40", - "-scroll-mbe-44", - "-scroll-mbe-48", - "-scroll-mbe-52", - "-scroll-mbe-56", - "-scroll-mbe-60", - "-scroll-mbe-64", - "-scroll-mbe-72", - "-scroll-mbe-80", - "-scroll-mbe-96", - "-scroll-mbe-px", - "-scroll-mbs-0", - "-scroll-mbs-0.5", - "-scroll-mbs-1", - "-scroll-mbs-1.5", - "-scroll-mbs-2", - "-scroll-mbs-2.5", - "-scroll-mbs-3", - "-scroll-mbs-3.5", - "-scroll-mbs-4", - "-scroll-mbs-5", - "-scroll-mbs-6", - "-scroll-mbs-7", - "-scroll-mbs-8", - "-scroll-mbs-9", - "-scroll-mbs-10", - "-scroll-mbs-11", - "-scroll-mbs-12", - "-scroll-mbs-14", - "-scroll-mbs-16", - "-scroll-mbs-20", - "-scroll-mbs-24", - "-scroll-mbs-28", - "-scroll-mbs-32", - "-scroll-mbs-36", - "-scroll-mbs-40", - "-scroll-mbs-44", - "-scroll-mbs-48", - "-scroll-mbs-52", - "-scroll-mbs-56", - "-scroll-mbs-60", - "-scroll-mbs-64", - "-scroll-mbs-72", - "-scroll-mbs-80", - "-scroll-mbs-96", - "-scroll-mbs-px", "-scroll-me-0", "-scroll-me-0.5", "-scroll-me-1", @@ -1900,6 +1572,66 @@ exports[`getClassList 1`] = ` "-space-y-80", "-space-y-96", "-space-y-px", + "-start-0", + "-start-0.5", + "-start-1", + "-start-1.5", + "-start-2", + "-start-2.5", + "-start-3", + "-start-3.5", + "-start-4", + "-start-5", + "-start-6", + "-start-7", + "-start-8", + "-start-9", + "-start-10", + "-start-11", + "-start-12", + "-start-14", + "-start-16", + "-start-20", + "-start-24", + "-start-28", + "-start-32", + "-start-36", + "-start-40", + "-start-44", + "-start-48", + "-start-52", + "-start-56", + "-start-60", + "-start-64", + "-start-72", + "-start-80", + "-start-96", + "-start-1/2", + "-start-1/3", + "-start-1/4", + "-start-1/5", + "-start-1/6", + "-start-1/12", + "-start-2/3", + "-start-2/4", + "-start-2/5", + "-start-2/6", + "-start-2/12", + "-start-3/4", + "-start-3/5", + "-start-3/6", + "-start-3/12", + "-start-4/5", + "-start-4/6", + "-start-4/12", + "-start-5/6", + "-start-5/12", + "-start-6/12", + "-start-7/12", + "-start-8/12", + "-start-9/12", + "-start-10/12", + "-start-11/12", "-start-full", "-start-px", "-top-0", @@ -2222,7 +1954,6 @@ exports[`getClassList 1`] = ` "accent-current/90", "accent-current/95", "accent-current/100", - "accent-current/background", "accent-inherit", "accent-inherit/0", "accent-inherit/5", @@ -2245,7 +1976,6 @@ exports[`getClassList 1`] = ` "accent-inherit/90", "accent-inherit/95", "accent-inherit/100", - "accent-inherit/background", "accent-transparent", "accent-transparent/0", "accent-transparent/5", @@ -2268,7 +1998,6 @@ exports[`getClassList 1`] = ` "accent-transparent/90", "accent-transparent/95", "accent-transparent/100", - "accent-transparent/background", "align-baseline", "align-bottom", "align-middle", @@ -2593,7 +2322,6 @@ exports[`getClassList 1`] = ` "bg-current/90", "bg-current/95", "bg-current/100", - "bg-current/background", "bg-fixed", "bg-inherit", "bg-inherit/0", @@ -2617,7 +2345,6 @@ exports[`getClassList 1`] = ` "bg-inherit/90", "bg-inherit/95", "bg-inherit/100", - "bg-inherit/background", "bg-left", "bg-linear-0", "bg-linear-0/oklab", @@ -2846,79 +2573,7 @@ exports[`getClassList 1`] = ` "bg-transparent/90", "bg-transparent/95", "bg-transparent/100", - "bg-transparent/background", "block", - "block-0", - "block-0.5", - "block-1", - "block-1.5", - "block-2", - "block-2.5", - "block-3", - "block-3.5", - "block-4", - "block-5", - "block-6", - "block-7", - "block-8", - "block-9", - "block-10", - "block-11", - "block-12", - "block-14", - "block-16", - "block-20", - "block-24", - "block-28", - "block-32", - "block-36", - "block-40", - "block-44", - "block-48", - "block-52", - "block-56", - "block-60", - "block-64", - "block-72", - "block-80", - "block-96", - "block-1/2", - "block-1/3", - "block-1/4", - "block-1/5", - "block-1/6", - "block-1/12", - "block-2/3", - "block-2/4", - "block-2/5", - "block-2/6", - "block-2/12", - "block-3/4", - "block-3/5", - "block-3/6", - "block-3/12", - "block-4/5", - "block-4/6", - "block-4/12", - "block-5/6", - "block-5/12", - "block-6/12", - "block-7/12", - "block-8/12", - "block-9/12", - "block-10/12", - "block-11/12", - "block-auto", - "block-dvh", - "block-fit", - "block-full", - "block-lh", - "block-lvh", - "block-max", - "block-min", - "block-px", - "block-screen", - "block-svh", "blur-none", "border", "border/0", @@ -2942,7 +2597,6 @@ exports[`getClassList 1`] = ` "border/90", "border/95", "border/100", - "border/background", "border-0", "border-2", "border-4", @@ -2969,7 +2623,6 @@ exports[`getClassList 1`] = ` "border-b/90", "border-b/95", "border-b/100", - "border-b/background", "border-b-0", "border-b-2", "border-b-4", @@ -2996,7 +2649,6 @@ exports[`getClassList 1`] = ` "border-b-current/90", "border-b-current/95", "border-b-current/100", - "border-b-current/background", "border-b-inherit", "border-b-inherit/0", "border-b-inherit/5", @@ -3019,7 +2671,6 @@ exports[`getClassList 1`] = ` "border-b-inherit/90", "border-b-inherit/95", "border-b-inherit/100", - "border-b-inherit/background", "border-b-transparent", "border-b-transparent/0", "border-b-transparent/5", @@ -3042,199 +2693,6 @@ exports[`getClassList 1`] = ` "border-b-transparent/90", "border-b-transparent/95", "border-b-transparent/100", - "border-b-transparent/background", - "border-be", - "border-be/0", - "border-be/5", - "border-be/10", - "border-be/15", - "border-be/20", - "border-be/25", - "border-be/30", - "border-be/35", - "border-be/40", - "border-be/45", - "border-be/50", - "border-be/55", - "border-be/60", - "border-be/65", - "border-be/70", - "border-be/75", - "border-be/80", - "border-be/85", - "border-be/90", - "border-be/95", - "border-be/100", - "border-be/background", - "border-be-0", - "border-be-2", - "border-be-4", - "border-be-8", - "border-be-current", - "border-be-current/0", - "border-be-current/5", - "border-be-current/10", - "border-be-current/15", - "border-be-current/20", - "border-be-current/25", - "border-be-current/30", - "border-be-current/35", - "border-be-current/40", - "border-be-current/45", - "border-be-current/50", - "border-be-current/55", - "border-be-current/60", - "border-be-current/65", - "border-be-current/70", - "border-be-current/75", - "border-be-current/80", - "border-be-current/85", - "border-be-current/90", - "border-be-current/95", - "border-be-current/100", - "border-be-current/background", - "border-be-inherit", - "border-be-inherit/0", - "border-be-inherit/5", - "border-be-inherit/10", - "border-be-inherit/15", - "border-be-inherit/20", - "border-be-inherit/25", - "border-be-inherit/30", - "border-be-inherit/35", - "border-be-inherit/40", - "border-be-inherit/45", - "border-be-inherit/50", - "border-be-inherit/55", - "border-be-inherit/60", - "border-be-inherit/65", - "border-be-inherit/70", - "border-be-inherit/75", - "border-be-inherit/80", - "border-be-inherit/85", - "border-be-inherit/90", - "border-be-inherit/95", - "border-be-inherit/100", - "border-be-inherit/background", - "border-be-transparent", - "border-be-transparent/0", - "border-be-transparent/5", - "border-be-transparent/10", - "border-be-transparent/15", - "border-be-transparent/20", - "border-be-transparent/25", - "border-be-transparent/30", - "border-be-transparent/35", - "border-be-transparent/40", - "border-be-transparent/45", - "border-be-transparent/50", - "border-be-transparent/55", - "border-be-transparent/60", - "border-be-transparent/65", - "border-be-transparent/70", - "border-be-transparent/75", - "border-be-transparent/80", - "border-be-transparent/85", - "border-be-transparent/90", - "border-be-transparent/95", - "border-be-transparent/100", - "border-be-transparent/background", - "border-bs", - "border-bs/0", - "border-bs/5", - "border-bs/10", - "border-bs/15", - "border-bs/20", - "border-bs/25", - "border-bs/30", - "border-bs/35", - "border-bs/40", - "border-bs/45", - "border-bs/50", - "border-bs/55", - "border-bs/60", - "border-bs/65", - "border-bs/70", - "border-bs/75", - "border-bs/80", - "border-bs/85", - "border-bs/90", - "border-bs/95", - "border-bs/100", - "border-bs/background", - "border-bs-0", - "border-bs-2", - "border-bs-4", - "border-bs-8", - "border-bs-current", - "border-bs-current/0", - "border-bs-current/5", - "border-bs-current/10", - "border-bs-current/15", - "border-bs-current/20", - "border-bs-current/25", - "border-bs-current/30", - "border-bs-current/35", - "border-bs-current/40", - "border-bs-current/45", - "border-bs-current/50", - "border-bs-current/55", - "border-bs-current/60", - "border-bs-current/65", - "border-bs-current/70", - "border-bs-current/75", - "border-bs-current/80", - "border-bs-current/85", - "border-bs-current/90", - "border-bs-current/95", - "border-bs-current/100", - "border-bs-current/background", - "border-bs-inherit", - "border-bs-inherit/0", - "border-bs-inherit/5", - "border-bs-inherit/10", - "border-bs-inherit/15", - "border-bs-inherit/20", - "border-bs-inherit/25", - "border-bs-inherit/30", - "border-bs-inherit/35", - "border-bs-inherit/40", - "border-bs-inherit/45", - "border-bs-inherit/50", - "border-bs-inherit/55", - "border-bs-inherit/60", - "border-bs-inherit/65", - "border-bs-inherit/70", - "border-bs-inherit/75", - "border-bs-inherit/80", - "border-bs-inherit/85", - "border-bs-inherit/90", - "border-bs-inherit/95", - "border-bs-inherit/100", - "border-bs-inherit/background", - "border-bs-transparent", - "border-bs-transparent/0", - "border-bs-transparent/5", - "border-bs-transparent/10", - "border-bs-transparent/15", - "border-bs-transparent/20", - "border-bs-transparent/25", - "border-bs-transparent/30", - "border-bs-transparent/35", - "border-bs-transparent/40", - "border-bs-transparent/45", - "border-bs-transparent/50", - "border-bs-transparent/55", - "border-bs-transparent/60", - "border-bs-transparent/65", - "border-bs-transparent/70", - "border-bs-transparent/75", - "border-bs-transparent/80", - "border-bs-transparent/85", - "border-bs-transparent/90", - "border-bs-transparent/95", - "border-bs-transparent/100", - "border-bs-transparent/background", "border-collapse", "border-current", "border-current/0", @@ -3258,7 +2716,6 @@ exports[`getClassList 1`] = ` "border-current/90", "border-current/95", "border-current/100", - "border-current/background", "border-dashed", "border-dotted", "border-double", @@ -3284,7 +2741,6 @@ exports[`getClassList 1`] = ` "border-e/90", "border-e/95", "border-e/100", - "border-e/background", "border-e-0", "border-e-2", "border-e-4", @@ -3311,7 +2767,6 @@ exports[`getClassList 1`] = ` "border-e-current/90", "border-e-current/95", "border-e-current/100", - "border-e-current/background", "border-e-inherit", "border-e-inherit/0", "border-e-inherit/5", @@ -3334,7 +2789,6 @@ exports[`getClassList 1`] = ` "border-e-inherit/90", "border-e-inherit/95", "border-e-inherit/100", - "border-e-inherit/background", "border-e-transparent", "border-e-transparent/0", "border-e-transparent/5", @@ -3357,7 +2811,6 @@ exports[`getClassList 1`] = ` "border-e-transparent/90", "border-e-transparent/95", "border-e-transparent/100", - "border-e-transparent/background", "border-hidden", "border-inherit", "border-inherit/0", @@ -3381,7 +2834,6 @@ exports[`getClassList 1`] = ` "border-inherit/90", "border-inherit/95", "border-inherit/100", - "border-inherit/background", "border-l", "border-l/0", "border-l/5", @@ -3404,7 +2856,6 @@ exports[`getClassList 1`] = ` "border-l/90", "border-l/95", "border-l/100", - "border-l/background", "border-l-0", "border-l-2", "border-l-4", @@ -3431,7 +2882,6 @@ exports[`getClassList 1`] = ` "border-l-current/90", "border-l-current/95", "border-l-current/100", - "border-l-current/background", "border-l-inherit", "border-l-inherit/0", "border-l-inherit/5", @@ -3454,7 +2904,6 @@ exports[`getClassList 1`] = ` "border-l-inherit/90", "border-l-inherit/95", "border-l-inherit/100", - "border-l-inherit/background", "border-l-transparent", "border-l-transparent/0", "border-l-transparent/5", @@ -3477,7 +2926,6 @@ exports[`getClassList 1`] = ` "border-l-transparent/90", "border-l-transparent/95", "border-l-transparent/100", - "border-l-transparent/background", "border-none", "border-r", "border-r/0", @@ -3501,7 +2949,6 @@ exports[`getClassList 1`] = ` "border-r/90", "border-r/95", "border-r/100", - "border-r/background", "border-r-0", "border-r-2", "border-r-4", @@ -3528,7 +2975,6 @@ exports[`getClassList 1`] = ` "border-r-current/90", "border-r-current/95", "border-r-current/100", - "border-r-current/background", "border-r-inherit", "border-r-inherit/0", "border-r-inherit/5", @@ -3551,7 +2997,6 @@ exports[`getClassList 1`] = ` "border-r-inherit/90", "border-r-inherit/95", "border-r-inherit/100", - "border-r-inherit/background", "border-r-transparent", "border-r-transparent/0", "border-r-transparent/5", @@ -3574,7 +3019,6 @@ exports[`getClassList 1`] = ` "border-r-transparent/90", "border-r-transparent/95", "border-r-transparent/100", - "border-r-transparent/background", "border-s", "border-s/0", "border-s/5", @@ -3597,7 +3041,6 @@ exports[`getClassList 1`] = ` "border-s/90", "border-s/95", "border-s/100", - "border-s/background", "border-s-0", "border-s-2", "border-s-4", @@ -3624,7 +3067,6 @@ exports[`getClassList 1`] = ` "border-s-current/90", "border-s-current/95", "border-s-current/100", - "border-s-current/background", "border-s-inherit", "border-s-inherit/0", "border-s-inherit/5", @@ -3647,7 +3089,6 @@ exports[`getClassList 1`] = ` "border-s-inherit/90", "border-s-inherit/95", "border-s-inherit/100", - "border-s-inherit/background", "border-s-transparent", "border-s-transparent/0", "border-s-transparent/5", @@ -3670,7 +3111,6 @@ exports[`getClassList 1`] = ` "border-s-transparent/90", "border-s-transparent/95", "border-s-transparent/100", - "border-s-transparent/background", "border-separate", "border-solid", "border-spacing-0", @@ -3800,7 +3240,6 @@ exports[`getClassList 1`] = ` "border-t/90", "border-t/95", "border-t/100", - "border-t/background", "border-t-0", "border-t-2", "border-t-4", @@ -3827,7 +3266,6 @@ exports[`getClassList 1`] = ` "border-t-current/90", "border-t-current/95", "border-t-current/100", - "border-t-current/background", "border-t-inherit", "border-t-inherit/0", "border-t-inherit/5", @@ -3850,7 +3288,6 @@ exports[`getClassList 1`] = ` "border-t-inherit/90", "border-t-inherit/95", "border-t-inherit/100", - "border-t-inherit/background", "border-t-transparent", "border-t-transparent/0", "border-t-transparent/5", @@ -3873,7 +3310,6 @@ exports[`getClassList 1`] = ` "border-t-transparent/90", "border-t-transparent/95", "border-t-transparent/100", - "border-t-transparent/background", "border-transparent", "border-transparent/0", "border-transparent/5", @@ -3896,7 +3332,6 @@ exports[`getClassList 1`] = ` "border-transparent/90", "border-transparent/95", "border-transparent/100", - "border-transparent/background", "border-x", "border-x/0", "border-x/5", @@ -3919,7 +3354,6 @@ exports[`getClassList 1`] = ` "border-x/90", "border-x/95", "border-x/100", - "border-x/background", "border-x-0", "border-x-2", "border-x-4", @@ -3946,7 +3380,6 @@ exports[`getClassList 1`] = ` "border-x-current/90", "border-x-current/95", "border-x-current/100", - "border-x-current/background", "border-x-inherit", "border-x-inherit/0", "border-x-inherit/5", @@ -3969,7 +3402,6 @@ exports[`getClassList 1`] = ` "border-x-inherit/90", "border-x-inherit/95", "border-x-inherit/100", - "border-x-inherit/background", "border-x-transparent", "border-x-transparent/0", "border-x-transparent/5", @@ -3992,7 +3424,6 @@ exports[`getClassList 1`] = ` "border-x-transparent/90", "border-x-transparent/95", "border-x-transparent/100", - "border-x-transparent/background", "border-y", "border-y/0", "border-y/5", @@ -4015,7 +3446,6 @@ exports[`getClassList 1`] = ` "border-y/90", "border-y/95", "border-y/100", - "border-y/background", "border-y-0", "border-y-2", "border-y-4", @@ -4042,7 +3472,6 @@ exports[`getClassList 1`] = ` "border-y-current/90", "border-y-current/95", "border-y-current/100", - "border-y-current/background", "border-y-inherit", "border-y-inherit/0", "border-y-inherit/5", @@ -4065,7 +3494,6 @@ exports[`getClassList 1`] = ` "border-y-inherit/90", "border-y-inherit/95", "border-y-inherit/100", - "border-y-inherit/background", "border-y-transparent", "border-y-transparent/0", "border-y-transparent/5", @@ -4088,7 +3516,6 @@ exports[`getClassList 1`] = ` "border-y-transparent/90", "border-y-transparent/95", "border-y-transparent/100", - "border-y-transparent/background", "bottom-0", "bottom-0.5", "bottom-1", @@ -4215,7 +3642,6 @@ exports[`getClassList 1`] = ` "caret-current/90", "caret-current/95", "caret-current/100", - "caret-current/background", "caret-inherit", "caret-inherit/0", "caret-inherit/5", @@ -4238,7 +3664,6 @@ exports[`getClassList 1`] = ` "caret-inherit/90", "caret-inherit/95", "caret-inherit/100", - "caret-inherit/background", "caret-transparent", "caret-transparent/0", "caret-transparent/5", @@ -4261,7 +3686,6 @@ exports[`getClassList 1`] = ` "caret-transparent/90", "caret-transparent/95", "caret-transparent/100", - "caret-transparent/background", "clear-both", "clear-end", "clear-left", @@ -4417,7 +3841,6 @@ exports[`getClassList 1`] = ` "decoration-current/90", "decoration-current/95", "decoration-current/100", - "decoration-current/background", "decoration-dashed", "decoration-dotted", "decoration-double", @@ -4444,7 +3867,6 @@ exports[`getClassList 1`] = ` "decoration-inherit/90", "decoration-inherit/95", "decoration-inherit/100", - "decoration-inherit/background", "decoration-solid", "decoration-transparent", "decoration-transparent/0", @@ -4468,7 +3890,6 @@ exports[`getClassList 1`] = ` "decoration-transparent/90", "decoration-transparent/95", "decoration-transparent/100", - "decoration-transparent/background", "decoration-wavy", "delay-75", "delay-100", @@ -4501,7 +3922,6 @@ exports[`getClassList 1`] = ` "divide-current/90", "divide-current/95", "divide-current/100", - "divide-current/background", "divide-dashed", "divide-dotted", "divide-double", @@ -4527,7 +3947,6 @@ exports[`getClassList 1`] = ` "divide-inherit/90", "divide-inherit/95", "divide-inherit/100", - "divide-inherit/background", "divide-none", "divide-solid", "divide-transparent", @@ -4552,7 +3971,6 @@ exports[`getClassList 1`] = ` "divide-transparent/90", "divide-transparent/95", "divide-transparent/100", - "divide-transparent/background", "divide-x", "divide-x-0", "divide-x-2", @@ -4587,7 +4005,6 @@ exports[`getClassList 1`] = ` "drop-shadow-current/90", "drop-shadow-current/95", "drop-shadow-current/100", - "drop-shadow-current/background", "drop-shadow-inherit", "drop-shadow-inherit/0", "drop-shadow-inherit/5", @@ -4610,7 +4027,6 @@ exports[`getClassList 1`] = ` "drop-shadow-inherit/90", "drop-shadow-inherit/95", "drop-shadow-inherit/100", - "drop-shadow-inherit/background", "drop-shadow-none", "drop-shadow-sm", "drop-shadow-transparent", @@ -4635,7 +4051,6 @@ exports[`getClassList 1`] = ` "drop-shadow-transparent/90", "drop-shadow-transparent/95", "drop-shadow-transparent/100", - "drop-shadow-transparent/background", "duration-75", "duration-100", "duration-150", @@ -4647,6 +4062,66 @@ exports[`getClassList 1`] = ` "duration-initial", "ease-initial", "ease-linear", + "end-0", + "end-0.5", + "end-1", + "end-1.5", + "end-2", + "end-2.5", + "end-3", + "end-3.5", + "end-4", + "end-5", + "end-6", + "end-7", + "end-8", + "end-9", + "end-10", + "end-11", + "end-12", + "end-14", + "end-16", + "end-20", + "end-24", + "end-28", + "end-32", + "end-36", + "end-40", + "end-44", + "end-48", + "end-52", + "end-56", + "end-60", + "end-64", + "end-72", + "end-80", + "end-96", + "end-1/2", + "end-1/3", + "end-1/4", + "end-1/5", + "end-1/6", + "end-1/12", + "end-2/3", + "end-2/4", + "end-2/5", + "end-2/6", + "end-2/12", + "end-3/4", + "end-3/5", + "end-3/6", + "end-3/12", + "end-4/5", + "end-4/6", + "end-4/12", + "end-5/6", + "end-5/12", + "end-6/12", + "end-7/12", + "end-8/12", + "end-9/12", + "end-10/12", + "end-11/12", "end-auto", "end-full", "end-px", @@ -4674,7 +4149,6 @@ exports[`getClassList 1`] = ` "fill-current/90", "fill-current/95", "fill-current/100", - "fill-current/background", "fill-inherit", "fill-inherit/0", "fill-inherit/5", @@ -4697,7 +4171,6 @@ exports[`getClassList 1`] = ` "fill-inherit/90", "fill-inherit/95", "fill-inherit/100", - "fill-inherit/background", "fill-none", "fill-transparent", "fill-transparent/0", @@ -4721,7 +4194,6 @@ exports[`getClassList 1`] = ` "fill-transparent/90", "fill-transparent/95", "fill-transparent/100", - "fill-transparent/background", "fixed", "flex", "flex-1", @@ -4845,7 +4317,6 @@ exports[`getClassList 1`] = ` "from-current/90", "from-current/95", "from-current/100", - "from-current/background", "from-inherit", "from-inherit/0", "from-inherit/5", @@ -4868,7 +4339,6 @@ exports[`getClassList 1`] = ` "from-inherit/90", "from-inherit/95", "from-inherit/100", - "from-inherit/background", "from-transparent", "from-transparent/0", "from-transparent/5", @@ -4891,7 +4361,6 @@ exports[`getClassList 1`] = ` "from-transparent/90", "from-transparent/95", "from-transparent/100", - "from-transparent/background", "gap-0", "gap-0.5", "gap-1", @@ -5159,81 +4628,9 @@ exports[`getClassList 1`] = ` "indent-96", "indent-px", "inline", - "inline-0", - "inline-0.5", - "inline-1", - "inline-1.5", - "inline-2", - "inline-2.5", - "inline-3", - "inline-3.5", - "inline-4", - "inline-5", - "inline-6", - "inline-7", - "inline-8", - "inline-9", - "inline-10", - "inline-11", - "inline-12", - "inline-14", - "inline-16", - "inline-20", - "inline-24", - "inline-28", - "inline-32", - "inline-36", - "inline-40", - "inline-44", - "inline-48", - "inline-52", - "inline-56", - "inline-60", - "inline-64", - "inline-72", - "inline-80", - "inline-96", - "inline-auto", "inline-block", - "inline-dvw", - "inline-fit", "inline-flex", - "inline-full", "inline-grid", - "inline-lg", - "inline-lvw", - "inline-max", - "inline-md", - "inline-1/2", - "inline-1/3", - "inline-1/4", - "inline-1/5", - "inline-1/6", - "inline-1/12", - "inline-2/3", - "inline-2/4", - "inline-2/5", - "inline-2/6", - "inline-2/12", - "inline-3/4", - "inline-3/5", - "inline-3/6", - "inline-3/12", - "inline-4/5", - "inline-4/6", - "inline-4/12", - "inline-5/6", - "inline-5/12", - "inline-6/12", - "inline-7/12", - "inline-8/12", - "inline-9/12", - "inline-10/12", - "inline-11/12", - "inline-min", - "inline-px", - "inline-screen", - "inline-svw", "inline-table", "inset-0", "inset-0.5", @@ -5296,195 +4693,6 @@ exports[`getClassList 1`] = ` "inset-10/12", "inset-11/12", "inset-auto", - "inset-be-0", - "inset-be-0.5", - "inset-be-1", - "inset-be-1.5", - "inset-be-2", - "inset-be-2.5", - "inset-be-3", - "inset-be-3.5", - "inset-be-4", - "inset-be-5", - "inset-be-6", - "inset-be-7", - "inset-be-8", - "inset-be-9", - "inset-be-10", - "inset-be-11", - "inset-be-12", - "inset-be-14", - "inset-be-16", - "inset-be-20", - "inset-be-24", - "inset-be-28", - "inset-be-32", - "inset-be-36", - "inset-be-40", - "inset-be-44", - "inset-be-48", - "inset-be-52", - "inset-be-56", - "inset-be-60", - "inset-be-64", - "inset-be-72", - "inset-be-80", - "inset-be-96", - "inset-be-1/2", - "inset-be-1/3", - "inset-be-1/4", - "inset-be-1/5", - "inset-be-1/6", - "inset-be-1/12", - "inset-be-2/3", - "inset-be-2/4", - "inset-be-2/5", - "inset-be-2/6", - "inset-be-2/12", - "inset-be-3/4", - "inset-be-3/5", - "inset-be-3/6", - "inset-be-3/12", - "inset-be-4/5", - "inset-be-4/6", - "inset-be-4/12", - "inset-be-5/6", - "inset-be-5/12", - "inset-be-6/12", - "inset-be-7/12", - "inset-be-8/12", - "inset-be-9/12", - "inset-be-10/12", - "inset-be-11/12", - "inset-be-auto", - "inset-be-full", - "inset-be-px", - "inset-bs-0", - "inset-bs-0.5", - "inset-bs-1", - "inset-bs-1.5", - "inset-bs-2", - "inset-bs-2.5", - "inset-bs-3", - "inset-bs-3.5", - "inset-bs-4", - "inset-bs-5", - "inset-bs-6", - "inset-bs-7", - "inset-bs-8", - "inset-bs-9", - "inset-bs-10", - "inset-bs-11", - "inset-bs-12", - "inset-bs-14", - "inset-bs-16", - "inset-bs-20", - "inset-bs-24", - "inset-bs-28", - "inset-bs-32", - "inset-bs-36", - "inset-bs-40", - "inset-bs-44", - "inset-bs-48", - "inset-bs-52", - "inset-bs-56", - "inset-bs-60", - "inset-bs-64", - "inset-bs-72", - "inset-bs-80", - "inset-bs-96", - "inset-bs-1/2", - "inset-bs-1/3", - "inset-bs-1/4", - "inset-bs-1/5", - "inset-bs-1/6", - "inset-bs-1/12", - "inset-bs-2/3", - "inset-bs-2/4", - "inset-bs-2/5", - "inset-bs-2/6", - "inset-bs-2/12", - "inset-bs-3/4", - "inset-bs-3/5", - "inset-bs-3/6", - "inset-bs-3/12", - "inset-bs-4/5", - "inset-bs-4/6", - "inset-bs-4/12", - "inset-bs-5/6", - "inset-bs-5/12", - "inset-bs-6/12", - "inset-bs-7/12", - "inset-bs-8/12", - "inset-bs-9/12", - "inset-bs-10/12", - "inset-bs-11/12", - "inset-bs-auto", - "inset-bs-full", - "inset-bs-px", - "inset-e-0", - "inset-e-0.5", - "inset-e-1", - "inset-e-1.5", - "inset-e-2", - "inset-e-2.5", - "inset-e-3", - "inset-e-3.5", - "inset-e-4", - "inset-e-5", - "inset-e-6", - "inset-e-7", - "inset-e-8", - "inset-e-9", - "inset-e-10", - "inset-e-11", - "inset-e-12", - "inset-e-14", - "inset-e-16", - "inset-e-20", - "inset-e-24", - "inset-e-28", - "inset-e-32", - "inset-e-36", - "inset-e-40", - "inset-e-44", - "inset-e-48", - "inset-e-52", - "inset-e-56", - "inset-e-60", - "inset-e-64", - "inset-e-72", - "inset-e-80", - "inset-e-96", - "inset-e-1/2", - "inset-e-1/3", - "inset-e-1/4", - "inset-e-1/5", - "inset-e-1/6", - "inset-e-1/12", - "inset-e-2/3", - "inset-e-2/4", - "inset-e-2/5", - "inset-e-2/6", - "inset-e-2/12", - "inset-e-3/4", - "inset-e-3/5", - "inset-e-3/6", - "inset-e-3/12", - "inset-e-4/5", - "inset-e-4/6", - "inset-e-4/12", - "inset-e-5/6", - "inset-e-5/12", - "inset-e-6/12", - "inset-e-7/12", - "inset-e-8/12", - "inset-e-9/12", - "inset-e-10/12", - "inset-e-11/12", - "inset-e-auto", - "inset-e-full", - "inset-e-px", "inset-full", "inset-px", "inset-ring", @@ -5515,7 +4723,6 @@ exports[`getClassList 1`] = ` "inset-ring-current/90", "inset-ring-current/95", "inset-ring-current/100", - "inset-ring-current/background", "inset-ring-inherit", "inset-ring-inherit/0", "inset-ring-inherit/5", @@ -5538,7 +4745,6 @@ exports[`getClassList 1`] = ` "inset-ring-inherit/90", "inset-ring-inherit/95", "inset-ring-inherit/100", - "inset-ring-inherit/background", "inset-ring-transparent", "inset-ring-transparent/0", "inset-ring-transparent/5", @@ -5561,70 +4767,6 @@ exports[`getClassList 1`] = ` "inset-ring-transparent/90", "inset-ring-transparent/95", "inset-ring-transparent/100", - "inset-ring-transparent/background", - "inset-s-0", - "inset-s-0.5", - "inset-s-1", - "inset-s-1.5", - "inset-s-2", - "inset-s-2.5", - "inset-s-3", - "inset-s-3.5", - "inset-s-4", - "inset-s-5", - "inset-s-6", - "inset-s-7", - "inset-s-8", - "inset-s-9", - "inset-s-10", - "inset-s-11", - "inset-s-12", - "inset-s-14", - "inset-s-16", - "inset-s-20", - "inset-s-24", - "inset-s-28", - "inset-s-32", - "inset-s-36", - "inset-s-40", - "inset-s-44", - "inset-s-48", - "inset-s-52", - "inset-s-56", - "inset-s-60", - "inset-s-64", - "inset-s-72", - "inset-s-80", - "inset-s-96", - "inset-s-1/2", - "inset-s-1/3", - "inset-s-1/4", - "inset-s-1/5", - "inset-s-1/6", - "inset-s-1/12", - "inset-s-2/3", - "inset-s-2/4", - "inset-s-2/5", - "inset-s-2/6", - "inset-s-2/12", - "inset-s-3/4", - "inset-s-3/5", - "inset-s-3/6", - "inset-s-3/12", - "inset-s-4/5", - "inset-s-4/6", - "inset-s-4/12", - "inset-s-5/6", - "inset-s-5/12", - "inset-s-6/12", - "inset-s-7/12", - "inset-s-8/12", - "inset-s-9/12", - "inset-s-10/12", - "inset-s-11/12", - "inset-s-auto", - "inset-s-full", - "inset-s-px", "inset-shadow-current", "inset-shadow-current/0", "inset-shadow-current/5", @@ -5647,7 +4789,6 @@ exports[`getClassList 1`] = ` "inset-shadow-current/90", "inset-shadow-current/95", "inset-shadow-current/100", - "inset-shadow-current/background", "inset-shadow-inherit", "inset-shadow-inherit/0", "inset-shadow-inherit/5", @@ -5670,7 +4811,6 @@ exports[`getClassList 1`] = ` "inset-shadow-inherit/90", "inset-shadow-inherit/95", "inset-shadow-inherit/100", - "inset-shadow-inherit/background", "inset-shadow-initial", "inset-shadow-none", "inset-shadow-sm", @@ -5717,7 +4857,6 @@ exports[`getClassList 1`] = ` "inset-shadow-transparent/90", "inset-shadow-transparent/95", "inset-shadow-transparent/100", - "inset-shadow-transparent/background", "inset-x-0", "inset-x-0.5", "inset-x-1", @@ -6119,7 +5258,6 @@ exports[`getClassList 1`] = ` "mask-b-from-current/90", "mask-b-from-current/95", "mask-b-from-current/100", - "mask-b-from-current/background", "mask-b-from-inherit", "mask-b-from-inherit/0", "mask-b-from-inherit/5", @@ -6142,7 +5280,6 @@ exports[`getClassList 1`] = ` "mask-b-from-inherit/90", "mask-b-from-inherit/95", "mask-b-from-inherit/100", - "mask-b-from-inherit/background", "mask-b-from-transparent", "mask-b-from-transparent/0", "mask-b-from-transparent/5", @@ -6165,7 +5302,6 @@ exports[`getClassList 1`] = ` "mask-b-from-transparent/90", "mask-b-from-transparent/95", "mask-b-from-transparent/100", - "mask-b-from-transparent/background", "mask-b-to-0", "mask-b-to-0%", "mask-b-to-0.5", @@ -6243,7 +5379,6 @@ exports[`getClassList 1`] = ` "mask-b-to-current/90", "mask-b-to-current/95", "mask-b-to-current/100", - "mask-b-to-current/background", "mask-b-to-inherit", "mask-b-to-inherit/0", "mask-b-to-inherit/5", @@ -6266,7 +5401,6 @@ exports[`getClassList 1`] = ` "mask-b-to-inherit/90", "mask-b-to-inherit/95", "mask-b-to-inherit/100", - "mask-b-to-inherit/background", "mask-b-to-transparent", "mask-b-to-transparent/0", "mask-b-to-transparent/5", @@ -6289,7 +5423,6 @@ exports[`getClassList 1`] = ` "mask-b-to-transparent/90", "mask-b-to-transparent/95", "mask-b-to-transparent/100", - "mask-b-to-transparent/background", "mask-bottom", "mask-bottom-left", "mask-bottom-right", @@ -6387,7 +5520,6 @@ exports[`getClassList 1`] = ` "mask-conic-from-current/90", "mask-conic-from-current/95", "mask-conic-from-current/100", - "mask-conic-from-current/background", "mask-conic-from-inherit", "mask-conic-from-inherit/0", "mask-conic-from-inherit/5", @@ -6410,7 +5542,6 @@ exports[`getClassList 1`] = ` "mask-conic-from-inherit/90", "mask-conic-from-inherit/95", "mask-conic-from-inherit/100", - "mask-conic-from-inherit/background", "mask-conic-from-transparent", "mask-conic-from-transparent/0", "mask-conic-from-transparent/5", @@ -6433,7 +5564,6 @@ exports[`getClassList 1`] = ` "mask-conic-from-transparent/90", "mask-conic-from-transparent/95", "mask-conic-from-transparent/100", - "mask-conic-from-transparent/background", "mask-conic-to-0", "mask-conic-to-0%", "mask-conic-to-0.5", @@ -6511,7 +5641,6 @@ exports[`getClassList 1`] = ` "mask-conic-to-current/90", "mask-conic-to-current/95", "mask-conic-to-current/100", - "mask-conic-to-current/background", "mask-conic-to-inherit", "mask-conic-to-inherit/0", "mask-conic-to-inherit/5", @@ -6534,7 +5663,6 @@ exports[`getClassList 1`] = ` "mask-conic-to-inherit/90", "mask-conic-to-inherit/95", "mask-conic-to-inherit/100", - "mask-conic-to-inherit/background", "mask-conic-to-transparent", "mask-conic-to-transparent/0", "mask-conic-to-transparent/5", @@ -6557,7 +5685,6 @@ exports[`getClassList 1`] = ` "mask-conic-to-transparent/90", "mask-conic-to-transparent/95", "mask-conic-to-transparent/100", - "mask-conic-to-transparent/background", "mask-contain", "mask-cover", "mask-ellipse", @@ -6640,7 +5767,6 @@ exports[`getClassList 1`] = ` "mask-l-from-current/90", "mask-l-from-current/95", "mask-l-from-current/100", - "mask-l-from-current/background", "mask-l-from-inherit", "mask-l-from-inherit/0", "mask-l-from-inherit/5", @@ -6663,7 +5789,6 @@ exports[`getClassList 1`] = ` "mask-l-from-inherit/90", "mask-l-from-inherit/95", "mask-l-from-inherit/100", - "mask-l-from-inherit/background", "mask-l-from-transparent", "mask-l-from-transparent/0", "mask-l-from-transparent/5", @@ -6686,7 +5811,6 @@ exports[`getClassList 1`] = ` "mask-l-from-transparent/90", "mask-l-from-transparent/95", "mask-l-from-transparent/100", - "mask-l-from-transparent/background", "mask-l-to-0", "mask-l-to-0%", "mask-l-to-0.5", @@ -6764,7 +5888,6 @@ exports[`getClassList 1`] = ` "mask-l-to-current/90", "mask-l-to-current/95", "mask-l-to-current/100", - "mask-l-to-current/background", "mask-l-to-inherit", "mask-l-to-inherit/0", "mask-l-to-inherit/5", @@ -6787,7 +5910,6 @@ exports[`getClassList 1`] = ` "mask-l-to-inherit/90", "mask-l-to-inherit/95", "mask-l-to-inherit/100", - "mask-l-to-inherit/background", "mask-l-to-transparent", "mask-l-to-transparent/0", "mask-l-to-transparent/5", @@ -6810,7 +5932,6 @@ exports[`getClassList 1`] = ` "mask-l-to-transparent/90", "mask-l-to-transparent/95", "mask-l-to-transparent/100", - "mask-l-to-transparent/background", "mask-left", "mask-linear-0", "mask-linear-1", @@ -6898,7 +6019,6 @@ exports[`getClassList 1`] = ` "mask-linear-from-current/90", "mask-linear-from-current/95", "mask-linear-from-current/100", - "mask-linear-from-current/background", "mask-linear-from-inherit", "mask-linear-from-inherit/0", "mask-linear-from-inherit/5", @@ -6921,7 +6041,6 @@ exports[`getClassList 1`] = ` "mask-linear-from-inherit/90", "mask-linear-from-inherit/95", "mask-linear-from-inherit/100", - "mask-linear-from-inherit/background", "mask-linear-from-transparent", "mask-linear-from-transparent/0", "mask-linear-from-transparent/5", @@ -6944,7 +6063,6 @@ exports[`getClassList 1`] = ` "mask-linear-from-transparent/90", "mask-linear-from-transparent/95", "mask-linear-from-transparent/100", - "mask-linear-from-transparent/background", "mask-linear-to-0", "mask-linear-to-0%", "mask-linear-to-0.5", @@ -7022,7 +6140,6 @@ exports[`getClassList 1`] = ` "mask-linear-to-current/90", "mask-linear-to-current/95", "mask-linear-to-current/100", - "mask-linear-to-current/background", "mask-linear-to-inherit", "mask-linear-to-inherit/0", "mask-linear-to-inherit/5", @@ -7045,7 +6162,6 @@ exports[`getClassList 1`] = ` "mask-linear-to-inherit/90", "mask-linear-to-inherit/95", "mask-linear-to-inherit/100", - "mask-linear-to-inherit/background", "mask-linear-to-transparent", "mask-linear-to-transparent/0", "mask-linear-to-transparent/5", @@ -7068,7 +6184,6 @@ exports[`getClassList 1`] = ` "mask-linear-to-transparent/90", "mask-linear-to-transparent/95", "mask-linear-to-transparent/100", - "mask-linear-to-transparent/background", "mask-luminance", "mask-match", "mask-no-clip", @@ -7157,7 +6272,6 @@ exports[`getClassList 1`] = ` "mask-r-from-current/90", "mask-r-from-current/95", "mask-r-from-current/100", - "mask-r-from-current/background", "mask-r-from-inherit", "mask-r-from-inherit/0", "mask-r-from-inherit/5", @@ -7180,7 +6294,6 @@ exports[`getClassList 1`] = ` "mask-r-from-inherit/90", "mask-r-from-inherit/95", "mask-r-from-inherit/100", - "mask-r-from-inherit/background", "mask-r-from-transparent", "mask-r-from-transparent/0", "mask-r-from-transparent/5", @@ -7203,7 +6316,6 @@ exports[`getClassList 1`] = ` "mask-r-from-transparent/90", "mask-r-from-transparent/95", "mask-r-from-transparent/100", - "mask-r-from-transparent/background", "mask-r-to-0", "mask-r-to-0%", "mask-r-to-0.5", @@ -7281,7 +6393,6 @@ exports[`getClassList 1`] = ` "mask-r-to-current/90", "mask-r-to-current/95", "mask-r-to-current/100", - "mask-r-to-current/background", "mask-r-to-inherit", "mask-r-to-inherit/0", "mask-r-to-inherit/5", @@ -7304,7 +6415,6 @@ exports[`getClassList 1`] = ` "mask-r-to-inherit/90", "mask-r-to-inherit/95", "mask-r-to-inherit/100", - "mask-r-to-inherit/background", "mask-r-to-transparent", "mask-r-to-transparent/0", "mask-r-to-transparent/5", @@ -7327,7 +6437,6 @@ exports[`getClassList 1`] = ` "mask-r-to-transparent/90", "mask-r-to-transparent/95", "mask-r-to-transparent/100", - "mask-r-to-transparent/background", "mask-radial-at-bottom", "mask-radial-at-bottom-left", "mask-radial-at-bottom-right", @@ -7418,7 +6527,6 @@ exports[`getClassList 1`] = ` "mask-radial-from-current/90", "mask-radial-from-current/95", "mask-radial-from-current/100", - "mask-radial-from-current/background", "mask-radial-from-inherit", "mask-radial-from-inherit/0", "mask-radial-from-inherit/5", @@ -7441,7 +6549,6 @@ exports[`getClassList 1`] = ` "mask-radial-from-inherit/90", "mask-radial-from-inherit/95", "mask-radial-from-inherit/100", - "mask-radial-from-inherit/background", "mask-radial-from-transparent", "mask-radial-from-transparent/0", "mask-radial-from-transparent/5", @@ -7464,7 +6571,6 @@ exports[`getClassList 1`] = ` "mask-radial-from-transparent/90", "mask-radial-from-transparent/95", "mask-radial-from-transparent/100", - "mask-radial-from-transparent/background", "mask-radial-to-0", "mask-radial-to-0%", "mask-radial-to-0.5", @@ -7542,7 +6648,6 @@ exports[`getClassList 1`] = ` "mask-radial-to-current/90", "mask-radial-to-current/95", "mask-radial-to-current/100", - "mask-radial-to-current/background", "mask-radial-to-inherit", "mask-radial-to-inherit/0", "mask-radial-to-inherit/5", @@ -7565,7 +6670,6 @@ exports[`getClassList 1`] = ` "mask-radial-to-inherit/90", "mask-radial-to-inherit/95", "mask-radial-to-inherit/100", - "mask-radial-to-inherit/background", "mask-radial-to-transparent", "mask-radial-to-transparent/0", "mask-radial-to-transparent/5", @@ -7588,7 +6692,6 @@ exports[`getClassList 1`] = ` "mask-radial-to-transparent/90", "mask-radial-to-transparent/95", "mask-radial-to-transparent/100", - "mask-radial-to-transparent/background", "mask-repeat", "mask-repeat-round", "mask-repeat-space", @@ -7673,7 +6776,6 @@ exports[`getClassList 1`] = ` "mask-t-from-current/90", "mask-t-from-current/95", "mask-t-from-current/100", - "mask-t-from-current/background", "mask-t-from-inherit", "mask-t-from-inherit/0", "mask-t-from-inherit/5", @@ -7696,7 +6798,6 @@ exports[`getClassList 1`] = ` "mask-t-from-inherit/90", "mask-t-from-inherit/95", "mask-t-from-inherit/100", - "mask-t-from-inherit/background", "mask-t-from-transparent", "mask-t-from-transparent/0", "mask-t-from-transparent/5", @@ -7719,7 +6820,6 @@ exports[`getClassList 1`] = ` "mask-t-from-transparent/90", "mask-t-from-transparent/95", "mask-t-from-transparent/100", - "mask-t-from-transparent/background", "mask-t-to-0", "mask-t-to-0%", "mask-t-to-0.5", @@ -7797,7 +6897,6 @@ exports[`getClassList 1`] = ` "mask-t-to-current/90", "mask-t-to-current/95", "mask-t-to-current/100", - "mask-t-to-current/background", "mask-t-to-inherit", "mask-t-to-inherit/0", "mask-t-to-inherit/5", @@ -7820,7 +6919,6 @@ exports[`getClassList 1`] = ` "mask-t-to-inherit/90", "mask-t-to-inherit/95", "mask-t-to-inherit/100", - "mask-t-to-inherit/background", "mask-t-to-transparent", "mask-t-to-transparent/0", "mask-t-to-transparent/5", @@ -7843,7 +6941,6 @@ exports[`getClassList 1`] = ` "mask-t-to-transparent/90", "mask-t-to-transparent/95", "mask-t-to-transparent/100", - "mask-t-to-transparent/background", "mask-top", "mask-top-left", "mask-top-right", @@ -7926,7 +7023,6 @@ exports[`getClassList 1`] = ` "mask-x-from-current/90", "mask-x-from-current/95", "mask-x-from-current/100", - "mask-x-from-current/background", "mask-x-from-inherit", "mask-x-from-inherit/0", "mask-x-from-inherit/5", @@ -7949,7 +7045,6 @@ exports[`getClassList 1`] = ` "mask-x-from-inherit/90", "mask-x-from-inherit/95", "mask-x-from-inherit/100", - "mask-x-from-inherit/background", "mask-x-from-transparent", "mask-x-from-transparent/0", "mask-x-from-transparent/5", @@ -7972,7 +7067,6 @@ exports[`getClassList 1`] = ` "mask-x-from-transparent/90", "mask-x-from-transparent/95", "mask-x-from-transparent/100", - "mask-x-from-transparent/background", "mask-x-to-0", "mask-x-to-0%", "mask-x-to-0.5", @@ -8050,7 +7144,6 @@ exports[`getClassList 1`] = ` "mask-x-to-current/90", "mask-x-to-current/95", "mask-x-to-current/100", - "mask-x-to-current/background", "mask-x-to-inherit", "mask-x-to-inherit/0", "mask-x-to-inherit/5", @@ -8073,7 +7166,6 @@ exports[`getClassList 1`] = ` "mask-x-to-inherit/90", "mask-x-to-inherit/95", "mask-x-to-inherit/100", - "mask-x-to-inherit/background", "mask-x-to-transparent", "mask-x-to-transparent/0", "mask-x-to-transparent/5", @@ -8096,7 +7188,6 @@ exports[`getClassList 1`] = ` "mask-x-to-transparent/90", "mask-x-to-transparent/95", "mask-x-to-transparent/100", - "mask-x-to-transparent/background", "mask-y-from-0", "mask-y-from-0%", "mask-y-from-0.5", @@ -8174,7 +7265,6 @@ exports[`getClassList 1`] = ` "mask-y-from-current/90", "mask-y-from-current/95", "mask-y-from-current/100", - "mask-y-from-current/background", "mask-y-from-inherit", "mask-y-from-inherit/0", "mask-y-from-inherit/5", @@ -8197,7 +7287,6 @@ exports[`getClassList 1`] = ` "mask-y-from-inherit/90", "mask-y-from-inherit/95", "mask-y-from-inherit/100", - "mask-y-from-inherit/background", "mask-y-from-transparent", "mask-y-from-transparent/0", "mask-y-from-transparent/5", @@ -8220,7 +7309,6 @@ exports[`getClassList 1`] = ` "mask-y-from-transparent/90", "mask-y-from-transparent/95", "mask-y-from-transparent/100", - "mask-y-from-transparent/background", "mask-y-to-0", "mask-y-to-0%", "mask-y-to-0.5", @@ -8298,7 +7386,6 @@ exports[`getClassList 1`] = ` "mask-y-to-current/90", "mask-y-to-current/95", "mask-y-to-current/100", - "mask-y-to-current/background", "mask-y-to-inherit", "mask-y-to-inherit/0", "mask-y-to-inherit/5", @@ -8321,7 +7408,6 @@ exports[`getClassList 1`] = ` "mask-y-to-inherit/90", "mask-y-to-inherit/95", "mask-y-to-inherit/100", - "mask-y-to-inherit/background", "mask-y-to-transparent", "mask-y-to-transparent/0", "mask-y-to-transparent/5", @@ -8344,78 +7430,6 @@ exports[`getClassList 1`] = ` "mask-y-to-transparent/90", "mask-y-to-transparent/95", "mask-y-to-transparent/100", - "mask-y-to-transparent/background", - "max-block-0", - "max-block-0.5", - "max-block-1", - "max-block-1.5", - "max-block-2", - "max-block-2.5", - "max-block-3", - "max-block-3.5", - "max-block-4", - "max-block-5", - "max-block-6", - "max-block-7", - "max-block-8", - "max-block-9", - "max-block-10", - "max-block-11", - "max-block-12", - "max-block-14", - "max-block-16", - "max-block-20", - "max-block-24", - "max-block-28", - "max-block-32", - "max-block-36", - "max-block-40", - "max-block-44", - "max-block-48", - "max-block-52", - "max-block-56", - "max-block-60", - "max-block-64", - "max-block-72", - "max-block-80", - "max-block-96", - "max-block-1/2", - "max-block-1/3", - "max-block-1/4", - "max-block-1/5", - "max-block-1/6", - "max-block-1/12", - "max-block-2/3", - "max-block-2/4", - "max-block-2/5", - "max-block-2/6", - "max-block-2/12", - "max-block-3/4", - "max-block-3/5", - "max-block-3/6", - "max-block-3/12", - "max-block-4/5", - "max-block-4/6", - "max-block-4/12", - "max-block-5/6", - "max-block-5/12", - "max-block-6/12", - "max-block-7/12", - "max-block-8/12", - "max-block-9/12", - "max-block-10/12", - "max-block-11/12", - "max-block-dvh", - "max-block-fit", - "max-block-full", - "max-block-lh", - "max-block-lvh", - "max-block-max", - "max-block-min", - "max-block-none", - "max-block-px", - "max-block-screen", - "max-block-svh", "max-h-0", "max-h-0.5", "max-h-1", @@ -8490,78 +7504,6 @@ exports[`getClassList 1`] = ` "max-h-screen", "max-h-svh", "max-h-svw", - "max-inline-0", - "max-inline-0.5", - "max-inline-1", - "max-inline-1.5", - "max-inline-2", - "max-inline-2.5", - "max-inline-3", - "max-inline-3.5", - "max-inline-4", - "max-inline-5", - "max-inline-6", - "max-inline-7", - "max-inline-8", - "max-inline-9", - "max-inline-10", - "max-inline-11", - "max-inline-12", - "max-inline-14", - "max-inline-16", - "max-inline-20", - "max-inline-24", - "max-inline-28", - "max-inline-32", - "max-inline-36", - "max-inline-40", - "max-inline-44", - "max-inline-48", - "max-inline-52", - "max-inline-56", - "max-inline-60", - "max-inline-64", - "max-inline-72", - "max-inline-80", - "max-inline-96", - "max-inline-dvw", - "max-inline-fit", - "max-inline-full", - "max-inline-lg", - "max-inline-lvw", - "max-inline-max", - "max-inline-md", - "max-inline-1/2", - "max-inline-1/3", - "max-inline-1/4", - "max-inline-1/5", - "max-inline-1/6", - "max-inline-1/12", - "max-inline-2/3", - "max-inline-2/4", - "max-inline-2/5", - "max-inline-2/6", - "max-inline-2/12", - "max-inline-3/4", - "max-inline-3/5", - "max-inline-3/6", - "max-inline-3/12", - "max-inline-4/5", - "max-inline-4/6", - "max-inline-4/12", - "max-inline-5/6", - "max-inline-5/12", - "max-inline-6/12", - "max-inline-7/12", - "max-inline-8/12", - "max-inline-9/12", - "max-inline-10/12", - "max-inline-11/12", - "max-inline-min", - "max-inline-none", - "max-inline-px", - "max-inline-screen", - "max-inline-svw", "max-w-0", "max-w-0.5", "max-w-1", @@ -8672,78 +7614,6 @@ exports[`getClassList 1`] = ` "mb-96", "mb-auto", "mb-px", - "mbe-0", - "mbe-0.5", - "mbe-1", - "mbe-1.5", - "mbe-2", - "mbe-2.5", - "mbe-3", - "mbe-3.5", - "mbe-4", - "mbe-5", - "mbe-6", - "mbe-7", - "mbe-8", - "mbe-9", - "mbe-10", - "mbe-11", - "mbe-12", - "mbe-14", - "mbe-16", - "mbe-20", - "mbe-24", - "mbe-28", - "mbe-32", - "mbe-36", - "mbe-40", - "mbe-44", - "mbe-48", - "mbe-52", - "mbe-56", - "mbe-60", - "mbe-64", - "mbe-72", - "mbe-80", - "mbe-96", - "mbe-auto", - "mbe-px", - "mbs-0", - "mbs-0.5", - "mbs-1", - "mbs-1.5", - "mbs-2", - "mbs-2.5", - "mbs-3", - "mbs-3.5", - "mbs-4", - "mbs-5", - "mbs-6", - "mbs-7", - "mbs-8", - "mbs-9", - "mbs-10", - "mbs-11", - "mbs-12", - "mbs-14", - "mbs-16", - "mbs-20", - "mbs-24", - "mbs-28", - "mbs-32", - "mbs-36", - "mbs-40", - "mbs-44", - "mbs-48", - "mbs-52", - "mbs-56", - "mbs-60", - "mbs-64", - "mbs-72", - "mbs-80", - "mbs-96", - "mbs-auto", - "mbs-px", "me-0", "me-0.5", "me-1", @@ -8780,77 +7650,6 @@ exports[`getClassList 1`] = ` "me-96", "me-auto", "me-px", - "min-block-0", - "min-block-0.5", - "min-block-1", - "min-block-1.5", - "min-block-2", - "min-block-2.5", - "min-block-3", - "min-block-3.5", - "min-block-4", - "min-block-5", - "min-block-6", - "min-block-7", - "min-block-8", - "min-block-9", - "min-block-10", - "min-block-11", - "min-block-12", - "min-block-14", - "min-block-16", - "min-block-20", - "min-block-24", - "min-block-28", - "min-block-32", - "min-block-36", - "min-block-40", - "min-block-44", - "min-block-48", - "min-block-52", - "min-block-56", - "min-block-60", - "min-block-64", - "min-block-72", - "min-block-80", - "min-block-96", - "min-block-1/2", - "min-block-1/3", - "min-block-1/4", - "min-block-1/5", - "min-block-1/6", - "min-block-1/12", - "min-block-2/3", - "min-block-2/4", - "min-block-2/5", - "min-block-2/6", - "min-block-2/12", - "min-block-3/4", - "min-block-3/5", - "min-block-3/6", - "min-block-3/12", - "min-block-4/5", - "min-block-4/6", - "min-block-4/12", - "min-block-5/6", - "min-block-5/12", - "min-block-6/12", - "min-block-7/12", - "min-block-8/12", - "min-block-9/12", - "min-block-10/12", - "min-block-11/12", - "min-block-auto", - "min-block-dvh", - "min-block-fit", - "min-block-full", - "min-block-lh", - "min-block-lvh", - "min-block-max", - "min-block-min", - "min-block-px", - "min-block-screen", - "min-block-svh", "min-h-0", "min-h-0.5", "min-h-1", @@ -8925,78 +7724,6 @@ exports[`getClassList 1`] = ` "min-h-screen", "min-h-svh", "min-h-svw", - "min-inline-0", - "min-inline-0.5", - "min-inline-1", - "min-inline-1.5", - "min-inline-2", - "min-inline-2.5", - "min-inline-3", - "min-inline-3.5", - "min-inline-4", - "min-inline-5", - "min-inline-6", - "min-inline-7", - "min-inline-8", - "min-inline-9", - "min-inline-10", - "min-inline-11", - "min-inline-12", - "min-inline-14", - "min-inline-16", - "min-inline-20", - "min-inline-24", - "min-inline-28", - "min-inline-32", - "min-inline-36", - "min-inline-40", - "min-inline-44", - "min-inline-48", - "min-inline-52", - "min-inline-56", - "min-inline-60", - "min-inline-64", - "min-inline-72", - "min-inline-80", - "min-inline-96", - "min-inline-auto", - "min-inline-dvw", - "min-inline-fit", - "min-inline-full", - "min-inline-lg", - "min-inline-lvw", - "min-inline-max", - "min-inline-md", - "min-inline-1/2", - "min-inline-1/3", - "min-inline-1/4", - "min-inline-1/5", - "min-inline-1/6", - "min-inline-1/12", - "min-inline-2/3", - "min-inline-2/4", - "min-inline-2/5", - "min-inline-2/6", - "min-inline-2/12", - "min-inline-3/4", - "min-inline-3/5", - "min-inline-3/6", - "min-inline-3/12", - "min-inline-4/5", - "min-inline-4/6", - "min-inline-4/12", - "min-inline-5/6", - "min-inline-5/12", - "min-inline-6/12", - "min-inline-7/12", - "min-inline-8/12", - "min-inline-9/12", - "min-inline-10/12", - "min-inline-11/12", - "min-inline-min", - "min-inline-px", - "min-inline-screen", - "min-inline-svw", "min-w-0", "min-w-0.5", "min-w-1", @@ -9394,7 +8121,6 @@ exports[`getClassList 1`] = ` "outline/90", "outline/95", "outline/100", - "outline/background", "outline-0", "outline-1", "outline-2", @@ -9422,7 +8148,6 @@ exports[`getClassList 1`] = ` "outline-current/90", "outline-current/95", "outline-current/100", - "outline-current/background", "outline-dashed", "outline-dotted", "outline-double", @@ -9449,7 +8174,6 @@ exports[`getClassList 1`] = ` "outline-inherit/90", "outline-inherit/95", "outline-inherit/100", - "outline-inherit/background", "outline-none", "outline-offset-0", "outline-offset-1", @@ -9479,7 +8203,6 @@ exports[`getClassList 1`] = ` "outline-transparent/90", "outline-transparent/95", "outline-transparent/100", - "outline-transparent/background", "overflow-auto", "overflow-clip", "overflow-hidden", @@ -9575,76 +8298,6 @@ exports[`getClassList 1`] = ` "pb-80", "pb-96", "pb-px", - "pbe-0", - "pbe-0.5", - "pbe-1", - "pbe-1.5", - "pbe-2", - "pbe-2.5", - "pbe-3", - "pbe-3.5", - "pbe-4", - "pbe-5", - "pbe-6", - "pbe-7", - "pbe-8", - "pbe-9", - "pbe-10", - "pbe-11", - "pbe-12", - "pbe-14", - "pbe-16", - "pbe-20", - "pbe-24", - "pbe-28", - "pbe-32", - "pbe-36", - "pbe-40", - "pbe-44", - "pbe-48", - "pbe-52", - "pbe-56", - "pbe-60", - "pbe-64", - "pbe-72", - "pbe-80", - "pbe-96", - "pbe-px", - "pbs-0", - "pbs-0.5", - "pbs-1", - "pbs-1.5", - "pbs-2", - "pbs-2.5", - "pbs-3", - "pbs-3.5", - "pbs-4", - "pbs-5", - "pbs-6", - "pbs-7", - "pbs-8", - "pbs-9", - "pbs-10", - "pbs-11", - "pbs-12", - "pbs-14", - "pbs-16", - "pbs-20", - "pbs-24", - "pbs-28", - "pbs-32", - "pbs-36", - "pbs-40", - "pbs-44", - "pbs-48", - "pbs-52", - "pbs-56", - "pbs-60", - "pbs-64", - "pbs-72", - "pbs-80", - "pbs-96", - "pbs-px", "pe-0", "pe-0.5", "pe-1", @@ -9773,7 +8426,6 @@ exports[`getClassList 1`] = ` "placeholder-current/90", "placeholder-current/95", "placeholder-current/100", - "placeholder-current/background", "placeholder-inherit", "placeholder-inherit/0", "placeholder-inherit/5", @@ -9796,7 +8448,6 @@ exports[`getClassList 1`] = ` "placeholder-inherit/90", "placeholder-inherit/95", "placeholder-inherit/100", - "placeholder-inherit/background", "placeholder-transparent", "placeholder-transparent/0", "placeholder-transparent/5", @@ -9819,7 +8470,6 @@ exports[`getClassList 1`] = ` "placeholder-transparent/90", "placeholder-transparent/95", "placeholder-transparent/100", - "placeholder-transparent/background", "pointer-events-auto", "pointer-events-none", "pr-0", @@ -10094,7 +8744,6 @@ exports[`getClassList 1`] = ` "ring-current/90", "ring-current/95", "ring-current/100", - "ring-current/background", "ring-inherit", "ring-inherit/0", "ring-inherit/5", @@ -10117,7 +8766,6 @@ exports[`getClassList 1`] = ` "ring-inherit/90", "ring-inherit/95", "ring-inherit/100", - "ring-inherit/background", "ring-inset", "ring-offset-0", "ring-offset-1", @@ -10146,7 +8794,6 @@ exports[`getClassList 1`] = ` "ring-offset-current/90", "ring-offset-current/95", "ring-offset-current/100", - "ring-offset-current/background", "ring-offset-inherit", "ring-offset-inherit/0", "ring-offset-inherit/5", @@ -10169,7 +8816,6 @@ exports[`getClassList 1`] = ` "ring-offset-inherit/90", "ring-offset-inherit/95", "ring-offset-inherit/100", - "ring-offset-inherit/background", "ring-offset-transparent", "ring-offset-transparent/0", "ring-offset-transparent/5", @@ -10192,7 +8838,6 @@ exports[`getClassList 1`] = ` "ring-offset-transparent/90", "ring-offset-transparent/95", "ring-offset-transparent/100", - "ring-offset-transparent/background", "ring-transparent", "ring-transparent/0", "ring-transparent/5", @@ -10215,7 +8860,6 @@ exports[`getClassList 1`] = ` "ring-transparent/90", "ring-transparent/95", "ring-transparent/100", - "ring-transparent/background", "rotate-0", "rotate-1", "rotate-2", @@ -10453,76 +9097,6 @@ exports[`getClassList 1`] = ` "scroll-mb-80", "scroll-mb-96", "scroll-mb-px", - "scroll-mbe-0", - "scroll-mbe-0.5", - "scroll-mbe-1", - "scroll-mbe-1.5", - "scroll-mbe-2", - "scroll-mbe-2.5", - "scroll-mbe-3", - "scroll-mbe-3.5", - "scroll-mbe-4", - "scroll-mbe-5", - "scroll-mbe-6", - "scroll-mbe-7", - "scroll-mbe-8", - "scroll-mbe-9", - "scroll-mbe-10", - "scroll-mbe-11", - "scroll-mbe-12", - "scroll-mbe-14", - "scroll-mbe-16", - "scroll-mbe-20", - "scroll-mbe-24", - "scroll-mbe-28", - "scroll-mbe-32", - "scroll-mbe-36", - "scroll-mbe-40", - "scroll-mbe-44", - "scroll-mbe-48", - "scroll-mbe-52", - "scroll-mbe-56", - "scroll-mbe-60", - "scroll-mbe-64", - "scroll-mbe-72", - "scroll-mbe-80", - "scroll-mbe-96", - "scroll-mbe-px", - "scroll-mbs-0", - "scroll-mbs-0.5", - "scroll-mbs-1", - "scroll-mbs-1.5", - "scroll-mbs-2", - "scroll-mbs-2.5", - "scroll-mbs-3", - "scroll-mbs-3.5", - "scroll-mbs-4", - "scroll-mbs-5", - "scroll-mbs-6", - "scroll-mbs-7", - "scroll-mbs-8", - "scroll-mbs-9", - "scroll-mbs-10", - "scroll-mbs-11", - "scroll-mbs-12", - "scroll-mbs-14", - "scroll-mbs-16", - "scroll-mbs-20", - "scroll-mbs-24", - "scroll-mbs-28", - "scroll-mbs-32", - "scroll-mbs-36", - "scroll-mbs-40", - "scroll-mbs-44", - "scroll-mbs-48", - "scroll-mbs-52", - "scroll-mbs-56", - "scroll-mbs-60", - "scroll-mbs-64", - "scroll-mbs-72", - "scroll-mbs-80", - "scroll-mbs-96", - "scroll-mbs-px", "scroll-me-0", "scroll-me-0.5", "scroll-me-1", @@ -10838,76 +9412,6 @@ exports[`getClassList 1`] = ` "scroll-pb-80", "scroll-pb-96", "scroll-pb-px", - "scroll-pbe-0", - "scroll-pbe-0.5", - "scroll-pbe-1", - "scroll-pbe-1.5", - "scroll-pbe-2", - "scroll-pbe-2.5", - "scroll-pbe-3", - "scroll-pbe-3.5", - "scroll-pbe-4", - "scroll-pbe-5", - "scroll-pbe-6", - "scroll-pbe-7", - "scroll-pbe-8", - "scroll-pbe-9", - "scroll-pbe-10", - "scroll-pbe-11", - "scroll-pbe-12", - "scroll-pbe-14", - "scroll-pbe-16", - "scroll-pbe-20", - "scroll-pbe-24", - "scroll-pbe-28", - "scroll-pbe-32", - "scroll-pbe-36", - "scroll-pbe-40", - "scroll-pbe-44", - "scroll-pbe-48", - "scroll-pbe-52", - "scroll-pbe-56", - "scroll-pbe-60", - "scroll-pbe-64", - "scroll-pbe-72", - "scroll-pbe-80", - "scroll-pbe-96", - "scroll-pbe-px", - "scroll-pbs-0", - "scroll-pbs-0.5", - "scroll-pbs-1", - "scroll-pbs-1.5", - "scroll-pbs-2", - "scroll-pbs-2.5", - "scroll-pbs-3", - "scroll-pbs-3.5", - "scroll-pbs-4", - "scroll-pbs-5", - "scroll-pbs-6", - "scroll-pbs-7", - "scroll-pbs-8", - "scroll-pbs-9", - "scroll-pbs-10", - "scroll-pbs-11", - "scroll-pbs-12", - "scroll-pbs-14", - "scroll-pbs-16", - "scroll-pbs-20", - "scroll-pbs-24", - "scroll-pbs-28", - "scroll-pbs-32", - "scroll-pbs-36", - "scroll-pbs-40", - "scroll-pbs-44", - "scroll-pbs-48", - "scroll-pbs-52", - "scroll-pbs-56", - "scroll-pbs-60", - "scroll-pbs-64", - "scroll-pbs-72", - "scroll-pbs-80", - "scroll-pbs-96", - "scroll-pbs-px", "scroll-pe-0", "scroll-pe-0.5", "scroll-pe-1", @@ -11154,150 +9658,6 @@ exports[`getClassList 1`] = ` "scroll-py-96", "scroll-py-px", "scroll-smooth", - "scrollbar-auto", - "scrollbar-gutter-auto", - "scrollbar-gutter-both", - "scrollbar-gutter-stable", - "scrollbar-none", - "scrollbar-thin", - "scrollbar-thumb-current", - "scrollbar-thumb-current/0", - "scrollbar-thumb-current/5", - "scrollbar-thumb-current/10", - "scrollbar-thumb-current/15", - "scrollbar-thumb-current/20", - "scrollbar-thumb-current/25", - "scrollbar-thumb-current/30", - "scrollbar-thumb-current/35", - "scrollbar-thumb-current/40", - "scrollbar-thumb-current/45", - "scrollbar-thumb-current/50", - "scrollbar-thumb-current/55", - "scrollbar-thumb-current/60", - "scrollbar-thumb-current/65", - "scrollbar-thumb-current/70", - "scrollbar-thumb-current/75", - "scrollbar-thumb-current/80", - "scrollbar-thumb-current/85", - "scrollbar-thumb-current/90", - "scrollbar-thumb-current/95", - "scrollbar-thumb-current/100", - "scrollbar-thumb-current/background", - "scrollbar-thumb-inherit", - "scrollbar-thumb-inherit/0", - "scrollbar-thumb-inherit/5", - "scrollbar-thumb-inherit/10", - "scrollbar-thumb-inherit/15", - "scrollbar-thumb-inherit/20", - "scrollbar-thumb-inherit/25", - "scrollbar-thumb-inherit/30", - "scrollbar-thumb-inherit/35", - "scrollbar-thumb-inherit/40", - "scrollbar-thumb-inherit/45", - "scrollbar-thumb-inherit/50", - "scrollbar-thumb-inherit/55", - "scrollbar-thumb-inherit/60", - "scrollbar-thumb-inherit/65", - "scrollbar-thumb-inherit/70", - "scrollbar-thumb-inherit/75", - "scrollbar-thumb-inherit/80", - "scrollbar-thumb-inherit/85", - "scrollbar-thumb-inherit/90", - "scrollbar-thumb-inherit/95", - "scrollbar-thumb-inherit/100", - "scrollbar-thumb-inherit/background", - "scrollbar-thumb-transparent", - "scrollbar-thumb-transparent/0", - "scrollbar-thumb-transparent/5", - "scrollbar-thumb-transparent/10", - "scrollbar-thumb-transparent/15", - "scrollbar-thumb-transparent/20", - "scrollbar-thumb-transparent/25", - "scrollbar-thumb-transparent/30", - "scrollbar-thumb-transparent/35", - "scrollbar-thumb-transparent/40", - "scrollbar-thumb-transparent/45", - "scrollbar-thumb-transparent/50", - "scrollbar-thumb-transparent/55", - "scrollbar-thumb-transparent/60", - "scrollbar-thumb-transparent/65", - "scrollbar-thumb-transparent/70", - "scrollbar-thumb-transparent/75", - "scrollbar-thumb-transparent/80", - "scrollbar-thumb-transparent/85", - "scrollbar-thumb-transparent/90", - "scrollbar-thumb-transparent/95", - "scrollbar-thumb-transparent/100", - "scrollbar-thumb-transparent/background", - "scrollbar-track-current", - "scrollbar-track-current/0", - "scrollbar-track-current/5", - "scrollbar-track-current/10", - "scrollbar-track-current/15", - "scrollbar-track-current/20", - "scrollbar-track-current/25", - "scrollbar-track-current/30", - "scrollbar-track-current/35", - "scrollbar-track-current/40", - "scrollbar-track-current/45", - "scrollbar-track-current/50", - "scrollbar-track-current/55", - "scrollbar-track-current/60", - "scrollbar-track-current/65", - "scrollbar-track-current/70", - "scrollbar-track-current/75", - "scrollbar-track-current/80", - "scrollbar-track-current/85", - "scrollbar-track-current/90", - "scrollbar-track-current/95", - "scrollbar-track-current/100", - "scrollbar-track-current/background", - "scrollbar-track-inherit", - "scrollbar-track-inherit/0", - "scrollbar-track-inherit/5", - "scrollbar-track-inherit/10", - "scrollbar-track-inherit/15", - "scrollbar-track-inherit/20", - "scrollbar-track-inherit/25", - "scrollbar-track-inherit/30", - "scrollbar-track-inherit/35", - "scrollbar-track-inherit/40", - "scrollbar-track-inherit/45", - "scrollbar-track-inherit/50", - "scrollbar-track-inherit/55", - "scrollbar-track-inherit/60", - "scrollbar-track-inherit/65", - "scrollbar-track-inherit/70", - "scrollbar-track-inherit/75", - "scrollbar-track-inherit/80", - "scrollbar-track-inherit/85", - "scrollbar-track-inherit/90", - "scrollbar-track-inherit/95", - "scrollbar-track-inherit/100", - "scrollbar-track-inherit/background", - "scrollbar-track-transparent", - "scrollbar-track-transparent/0", - "scrollbar-track-transparent/5", - "scrollbar-track-transparent/10", - "scrollbar-track-transparent/15", - "scrollbar-track-transparent/20", - "scrollbar-track-transparent/25", - "scrollbar-track-transparent/30", - "scrollbar-track-transparent/35", - "scrollbar-track-transparent/40", - "scrollbar-track-transparent/45", - "scrollbar-track-transparent/50", - "scrollbar-track-transparent/55", - "scrollbar-track-transparent/60", - "scrollbar-track-transparent/65", - "scrollbar-track-transparent/70", - "scrollbar-track-transparent/75", - "scrollbar-track-transparent/80", - "scrollbar-track-transparent/85", - "scrollbar-track-transparent/90", - "scrollbar-track-transparent/95", - "scrollbar-track-transparent/100", - "scrollbar-track-transparent/background", "select-all", "select-auto", "select-none", @@ -11337,7 +9697,6 @@ exports[`getClassList 1`] = ` "shadow-current/90", "shadow-current/95", "shadow-current/100", - "shadow-current/background", "shadow-inherit", "shadow-inherit/0", "shadow-inherit/5", @@ -11360,7 +9719,6 @@ exports[`getClassList 1`] = ` "shadow-inherit/90", "shadow-inherit/95", "shadow-inherit/100", - "shadow-inherit/background", "shadow-initial", "shadow-none", "shadow-transparent", @@ -11385,7 +9743,6 @@ exports[`getClassList 1`] = ` "shadow-transparent/90", "shadow-transparent/95", "shadow-transparent/100", - "shadow-transparent/background", "shrink", "shrink-0", "size-0", @@ -11565,6 +9922,66 @@ exports[`getClassList 1`] = ` "space-y-reverse", "sr-only", "stacked-fractions", + "start-0", + "start-0.5", + "start-1", + "start-1.5", + "start-2", + "start-2.5", + "start-3", + "start-3.5", + "start-4", + "start-5", + "start-6", + "start-7", + "start-8", + "start-9", + "start-10", + "start-11", + "start-12", + "start-14", + "start-16", + "start-20", + "start-24", + "start-28", + "start-32", + "start-36", + "start-40", + "start-44", + "start-48", + "start-52", + "start-56", + "start-60", + "start-64", + "start-72", + "start-80", + "start-96", + "start-1/2", + "start-1/3", + "start-1/4", + "start-1/5", + "start-1/6", + "start-1/12", + "start-2/3", + "start-2/4", + "start-2/5", + "start-2/6", + "start-2/12", + "start-3/4", + "start-3/5", + "start-3/6", + "start-3/12", + "start-4/5", + "start-4/6", + "start-4/12", + "start-5/6", + "start-5/12", + "start-6/12", + "start-7/12", + "start-8/12", + "start-9/12", + "start-10/12", + "start-11/12", "start-auto", "start-full", "start-px", @@ -11596,7 +10013,6 @@ exports[`getClassList 1`] = ` "stroke-current/90", "stroke-current/95", "stroke-current/100", - "stroke-current/background", "stroke-inherit", "stroke-inherit/0", "stroke-inherit/5", @@ -11619,7 +10035,6 @@ exports[`getClassList 1`] = ` "stroke-inherit/90", "stroke-inherit/95", "stroke-inherit/100", - "stroke-inherit/background", "stroke-none", "stroke-transparent", "stroke-transparent/0", @@ -11643,11 +10058,7 @@ exports[`getClassList 1`] = ` "stroke-transparent/90", "stroke-transparent/95", "stroke-transparent/100", - "stroke-transparent/background", "subpixel-antialiased", - "tab-2", - "tab-4", - "tab-8", "table", "table-auto", "table-caption", @@ -11685,7 +10096,6 @@ exports[`getClassList 1`] = ` "text-current/90", "text-current/95", "text-current/100", - "text-current/background", "text-ellipsis", "text-end", "text-inherit", @@ -11710,7 +10120,6 @@ exports[`getClassList 1`] = ` "text-inherit/90", "text-inherit/95", "text-inherit/100", - "text-inherit/background", "text-justify", "text-left", "text-nowrap", @@ -11738,7 +10147,6 @@ exports[`getClassList 1`] = ` "text-shadow-current/90", "text-shadow-current/95", "text-shadow-current/100", - "text-shadow-current/background", "text-shadow-inherit", "text-shadow-inherit/0", "text-shadow-inherit/5", @@ -11761,7 +10169,6 @@ exports[`getClassList 1`] = ` "text-shadow-inherit/90", "text-shadow-inherit/95", "text-shadow-inherit/100", - "text-shadow-inherit/background", "text-shadow-initial", "text-shadow-none", "text-shadow-transparent", @@ -11786,7 +10193,6 @@ exports[`getClassList 1`] = ` "text-shadow-transparent/90", "text-shadow-transparent/95", "text-shadow-transparent/100", - "text-shadow-transparent/background", "text-start", "text-transparent", "text-transparent/0", @@ -11810,7 +10216,6 @@ exports[`getClassList 1`] = ` "text-transparent/90", "text-transparent/95", "text-transparent/100", - "text-transparent/background", "text-wrap", "text-xs", "to-0%", @@ -11856,7 +10261,6 @@ exports[`getClassList 1`] = ` "to-current/90", "to-current/95", "to-current/100", - "to-current/background", "to-inherit", "to-inherit/0", "to-inherit/5", @@ -11879,7 +10283,6 @@ exports[`getClassList 1`] = ` "to-inherit/90", "to-inherit/95", "to-inherit/100", - "to-inherit/background", "to-transparent", "to-transparent/0", "to-transparent/5", @@ -11902,7 +10305,6 @@ exports[`getClassList 1`] = ` "to-transparent/90", "to-transparent/95", "to-transparent/100", - "to-transparent/background", "top-0", "top-0.5", "top-1", @@ -12271,7 +10673,6 @@ exports[`getClassList 1`] = ` "via-current/90", "via-current/95", "via-current/100", - "via-current/background", "via-inherit", "via-inherit/0", "via-inherit/5", @@ -12294,7 +10695,6 @@ exports[`getClassList 1`] = ` "via-inherit/90", "via-inherit/95", "via-inherit/100", - "via-inherit/background", "via-none", "via-transparent", "via-transparent/0", @@ -12318,7 +10718,6 @@ exports[`getClassList 1`] = ` "via-transparent/90", "via-transparent/95", "via-transparent/100", - "via-transparent/background", "visible", "w-0", "w-0.5", @@ -12415,16 +10814,6 @@ exports[`getClassList 1`] = ` "z-40", "z-50", "z-auto", - "zoom-50", - "zoom-75", - "zoom-90", - "zoom-95", - "zoom-100", - "zoom-105", - "zoom-110", - "zoom-125", - "zoom-150", - "zoom-200", ] `; diff --git a/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap b/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap index 43e8fdf6f..6adc96113 100644 --- a/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap @@ -1,8 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`border-* 1`] = ` -" -@layer properties { +"@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-border-style: solid; @@ -183,13 +182,11 @@ exports[`border-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -} -" +}" `; exports[`border-b-* 1`] = ` -" -@layer properties { +"@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-border-style: solid; @@ -370,387 +367,11 @@ exports[`border-b-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -} -" -`; - -exports[`border-be-* 1`] = ` -" -@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-border-style: solid; - } - } -} - -:root, :host { - --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; -} - -.border-be { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 1px; -} - -.border-be-0 { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 0; -} - -.border-be-2 { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 2px; -} - -.border-be-4 { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 4px; -} - -.border-be-123 { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 123px; -} - -.border-be-\\[0_1\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 0 1; -} - -.border-be-\\[0_2px_0_2px\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 0 2px 0 2px; -} - -.border-be-\\[12px\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 12px; -} - -.border-be-\\[12px_8px\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 12px 8px; -} - -.border-be-\\[length\\:var\\(--my-width\\)\\], .border-be-\\[line-width\\:var\\(--my-width\\)\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: var(--my-width); -} - -.border-be-\\[medium\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: medium; -} - -.border-be-\\[thick\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: thick; -} - -.border-be-\\[thin\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: thin; -} - -.border-be-\\[thin_2px\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: thin 2px; -} - -.border-be-\\[\\#0088cc\\] { - border-block-end-color: #08c; -} - -.border-be-\\[\\#0088cc\\]\\/50 { - border-block-end-color: oklab(59.9824% -.067 -.124 / .5); -} - -.border-be-\\[color\\:var\\(--my-color\\)\\], .border-be-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-block-end-color: var(--my-color); -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-block-end-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } -} - -.border-be-\\[var\\(--my-color\\)\\], .border-be-\\[var\\(--my-color\\)\\]\\/50 { - border-block-end-color: var(--my-color); -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-\\[var\\(--my-color\\)\\]\\/50 { - border-block-end-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } -} - -.border-be-blue-500 { - border-block-end-color: var(--border-color-blue-500); -} - -.border-be-current, .border-be-current\\/50 { - border-block-end-color: currentColor; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-current\\/50 { - border-block-end-color: color-mix(in oklab, currentcolor 50%, transparent); - } -} - -.border-be-inherit { - border-block-end-color: inherit; -} - -.border-be-red-500 { - border-block-end-color: var(--color-red-500); -} - -.border-be-red-500\\/2\\.5 { - border-block-end-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-red-500\\/2\\.5 { - border-block-end-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-be-red-500\\/2\\.25 { - border-block-end-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-red-500\\/2\\.25 { - border-block-end-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-be-red-500\\/2\\.75 { - border-block-end-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-red-500\\/2\\.75 { - border-block-end-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } -} - -.border-be-red-500\\/50 { - border-block-end-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-red-500\\/50 { - border-block-end-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } -} - -.border-be-transparent { - border-block-end-color: #0000; -} - -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -" -`; - -exports[`border-bs-* 1`] = ` -" -@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-border-style: solid; - } - } -} - -:root, :host { - --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; -} - -.border-bs { - border-block-start-style: var(--tw-border-style); - border-block-start-width: 1px; -} - -.border-bs-0 { - border-block-start-style: var(--tw-border-style); - border-block-start-width: 0; -} - -.border-bs-2 { - border-block-start-style: var(--tw-border-style); - border-block-start-width: 2px; -} - -.border-bs-4 { - border-block-start-style: var(--tw-border-style); - border-block-start-width: 4px; -} - -.border-bs-123 { - border-block-start-style: var(--tw-border-style); - border-block-start-width: 123px; -} - -.border-bs-\\[0_1\\] { - border-block-start-style: var(--tw-border-style); - border-block-start-width: 0 1; -} - -.border-bs-\\[0_2px_0_2px\\] { - border-block-start-style: var(--tw-border-style); - border-block-start-width: 0 2px 0 2px; -} - -.border-bs-\\[12px\\] { - border-block-start-style: var(--tw-border-style); - border-block-start-width: 12px; -} - -.border-bs-\\[12px_8px\\] { - border-block-start-style: var(--tw-border-style); - border-block-start-width: 12px 8px; -} - -.border-bs-\\[length\\:var\\(--my-width\\)\\], .border-bs-\\[line-width\\:var\\(--my-width\\)\\] { - border-block-start-style: var(--tw-border-style); - border-block-start-width: var(--my-width); -} - -.border-bs-\\[medium\\] { - border-block-start-style: var(--tw-border-style); - border-block-start-width: medium; -} - -.border-bs-\\[thick\\] { - border-block-start-style: var(--tw-border-style); - border-block-start-width: thick; -} - -.border-bs-\\[thin\\] { - border-block-start-style: var(--tw-border-style); - border-block-start-width: thin; -} - -.border-bs-\\[thin_2px\\] { - border-block-start-style: var(--tw-border-style); - border-block-start-width: thin 2px; -} - -.border-bs-\\[\\#0088cc\\] { - border-block-start-color: #08c; -} - -.border-bs-\\[\\#0088cc\\]\\/50 { - border-block-start-color: oklab(59.9824% -.067 -.124 / .5); -} - -.border-bs-\\[color\\:var\\(--my-color\\)\\], .border-bs-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-block-start-color: var(--my-color); -} - -@supports (color: color-mix(in lab, red, red)) { - .border-bs-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-block-start-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } -} - -.border-bs-\\[var\\(--my-color\\)\\], .border-bs-\\[var\\(--my-color\\)\\]\\/50 { - border-block-start-color: var(--my-color); -} - -@supports (color: color-mix(in lab, red, red)) { - .border-bs-\\[var\\(--my-color\\)\\]\\/50 { - border-block-start-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } -} - -.border-bs-blue-500 { - border-block-start-color: var(--border-color-blue-500); -} - -.border-bs-current, .border-bs-current\\/50 { - border-block-start-color: currentColor; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-bs-current\\/50 { - border-block-start-color: color-mix(in oklab, currentcolor 50%, transparent); - } -} - -.border-bs-inherit { - border-block-start-color: inherit; -} - -.border-bs-red-500 { - border-block-start-color: var(--color-red-500); -} - -.border-bs-red-500\\/2\\.5 { - border-block-start-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-bs-red-500\\/2\\.5 { - border-block-start-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-bs-red-500\\/2\\.25 { - border-block-start-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-bs-red-500\\/2\\.25 { - border-block-start-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-bs-red-500\\/2\\.75 { - border-block-start-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-bs-red-500\\/2\\.75 { - border-block-start-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } -} - -.border-bs-red-500\\/50 { - border-block-start-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-bs-red-500\\/50 { - border-block-start-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } -} - -.border-bs-transparent { - border-block-start-color: #0000; -} - -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -" +}" `; exports[`border-e-* 1`] = ` -" -@layer properties { +"@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-border-style: solid; @@ -931,13 +552,11 @@ exports[`border-e-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -} -" +}" `; exports[`border-l-* 1`] = ` -" -@layer properties { +"@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-border-style: solid; @@ -1118,13 +737,11 @@ exports[`border-l-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -} -" +}" `; exports[`border-r-* 1`] = ` -" -@layer properties { +"@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-border-style: solid; @@ -1305,13 +922,11 @@ exports[`border-r-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -} -" +}" `; exports[`border-s-* 1`] = ` -" -@layer properties { +"@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-border-style: solid; @@ -1492,13 +1107,11 @@ exports[`border-s-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -} -" +}" `; exports[`border-t-* 1`] = ` -" -@layer properties { +"@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-border-style: solid; @@ -1679,13 +1292,11 @@ exports[`border-t-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -} -" +}" `; exports[`border-x-* 1`] = ` -" -@layer properties { +"@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-border-style: solid; @@ -1866,13 +1477,11 @@ exports[`border-x-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -} -" +}" `; exports[`border-y-* 1`] = ` -" -@layer properties { +"@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-border-style: solid; @@ -2053,6 +1662,5 @@ exports[`border-y-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -} -" +}" `; diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 2d5e5b847..c7bcc5e28 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -163,52 +163,13 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { let parts = child.params.split(/(\s+)/g) let candidateOffsets: Record = {} - let normalIdents: string[] = [] - let dashedIdents: string[] = [] let offset = 0 for (let [idx, part] of parts.entries()) { - if (idx % 2 === 0) { - if (part[0] === '-' && part[1] === '-') { - dashedIdents.push(part) - } else { - normalIdents.push(part) - } - - candidateOffsets[part] = offset - } - + if (idx % 2 === 0) candidateOffsets[part] = offset offset += part.length } - if (dashedIdents.length) { - // If we have an `@apply` that only consists of dashed idents then the - // user is intending to use a CSS mixin: - // https://drafts.csswg.org/css-mixins-1/#apply-rule - // - // These are not considered utilities and need to be emitted literally. - if (normalIdents.length === 0) return WalkAction.Skip - - // If we find a dashed ident *here* it means that someone is trying - // to use mixins and our `@apply` behavior together. - // - // This is invalid and the rules must be written separately. Let the - // user know they need to move them into a separate rule. - let list = dashedIdents.join(' ') - - throw new Error( - `You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply ${list}\` into a separate rule.`, - ) - } - - let hasBody = child.nodes.length > 0 - - if (hasBody && normalIdents.length) { - let list = normalIdents.join(' ') - - throw new Error(`The rule \`@apply ${list}\` must not have a body.`) - } - // Replace the `@apply` rule with the actual utility classes { // Parse the candidates to an AST that we can replace the `@apply` rule diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index 14092c9f5..63a621c6a 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -1,18 +1,16 @@ -import { describe, expect, it, test } from 'vitest' +import { expect, it } from 'vitest' import { + atRule, context, cssContext, decl, - handleNesting, optimizeAst, - rule, styleRule, toCss, type AstNode, } from './ast' import * as CSS from './css-parser' import { buildDesignSystem } from './design-system' -import { pretty } from './test-utils/run' import { Theme } from './theme' import { walk, WalkAction } from './walk' @@ -20,9 +18,9 @@ const css = String.raw const defaultDesignSystem = buildDesignSystem(new Theme()) it('should pretty print an AST', () => { - expect(pretty(toCss(CSS.parse('.foo{color:red;&:hover{color:blue;}}')))).toMatchInlineSnapshot(` - " - .foo { + expect(toCss(optimizeAst(CSS.parse('.foo{color:red;&:hover{color:blue;}}'), defaultDesignSystem))) + .toMatchInlineSnapshot(` + ".foo { color: red; &:hover { color: blue; @@ -70,9 +68,8 @@ it('allows the placement of context nodes', () => { expect(blueContext).toEqual({ context: 'a' }) expect(greenContext).toEqual({ context: 'b' }) - expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` - " - .foo { + expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(` + ".foo { color: red; } .bar { @@ -85,6 +82,36 @@ it('allows the placement of context nodes', () => { `) }) +it('should stop walking when returning `WalkAction.Stop`', () => { + let ast = [ + styleRule('.foo', [styleRule('.nested', [styleRule('.bail', [decl('color', 'red')])])]), + styleRule('.bar'), + styleRule('.baz'), + styleRule('.qux'), + ] + + let seen = new Set() + + walk(ast, (node) => { + if (node.kind === 'rule') { + seen.add(node.selector) + } + + if (node.kind === 'rule' && node.selector === '.bail') { + return WalkAction.Stop + } + }) + + // We do not want to see `.bar`, `.baz`, or `.qux` because we bailed early + expect(seen).toMatchInlineSnapshot(` + Set { + ".foo", + ".nested", + ".bail", + } + `) +}) + it('should not emit empty rules once optimized', () => { let ast = CSS.parse(css` /* Empty rule */ @@ -127,15 +154,13 @@ it('should not emit empty rules once optimized', () => { /* Exceptions: */ @charset "UTF-8"; @layer foo, bar, baz; - @layer foo, bar, baz; /* Will be deduped */ @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml'; @import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap'); `) - expect(pretty(toCss(ast))).toMatchInlineSnapshot(` - " - .foo { + expect(toCss(ast)).toMatchInlineSnapshot(` + ".foo { } .foo { .bar { @@ -162,16 +187,14 @@ it('should not emit empty rules once optimized', () => { } @charset "UTF-8"; @layer foo, bar, baz; - @layer foo, bar, baz; @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml'; @import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap'); " `) - expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` - " - @charset "UTF-8"; + expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(` + "@charset "UTF-8"; @layer foo, bar, baz; @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml'; @@ -212,9 +235,8 @@ it('should not emit exact duplicate declarations in the same rule', () => { } `) - expect(pretty(toCss(ast))).toMatchInlineSnapshot(` - " - .foo { + expect(toCss(ast)).toMatchInlineSnapshot(` + ".foo { color: red; .bar { color: green; @@ -245,27 +267,20 @@ it('should not emit exact duplicate declarations in the same rule', () => { " `) - expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` - " - .foo { + expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(` + ".foo { .bar { - color: green; color: blue; color: green; } color: red; - & { - color: green; - & { - color: red; - color: green; - color: blue; - } - color: red; - } + } + .foo { + color: green; + color: blue; + color: red; background: blue; .bar { - color: green; color: blue; color: green; } @@ -275,1390 +290,303 @@ it('should not emit exact duplicate declarations in the same rule', () => { `) }) -it('should not emit color-mix() fallbacks inside @keyframes', () => { - let ast = CSS.parse(css` - @keyframes my-animation { - 0% { - color: color-mix(in oklab, var(--color-emerald-600) 0%, transparent); - } - 100% { - color: color-mix(in oklab, var(--color-emerald-600) 0%, transparent); - } +it('should only visit children once when calling `replaceWith` with single element array', () => { + let visited = new Set() + + let ast: AstNode[] = [ + atRule('@media', '', [styleRule('.foo', [decl('color', 'blue')])]), + styleRule('.bar', [decl('color', 'blue')]), + ] + + walk(ast, (node) => { + if (visited.has(node)) { + throw new Error('Visited node twice') } - `) + visited.add(node) - let theme = new Theme() - theme.add('--color-emerald-600', 'oklch(59.6% 0.145 163.225)') - - let design = buildDesignSystem(theme) - - expect(pretty(toCss(optimizeAst(ast, design)))).toMatchInlineSnapshot(` - " - @keyframes my-animation { - 0% { - color: color-mix(in oklab, var(--color-emerald-600) 0%, transparent); - } - 100% { - color: color-mix(in oklab, var(--color-emerald-600) 0%, transparent); - } - } - " - `) -}) - -describe('optimization', () => { - function optimize(input: string) { - let ast = CSS.parse(input) - walk(ast, { - exit(node) { - // Depending on where we use `@scope`, it's handled slightly differently: - // - // 1. In user authored CSS, we perform the normal CSS nesting and - // hoisting of `@scope` rules - // 2. In a `@custom-variant` definition, we need to swap the `@scope` - // with the parent definition. Otherwise you'll never get the - // expected "sandwich" effect. - // - // This little `@in-variant` hack is just a different way of - // differentiating between both cases for these tests. - if (node.kind === 'at-rule' && node.name === '@in-variant') { - return WalkAction.Replace(context({ source: 'variant' }, node.nodes)) - } - }, - }) - return pretty(toCss(handleNesting(ast))) - } - - // See: https://drafts.csswg.org/css-nesting-1/ - describe('CSS Nesting Module Level 1', () => { - it('uses the descendant combinator by default', async () => { - expect( - optimize(css` - .a { - element { - --x: 1; - } - .class { - --x: 2; - } - #id { - --x: 3; - } - :pseudo-class { - --x: 4; - } - ::pseudo-element { - --x: 5; - } - [attribute] { - --x: 6; - } - * { - --x: 7; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a element { - --x: 1; - } - .a .class { - --x: 2; - } - .a #id { - --x: 3; - } - .a :pseudo-class { - --x: 4; - } - .a ::pseudo-element { - --x: 5; - } - .a [attribute] { - --x: 6; - } - .a * { - --x: 7; - } - " - `) - }) - - it('should be possible to change the combinator', async () => { - expect( - optimize(css` - .a { - + .b { - --x: 1; - } - > .c { - --x: 2; - } - ~ .d { - --x: 3; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a + .b { - --x: 1; - } - .a > .c { - --x: 2; - } - .a ~ .d { - --x: 3; - } - " - `) - }) - - it('should replace the first rule, that contains `&` with `:scope`', async () => { - expect( - optimize(css` - /* Standalone */ - & { - --x: 1; - } - - /* In an at-rule */ - @supports (--y: 1) { - & { - --x: 2; - } - } - - /* With :is(…) */ - :is(&) { - --x: 3; - } - - /* In an at-rule, with :is(…) */ - @supports (--y: 2) { - :is(&) { - --x: 4; - } - } - - /* With multiple selectors */ - &, - .a { - --x: 5; - } - - /* With multiple selectors + :is(…) */ - :is(&), - .b { - --x: 6; - } - - /* With multiple selectors in an at-rule */ - @supports (--y: 3) { - &, - .c { - --x: 7; - } - } - - /* With multiple selectors in an at-rule + :is(…) */ - @supports (--y: 4) { - :is(&), - .d { - --x: 8; - } - } - `), - ).toMatchInlineSnapshot(` - " - :scope { - --x: 1; - } - @supports (--y: 1) { - :scope { - --x: 2; - } - } - :is(:scope) { - --x: 3; - } - @supports (--y: 2) { - :is(:scope) { - --x: 4; - } - } - :scope, .a { - --x: 5; - } - :is(:scope), .b { - --x: 6; - } - @supports (--y: 3) { - :scope, .c { - --x: 7; - } - } - @supports (--y: 4) { - :is(:scope), .d { - --x: 8; - } - } - " - `) - }) - - it('should be possible to use `&` to explicitly match the parent', async () => { - expect( - optimize(css` - .a { - & + .b { - --x: 1; - } - & > .c { - --x: 2; - } - & ~ .d { - --x: 3; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a + .b { - --x: 1; - } - .a > .c { - --x: 2; - } - .a ~ .d { - --x: 3; - } - " - `) - }) - - it('should be possible to use `&` in a different location', async () => { - expect( - optimize(css` - .a { - .b & { - --x: 1; - } - .c + & { - --x: 2; - } - .d > & { - --x: 3; - } - .e ~ & { - --x: 4; - } - } - `), - ).toMatchInlineSnapshot(` - " - .b .a { - --x: 1; - } - .c + .a { - --x: 2; - } - .d > .a { - --x: 3; - } - .e ~ .a { - --x: 4; - } - " - `) - }) - - it('should be possible to use `&` on its own', async () => { - expect( - optimize(css` - .a { - & { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a { - --x: 1; - } - " - `) - }) - - it('should be possible to use `&` nested in `:is(…)`', async () => { - expect( - optimize(css` - .a { - :is(&) { - --x: 1; - } - :is(:is(:is(&))) { - --x: 2; - } - } - `), - ).toMatchInlineSnapshot(` - " - :is(.a) { - --x: 1; - } - :is(:is(:is(.a))) { - --x: 2; - } - " - `) - }) - - it('should be possible to handle nesting with a parent selector list', async () => { - expect( - optimize(css` - .a, - .b { - .c, - .d & { - --x: 1; - &:hover { - --x: 2; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - :is(.a, .b) .c, .d :is(.a, .b) { - --x: 1; - &:hover { - --x: 2; - } - } - " - `) - }) - - it('should not replace `\&`', () => { - expect( - optimize(css` - .a { - .b-\& { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a .b-\\& { - --x: 1; - } - " - `) - }) - - it('should not replace `&` as part of a string', () => { - expect( - optimize(css` - .a { - [data-b='c&d'] { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a [data-b='c&d'] { - --x: 1; - } - " - `) - }) - - it.each([ - ['element', '&element', ':is(element)element'], // Invalid CSS - ['element', 'element&', 'element:is(element)'], - ['element', '&.class', 'element.class'], - ['element', '.class&', '.class:is(element)'], // Optimization: element.class - ['element', '&#id', 'element#id'], - ['element', '#id&', '#id:is(element)'], // Optimization: element#id - ['element', '&:hover', 'element:hover'], - ['element', ':hover&', ':hover:is(element)'], - ['element', '&::before', 'element::before'], - ['element', '::before&', '::before:is(element)'], // Invalid CSS - ['element', '&:not(.x)', 'element:not(.x)'], - ['element', ':not(.x)&', ':not(.x):is(element)'], // Optimization: element:not(.x) - ['element', '&[attribute]', 'element[attribute]'], - ['element', '[attribute]&', '[attribute]:is(element)'], // Optimization: element[attribute] - ['element', '&*', ':is(element)*'], // Invalid CSS - ['element', '*&', 'element'], - - ['.class', '&element', ':is(.class)element'], // Invalid CSS - ['.class', 'element&', 'element.class'], - ['.class', '&.class', '.class.class'], - ['.class', '.class&', '.class.class'], - ['.class', '&#id', '.class#id'], - ['.class', '#id&', '#id.class'], - ['.class', '&:hover', '.class:hover'], - ['.class', ':hover&', ':hover.class'], - ['.class', '&::before', '.class::before'], - ['.class', '::before&', '::before.class'], // Invalid CSS - ['.class', '&:not(.x)', '.class:not(.x)'], - ['.class', ':not(.x)&', ':not(.x).class'], - ['.class', '&[attribute]', '.class[attribute]'], - ['.class', '[attribute]&', '[attribute].class'], - ['.class', '&*', ':is(.class)*'], // Invalid CSS - ['.class', '*&', '.class'], - - ['#id', '&element', ':is(#id)element'], // Invalid CSS - ['#id', 'element&', 'element#id'], - ['#id', '&.class', '#id.class'], - ['#id', '.class&', '.class#id'], - ['#id', '&#id', '#id#id'], - ['#id', '#id&', '#id#id'], - ['#id', '&:hover', '#id:hover'], - ['#id', ':hover&', ':hover#id'], - ['#id', '&::before', '#id::before'], - ['#id', '::before&', '::before#id'], // Invalid CSS - ['#id', '&:not(.x)', '#id:not(.x)'], - ['#id', ':not(.x)&', ':not(.x)#id'], - ['#id', '&[attribute]', '#id[attribute]'], - ['#id', '[attribute]&', '[attribute]#id'], - ['#id', '&*', ':is(#id)*'], // Invalid CSS - ['#id', '*&', '#id'], - - [':hover', '&element', ':is(:hover)element'], // Invalid CSS - [':hover', 'element&', 'element:hover'], - [':hover', '&.class', ':hover.class'], - [':hover', '.class&', '.class:hover'], - [':hover', '&#id', ':hover#id'], - [':hover', '#id&', '#id:hover'], - [':hover', '&:hover', ':hover:hover'], - [':hover', ':hover&', ':hover:hover'], - [':hover', '&::before', ':hover::before'], - [':hover', '::before&', '::before:hover'], - [':hover', '&:not(.x)', ':hover:not(.x)'], - [':hover', ':not(.x)&', ':not(.x):hover'], - [':hover', '&[attribute]', ':hover[attribute]'], - [':hover', '[attribute]&', '[attribute]:hover'], - [':hover', '&*', ':is(:hover)*'], // Invalid CSS - [':hover', '*&', ':hover'], - - ['::before', '&element', ':is(::before)element'], // Invalid CSS - ['::before', 'element&', 'element::before'], - ['::before', '&.class', '::before.class'], - ['::before', '.class&', '.class::before'], - ['::before', '&#id', '::before#id'], - ['::before', '#id&', '#id::before'], - ['::before', '&:hover', '::before:hover'], - ['::before', ':hover&', ':hover::before'], - ['::before', '&::before', '::before::before'], - ['::before', '::before&', '::before::before'], - ['::before', '&:not(.x)', '::before:not(.x)'], - ['::before', ':not(.x)&', ':not(.x)::before'], - ['::before', '&[attribute]', '::before[attribute]'], - ['::before', '[attribute]&', '[attribute]::before'], - ['::before', '&*', ':is(::before)*'], // Invalid CSS - ['::before', '*&', '::before'], - - [':not(.x)', '&element', ':is(:not(.x))element'], // Invalid CSS - [':not(.x)', 'element&', 'element:not(.x)'], - [':not(.x)', '&.class', ':not(.x).class'], - [':not(.x)', '.class&', '.class:not(.x)'], - [':not(.x)', '&#id', ':not(.x)#id'], - [':not(.x)', '#id&', '#id:not(.x)'], - [':not(.x)', '&:hover', ':not(.x):hover'], - [':not(.x)', ':hover&', ':hover:not(.x)'], - [':not(.x)', '&::before', ':not(.x)::before'], - [':not(.x)', '::before&', '::before:not(.x)'], - [':not(.x)', '&:not(.x)', ':not(.x):not(.x)'], - [':not(.x)', ':not(.x)&', ':not(.x):not(.x)'], - [':not(.x)', '&[attribute]', ':not(.x)[attribute]'], - [':not(.x)', '[attribute]&', '[attribute]:not(.x)'], - [':not(.x)', '&*', ':is(:not(.x))*'], // Invalid CSS - [':not(.x)', '*&', ':not(.x)'], - - ['[attribute]', '&element', ':is([attribute])element'], // Invalid CSS - ['[attribute]', 'element&', 'element[attribute]'], - ['[attribute]', '&.class', '[attribute].class'], - ['[attribute]', '.class&', '.class[attribute]'], - ['[attribute]', '&#id', '[attribute]#id'], - ['[attribute]', '#id&', '#id[attribute]'], - ['[attribute]', '&:hover', '[attribute]:hover'], - ['[attribute]', ':hover&', ':hover[attribute]'], - ['[attribute]', '&::before', '[attribute]::before'], - ['[attribute]', '::before&', '::before[attribute]'], - ['[attribute]', '&:not(.x)', '[attribute]:not(.x)'], - ['[attribute]', ':not(.x)&', ':not(.x)[attribute]'], - ['[attribute]', '&[attribute]', '[attribute][attribute]'], - ['[attribute]', '[attribute]&', '[attribute][attribute]'], - ['[attribute]', '&*', ':is([attribute])*'], // Invalid CSS - ['[attribute]', '*&', '[attribute]'], - - ['*', '&element', ':is(*)element'], // Invalid CSS - ['*', 'element&', 'element:is(*)'], - ['*', '&.class', '.class'], - ['*', '.class&', '.class:is(*)'], // Optimization: *.class → .class - ['*', '&#id', '#id'], - ['*', '#id&', '#id:is(*)'], // Optimization: *#id → #id - ['*', '&:hover', ':hover'], - ['*', ':hover&', ':hover:is(*)'], - ['*', '&::before', '::before'], - ['*', '::before&', '::before:is(*)'], // Invalid CSS - ['*', '&:not(.x)', ':not(.x)'], - ['*', ':not(.x)&', ':not(.x):is(*)'], // Optimization: *:not(.x) → :not(.x) - ['*', '&[attribute]', '[attribute]'], - ['*', '[attribute]&', '[attribute]:is(*)'], // Optimization: *[attribute] → [attribute] - ['*', '&*', ':is(*)*'], // Invalid CSS - ['*', '*&', '*'], - - ['&', '&element', ':is(:scope)element'], // Invalid CSS - ['&', 'element&', 'element:scope'], - ['&', '&.class', ':scope.class'], - ['&', '.class&', '.class:scope'], - ['&', '&#id', ':scope#id'], - ['&', '#id&', '#id:scope'], - ['&', '&:hover', ':scope:hover'], - ['&', ':hover&', ':hover:scope'], - ['&', '&::before', ':scope::before'], - ['&', '::before&', '::before:scope'], - ['&', '&:not(.x)', ':scope:not(.x)'], - ['&', ':not(.x)&', ':not(.x):scope'], - ['&', '&[attribute]', ':scope[attribute]'], - ['&', '[attribute]&', '[attribute]:scope'], - ['&', '&*', ':is(:scope)*'], // Invalid CSS - ['&', '*&', ':scope'], - ])(`'%s { %s }' → '%s' (%#)`, async (root, nested, expected) => { - let optimized = optimize(toCss([rule(root, [rule(nested, [decl('--x', '0')])])])) - let ast = CSS.parse(optimized) - - let count = 0 - walk(ast, () => void count++) - - // 1 rule, 1 declaration - expect(count).toBe(2) - - if (ast[0].kind !== 'rule') throw new Error('expected a rule') - expect(ast[0].selector).toEqual(expected) - }) - - it('should not remove the `*` namespace from namespaced selectors', () => { - expect( - optimize(css` - .a { - & *|div { - color: red; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a *|div { - color: red; - } - " - `) - }) - - test('multiple selectors in the list are relative to the parent', async () => { - expect( - optimize(css` - .a, - .b { - --x: 1; - + .c { - --x: 2; - } - &.d { - --x: 3; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a, .b { - --x: 1; - + .c { - --x: 2; - } - &.d { - --x: 3; - } - } - " - `) - }) - - it('should be possible to use `&` multiple times', async () => { - expect( - optimize(css` - .a { - & .b & .c & .d { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a .b .a .c .a .d { - --x: 1; - } - " - `) - }) - - it('should be possible to use `&` multiple times in a row', async () => { - expect( - optimize(css` - .a { - &&& { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a.a.a { - --x: 1; - } - " - `) - }) - - it('should be possible to use `&` inside a selector', async () => { - expect( - optimize(css` - .a { - :not(&) { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - :not(.a) { - --x: 1; - } - " - `) - }) - - it('should be possible to use deeply nested CSS', async () => { - expect( - optimize(css` - .a, - .b { - --x: 1; - - .c & { - --x: 2; - - &:hover, - &:focus { - --x: 3; - .d { - --x: 4; - } - } - } - } - `), - ).toMatchInlineSnapshot(` - " - .a, .b { - --x: 1; - .c & { - --x: 2; - &:hover, &:focus { - --x: 3; - .d { - --x: 4; - } - } - } - } - " - `) - }) - - it('should properly split rules to guarantee specificity', async () => { - expect( - optimize(css` - .a { - --before: 1; - &:hover { - --inside: 1; - } - --after: 1; - } - `), - ).toMatchInlineSnapshot(` - " - .a { - --before: 1; - &:hover { - --inside: 1; - } - --after: 1; - } - " - `) - }) - - it.each([ - ['div', '&', 'div'], - ['div', '[before]&', '[before]:is(div)'], - ['div', '&[after]', 'div[after]'], - ['div', '[before]&[after]', '[before]:is(div)[after]'], - ['div', '[before] &', '[before] div'], - ['div', '& [after]', 'div [after]'], - ['div', '[before] & [after]', '[before] div [after]'], - - ['.parent', '&', '.parent'], - ['.parent', '[before]&', '[before].parent'], - ['.parent', '&[after]', '.parent[after]'], - ['.parent', '[before]&[after]', '[before].parent[after]'], - ['.parent', '[before] &', '[before] .parent'], - ['.parent', '& [after]', '.parent [after]'], - ['.parent', '[before] & [after]', '[before] .parent [after]'], - - ['.a > .b', '&', '.a > .b'], - ['.a > .b', '[before]&', '[before]:is(.a > .b)'], - ['.a > .b', '&[after]', '.a > .b[after]'], - ['.a > .b', '[before]&[after]', '[before]:is(.a > .b)[after]'], - ['.a > .b', '[before] &', '[before] :is(.a > .b)'], - ['.a > .b', '& [after]', '.a > .b [after]'], - ['.a > .b', '[before] & [after]', '[before] :is(.a > .b) [after]'], - ])(`should optimize '%s { %s }' → '%s' (%#)`, async (root, nested, expected) => { - let optimized = optimize(toCss([rule(root, [rule(nested, [decl('--x', '0')])])])) - let ast = CSS.parse(optimized) - - let count = 0 - walk(ast, () => void count++) - - // 1 rule, 1 declaration - expect(count).toBe(2) - - if (ast[0].kind !== 'rule') throw new Error('expected a rule') - expect(ast[0].selector).toEqual(expected) - }) - - it('should hoist at-rules', async () => { - expect( - optimize(css` - @layer utilities { - .a, - .b { - @media (print) { - --x: 1; - .c { - @media (min-width: 123px) { - --x: 2; - } - } - } - } - .d { - @media (print) { - @media (min-width: 123px) { - --x: 3; - } - } - } - } - @property --foo { - syntax: '*'; - } - @layer utilities { - .e { - @media (print) { - --x: 4; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (print) { - .a, .b { - --x: 1; - } - @media (min-width: 123px) { - :is(.a, .b) .c { - --x: 2; - } - .d { - --x: 3; - } - } - } - } - @property --foo { - syntax: '*'; - } - @layer utilities { - @media (print) { - .e { - --x: 4; - } - } - } - " - `) - }) - - it('should hoist `@scope` at-rules', async () => { - expect( - optimize(css` - @layer utilities { - .parent { - @in-variant { - @scope (.from) to (.to) { - --x: 1; - } - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope (.from) to (.to) { - .parent { - --x: 1; - } - } - } - " - `) - }) - - // We have an optimization where the moment we see a declaration that we - // stop handling the nesting and emit everything as-is. This won't work when - // we're dealing with `@scope` and we still want to hoist that out. - it('should hoist `@scope` at-rules, even when sibling declarations are present', async () => { - expect( - optimize(css` - @layer utilities { - .parent { - --x: 1; - @in-variant { - @scope (.from) { - .inside { - --y: 2; - } - } - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @layer utilities { - .parent { - --x: 1; - } - @scope (.from) { - .parent .inside { - --y: 2; - } - } - } - " - `) - }) - - it('should resolve `&` in the `@scope` prelude when hoisting', async () => { - // `&` in the `` selector refers to the elements matched by - // the nearest ancestor style rule, while `&` in the `` - // selector refers to the scoping root. - // - // See: https://drafts.csswg.org/css-nesting-1/#nesting-at-scope - expect( - optimize(css` - .parent { - @scope (& > .scope) to (& .limit) { - .content { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.parent > .scope) to (:where(:scope) .limit) { - .content { - --x: 1; - } - } - " - `) - - // Selector lists need `:is(…)` to keep their meaning - expect( - optimize(css` - .parent-a, - .parent-b { - @scope (& > .from) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (:is(.parent-a, .parent-b) > .from) { - .inside { - --x: 1; - } - } - " - `) - - // The `` selector is optional, so the first group in the - // prelude is not necessarily the `` selector. - expect( - optimize(css` - .parent { - @scope to (& .to) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope to (:where(:scope) .to) { - .parent .inside { - --x: 1; - } - } - " - `) - - // The parent selector is substituted with the same logic as the nesting - // selector in regular style rules: `:is(…)` is used when substituting the - // parent as-is would change the meaning (e.g. a type selector cannot - // appear after a class selector in a compound selector), … - expect( - optimize(css` - main { - @scope (.card&) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.card:is(main)) { - .inside { - --x: 1; - } - } - " - `) - - // … and dropped when the substitution is provably equivalent, even for - // complex parent selectors - expect( - optimize(css` - .a .b { - @scope (& > .from) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.a .b > .from) { - .inside { - --x: 1; - } - } - " - `) - }) - - it('should wrap bare declarations inside `@scope` in a `&` rule', async () => { - // A bug in Lightning CSS causes an error when there are declarations - // directly in the `@scope`. To fix this we will ensure a rule with - // `:where(:scope)` is available. - expect( - optimize(css` - @scope (.from) to (.to) { - --x: 1; - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) to (.to) { - :where(:scope) { - --x: 1; - } - } - " - `) - - // When generated by a variant, the declarations resolve against the - // parent rule: the `@scope` rule scopes _where the utility applies_ - // (like nesting `@media` does, and like variants do). - // - // Note: this deviates from native CSS nesting, where the `` - // selector would be relative to `.parent` and the declarations would - // apply to the scoping root. User-authored `@scope` rules (without the - // `@in-variant` marker) keep those native semantics. - expect( - optimize(css` - .parent { - @in-variant { - @scope (.from) { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) { - .parent { - --x: 1; - } - } - " - `) - }) - - it('should replace `&` with `:where(:scope)` for rules directly inside `@scope`', async () => { - // Inside an `@scope` rule, the nesting selector behaves like - // `:where(:scope)` and matches the scoping root with zero specificity. - expect( - optimize(css` - @scope (.from) { - & > p { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) { - :where(:scope) > p { - --x: 1; - } - } - " - `) - }) - - it('should handle `&` deeply nested in `@scope` as normal references to a parent', async () => { - expect( - optimize(css` - @scope (.from) { - &, - .inside { - --x: 1; - - &:hover { - --x: 2; - } - - &:focus { - --x: 3; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) { - :where(:scope), .inside { - --x: 1; - &:hover { - --x: 2; - } - &:focus { - --x: 3; - } - } - } - " - `) - }) - - it('should compile user-authored `@scope` rules with their native CSS nesting semantics', async () => { - // The `` selector is relative to the parent rule (like - // nested style rule selectors are), the rules inside are relative to - // the scoping root, and `&` refers to the scoping root - expect( - optimize(css` - .parent { - @scope (.from) to (.to) { - .inside { - --x: 1; - } - - & > .child { - --x: 2; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.parent .from) to (.to) { - .inside { - --x: 1; - } - :where(:scope) > .child { - --x: 2; - } - } - " - `) - - // A trailing `&` makes the parent rule the scoping root: the scoping - // roots are `.parent` elements inside `.from` elements - expect( - optimize(css` - .parent { - @scope (.from &) to (.to) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from .parent) to (.to) { - .inside { - --x: 1; - } - } - " - `) - - // Bare declarations apply to the scoping root. They are wrapped in a - // `:where(:scope)` rule, which is the spec's exact equivalent, because - // Lightning CSS drops bare declarations inside `@scope` entirely. - expect( - optimize(css` - .parent { - @scope (.from) { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.parent .from) { - :where(:scope) { - --x: 1; - } - } - " - `) - - // Also at the top level - expect( - optimize(css` - @scope (.from) to (.to) { - --x: 1; - - .inside { - --x: 2; - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) to (.to) { - :where(:scope) { - --x: 1; - } - .inside { - --x: 2; - } - } - " - `) - - // Every selector in a `` selector list is relative to the - // parent rule, whether it uses `&` or not - expect( - optimize(css` - .parent { - @scope (.a, & > .b) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.parent .a, .parent > .b) { - .inside { - --x: 1; - } - } - " - `) - }) - - it('should leave `@property` and `@apply` alone', async () => { - expect( - optimize(css` - .foo { - .bar { - @apply text-red-500 hover:text-red-600; - } - } - - .baz { - @property --tw-content { - syntax: '*'; - initial-value: ''; - inherits: false; - } - - @property --tw-border-spacing-x { - syntax: ''; - inherits: false; - initial-value: 0; - } - } - `), - ).toMatchInlineSnapshot(` - " - .foo .bar { - @apply text-red-500 hover:text-red-600; - } - .baz { - @property --tw-content { - syntax: '*'; - initial-value: ''; - inherits: false; - } - @property --tw-border-spacing-x { - syntax: ''; - inherits: false; - initial-value: 0; - } - } - " - `) - }) - - it('should merge a body-less @layer with an @layer with the same name that has a body', async () => { - expect( - optimize(css` - @layer a; - - @layer a { - @layer b { - .x { - color: red; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @layer a { - @layer b { - .x { - color: red; - } - } - } - " - `) - }) - - it('should not get rid of `:is(…)` when the compound selector is part of a complex selector', async () => { - expect( - optimize(css` - .foo .bar { - .system &:focus { - --x: 1; - } - } - .foo:hover { - .system &:focus { - --x: 2; - } - } - `), - ).toMatchInlineSnapshot(` - " - .system :is(.foo .bar):focus { - --x: 1; - } - .system .foo:hover:focus { - --x: 2; - } - " - `) - }) - - it('should not dedupe adjacent at-rules with the same prelude but different bodies', async () => { - expect( - optimize(css` - @font-face { - font-family: 'A'; - src: url('/fonts/a.woff2'); - } - - @font-face { - font-family: 'B'; - src: url('/fonts/b.woff2'); - } - - @keyframes spin { - to { - transform: rotate(360deg); - } - } - - @keyframes spin { - to { - transform: rotate(-360deg); - } - } - `), - ).toMatchInlineSnapshot(` - " - @font-face { - font-family: 'A'; - src: url('/fonts/a.woff2'); - } - @font-face { - font-family: 'B'; - src: url('/fonts/b.woff2'); - } - @keyframes spin { - to { - transform: rotate(360deg); - } - } - @keyframes spin { - to { - transform: rotate(-360deg); - } - } - " - `) - }) + if (node.kind === 'at-rule') return WalkAction.Replace(node.nodes) }) }) + +it('should only visit children once when calling `replaceWith` with multi-element array', () => { + let visited = new Set() + + let ast: AstNode[] = [ + atRule('@media', '', [ + context({}, [ + styleRule('.foo', [decl('color', 'red')]), + styleRule('.baz', [decl('color', 'blue')]), + ]), + ]), + styleRule('.bar', [decl('color', 'green')]), + ] + + walk(ast, (node) => { + let key = id(node) + if (visited.has(key)) { + throw new Error('Visited node twice') + } + visited.add(key) + + if (node.kind === 'at-rule') return WalkAction.Replace(node.nodes) + }) + + expect(visited).toMatchInlineSnapshot(` + Set { + "@media ", + "", + ".foo", + "color: red", + ".baz", + "color: blue", + ".bar", + "color: green", + } + `) +}) + +it('should never visit children when calling `replaceWith` with `WalkAction.Skip`', () => { + let visited = new Set() + + let inner = styleRule('.foo', [decl('color', 'blue')]) + + let ast: AstNode[] = [atRule('@media', '', [inner]), styleRule('.bar', [decl('color', 'blue')])] + + walk(ast, (node) => { + visited.add(node) + + if (node.kind === 'at-rule') { + return WalkAction.ReplaceSkip(node.nodes) + } + }) + + expect(visited).not.toContain(inner) + expect(visited).toMatchInlineSnapshot(` + Set { + { + "kind": "at-rule", + "name": "@media", + "nodes": [ + { + "kind": "rule", + "nodes": [ + { + "important": false, + "kind": "declaration", + "property": "color", + "value": "blue", + }, + ], + "selector": ".foo", + }, + ], + "params": "", + }, + { + "kind": "rule", + "nodes": [ + { + "important": false, + "kind": "declaration", + "property": "color", + "value": "blue", + }, + ], + "selector": ".bar", + }, + { + "important": false, + "kind": "declaration", + "property": "color", + "value": "blue", + }, + } + `) +}) + +it('should skip the correct number of children based on the replaced children nodes', () => { + { + let ast = [ + decl('--index', '0'), + decl('--index', '1'), + decl('--index', '2'), + decl('--index', '3'), + decl('--index', '4'), + ] + let visited: string[] = [] + walk(ast, (node) => { + visited.push(id(node)) + if (node.kind === 'declaration' && node.value === '2') { + return WalkAction.ReplaceSkip([]) + } + }) + + expect(visited).toMatchInlineSnapshot(` + [ + "--index: 0", + "--index: 1", + "--index: 2", + "--index: 3", + "--index: 4", + ] + `) + } + + { + let ast = [ + decl('--index', '0'), + decl('--index', '1'), + decl('--index', '2'), + decl('--index', '3'), + decl('--index', '4'), + ] + let visited: string[] = [] + walk(ast, (node) => { + visited.push(id(node)) + if (node.kind === 'declaration' && node.value === '2') { + return WalkAction.Replace([]) + } + }) + + expect(visited).toMatchInlineSnapshot(` + [ + "--index: 0", + "--index: 1", + "--index: 2", + "--index: 3", + "--index: 4", + ] + `) + } + + { + let ast = [ + decl('--index', '0'), + decl('--index', '1'), + decl('--index', '2'), + decl('--index', '3'), + decl('--index', '4'), + ] + let visited: string[] = [] + walk(ast, (node) => { + visited.push(id(node)) + if (node.kind === 'declaration' && node.value === '2') { + return WalkAction.ReplaceSkip([decl('--index', '2.1')]) + } + }) + + expect(visited).toMatchInlineSnapshot(` + [ + "--index: 0", + "--index: 1", + "--index: 2", + "--index: 3", + "--index: 4", + ] + `) + } + + { + let ast = [ + decl('--index', '0'), + decl('--index', '1'), + decl('--index', '2'), + decl('--index', '3'), + decl('--index', '4'), + ] + let visited: string[] = [] + walk(ast, (node) => { + visited.push(id(node)) + if (node.kind === 'declaration' && node.value === '2') { + return WalkAction.Replace([decl('--index', '2.1')]) + } + }) + + expect(visited).toMatchInlineSnapshot(` + [ + "--index: 0", + "--index: 1", + "--index: 2", + "--index: 2.1", + "--index: 3", + "--index: 4", + ] + `) + } + + { + let ast = [ + decl('--index', '0'), + decl('--index', '1'), + decl('--index', '2'), + decl('--index', '3'), + decl('--index', '4'), + ] + let visited: string[] = [] + walk(ast, (node) => { + visited.push(id(node)) + if (node.kind === 'declaration' && node.value === '2') { + return WalkAction.ReplaceSkip([decl('--index', '2.1'), decl('--index', '2.2')]) + } + }) + + expect(visited).toMatchInlineSnapshot(` + [ + "--index: 0", + "--index: 1", + "--index: 2", + "--index: 3", + "--index: 4", + ] + `) + } + + { + let ast = [ + decl('--index', '0'), + decl('--index', '1'), + decl('--index', '2'), + decl('--index', '3'), + decl('--index', '4'), + ] + let visited: string[] = [] + walk(ast, (node) => { + visited.push(id(node)) + if (node.kind === 'declaration' && node.value === '2') { + return WalkAction.Replace([decl('--index', '2.1'), decl('--index', '2.2')]) + } + }) + + expect(visited).toMatchInlineSnapshot(` + [ + "--index: 0", + "--index: 1", + "--index: 2", + "--index: 2.1", + "--index: 2.2", + "--index: 3", + "--index: 4", + ] + `) + } +}) + +function id(node: AstNode) { + switch (node.kind) { + case 'at-rule': + return `${node.name} ${node.params}` + case 'rule': + return node.selector + case 'context': + return '' + case 'at-root': + return '' + case 'declaration': + return `${node.property}: ${node.value}` + case 'comment': + return `// ${node.value}` + default: + node satisfies never + throw new Error('Unknown node kind') + } +} diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 09c3ea051..4069268c1 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -1,17 +1,14 @@ import { Polyfills } from '.' -import * as SelectorParser from '../src/selector-parser' import { parseAtRule } from './css-parser' import type { DesignSystem } from './design-system' import type { Source, SourceLocation } from './source-maps/source' import { Theme, ThemeOptions } from './theme' import { DefaultMap } from './utils/default-map' -import { segment } from './utils/segment' import { extractUsedVariables } from './utils/variables' import * as ValueParser from './value-parser' import { walk, WalkAction, type VisitContext } from './walk' const AT_SIGN = 0x40 -const PIPE = 0x7c export type StyleRule = { kind: 'rule' @@ -193,8 +190,6 @@ export function cssContext( ): VisitContext & { context: Record } { return { depth: ctx.depth, - index: ctx.index, - siblings: ctx.siblings, get context() { let context: Record = {} for (let child of ctx.path()) { @@ -280,19 +275,13 @@ export function optimizeAst( // Track used animation names if (node.property === 'animation') { - for (let keyframeName of extractKeyframeNames(node.value)) { + for (let keyframeName of extractKeyframeNames(node.value)) usedKeyframeNames.add(keyframeName) - } } // Create fallback values for usages of the `color-mix(…)` function that reference variables - // found in the theme config. - if ( - polyfills & Polyfills.ColorMix && - !context.supportsColorMix && - !context.keyframes && - node.value.includes('color-mix(') - ) { + // found in the theme config. + if (polyfills & Polyfills.ColorMix && node.value.includes('color-mix(')) { colorMixDeclarations.get(parent).add(node) } @@ -307,7 +296,37 @@ export function optimizeAst( transform(child, nodes, context, depth + 1) } - if (nodes.length > 0) { + // Keep the last decl when there are exact duplicates. Keeping the *first* one might + // not be correct when given nested rules where a rule sits between declarations. + let seen: Record = {} + let toRemove = new Set() + + // Keep track of all nodes that produce a given declaration + for (let child of nodes) { + if (child.kind !== 'declaration') continue + + let key = `${child.property}:${child.value}:${child.important}` + seen[key] ??= [] + seen[key].push(child) + } + + // And remove all but the last of each + for (let key in seen) { + for (let i = 0; i < seen[key].length - 1; ++i) { + toRemove.add(seen[key][i]) + } + } + + if (toRemove.size > 0) { + nodes = nodes.filter((node) => !toRemove.has(node)) + } + + if (nodes.length === 0) return + + // Rules with `&` as the selector should be flattened + if (node.selector === '&') { + parent.push(...nodes) + } else { parent.push({ ...node, nodes }) } } @@ -358,8 +377,6 @@ export function optimizeAst( else if (node.kind === 'at-rule') { if (node.name === '@keyframes') { context = { ...context, keyframes: true } - } else if (node.name === '@supports' && node.params.includes('color-mix(')) { - context = { ...context, supportsColorMix: true } } let copy = { ...node, nodes: [] } @@ -378,8 +395,7 @@ export function optimizeAst( copy.name === '@charset' || copy.name === '@custom-media' || copy.name === '@namespace' || - copy.name === '@import' || - copy.name === '@apply' + copy.name === '@import' ) { parent.push(copy) } @@ -401,21 +417,6 @@ export function optimizeAst( // Remove reference imports from printing if (node.context.reference) { return - } - - // Preserve context nodes that track the source of their subtree (variant - // bodies and the user content slotted into them), so `handleNesting` - // knows whether to compose `@scope` rules with the variant semantics or - // their native CSS nesting semantics - else if (node.context.source) { - let copy = { ...node, nodes: [] as AstNode[] } - for (let child of node.nodes) { - transform(child, copy.nodes, { ...context, ...node.context }, depth) - } - - if (copy.nodes.length > 0) { - parent.push(copy) - } } else { for (let child of node.nodes) { transform(child, parent, { ...context, ...node.context }, depth) @@ -667,975 +668,9 @@ export function optimizeAst( } } - return handleNesting(newAst) + return newAst } -export function handleNesting(ast: AstNode[]): AstNode[] { - let parseSelectorCache = new DefaultMap(SelectorParser.parse) - - // Nodes with an `@scope` rule somewhere in their subtree. These cannot use - // the "emit as-is" fast path for rules containing declarations, because - // `@scope` rules change meaning when they stay nested inside a style rule and - // must go through the hoisting machinery instead. - let containsAtScope = new Set() - { - function scan(nodes: AstNode[]): boolean { - let found = false - for (let node of nodes) { - if (node.kind === 'declaration' || node.kind === 'comment') continue - - if (scan(node.nodes) || (node.kind === 'at-rule' && node.name === '@scope')) { - containsAtScope.add(node) - found = true - } - } - return found - } - scan(ast) - } - - // Track `rule` selectors as we go - let selectorStack: [ - selector: string, - src: SourceLocation | undefined, - dst: SourceLocation | undefined, - ][] = [] - - // Track `at-rule` information as we go. Tracking this separately from the - // selector stack for rules such that we can hoist this above all the rules. - let atRuleStack: [ - name: string, - params: string, - src: SourceLocation | undefined, - dst: SourceLocation | undefined, - ][] = [] - - // The current "nodes" we can push to - let nodes = null as AstNode[] | null - - // Optimization: Track the declaration properties we've seen in the current - // nodes. - let seenDeclarationProperties = new Set() - - // Track nodes lists where we want to dedupe declarations - let dedupeDeclarationsInNodes = new Set() - - // The final, new AST - let result: AstNode[] = [] - - // Track whether we should skip a node in the `exit` phase - let skipExit = new Set() - - // Track the source of the subtree we are currently in. Variant bodies are - // wrapped in a `context` node with `source: 'variant'`, and the user content - // slotted into them is wrapped in `source: 'user'`. `@scope` rules whose - // nearest source is a variant are hoisted with the variant semantics, all - // other `@scope` rules use their native CSS nesting semantics. - let sourceStack: (string | boolean)[] = [] - - // Selector stacks that are masked while inside an `@scope` rule whose - // `` selector consumed the parent selector via `&`. - // - // Once the parent selector is part of the `` selector, the rules - // inside the `@scope` are relative to the scoping root, not to the parent - // rule. Selectors in scoped style rules only match elements in scope, and the - // parent matches ancestors of the scoping root, so prefixing the rules with - // the parent selector would make them match nothing at all. - // - // E.g.: - // - // ```css - // .parent { - // @scope (& > .scope) { - // .content { - // color: red; - // } - // } - // } - // ``` - // - // becomes: - // - // ```css - // @scope (.parent > .scope) { - // .content { /* ← not `.parent .content` */ - // color: red; - // } - // } - // ``` - let maskedSelectorStacks = new Map() - - walk(ast, { - enter(node) { - switch (node.kind) { - case 'rule': { - nodes = null // Start a new level - - // First time we see a rule - if (selectorStack.length === 0) { - // A rule with a selector containing `&` should replace the `&` with - // `:scope` if there is no parent rule. - // - // Note: there could be false positives when the `&` is escaped or - // part of a string inside an attribute selector. But the - // SelectorParser will take care of that. - if (node.selector.includes('&')) { - // Inside an `@scope` rule, the nesting selector behaves like - // `:where(:scope)` and matches the scoping root with zero - // specificity. E.g.: - // - // ```css - // @scope (.checkout) { - // & > p { - // color: red; - // } - // } - // ``` - // - // becomes: - // - // ```css - // @scope (.checkout) { - // :where(:scope) > p { - // color: red; - // } - // } - // ``` - // - // See: https://drafts.csswg.org/css-nesting-1/#nesting-at-scope - let replacement = atRuleStack.some(([name]) => name === '@scope') - ? ':where(:scope)' - : ':scope' - - let ast = SelectorParser.parse(node.selector) - let changed = false - - walk(ast, (node) => { - if (node.kind === 'selector' && node.value === '&') { - changed = true - node.value = replacement - } - }) - - if (changed) { - selectorStack.push([SelectorParser.toCss(ast), node.src, node.dst]) - } else { - selectorStack.push([node.selector, node.src, node.dst]) - } - } - - // No nesting markers, track as-is - else { - selectorStack.push([node.selector, node.src, node.dst]) - } - } - - // Nested rule, ensure `&` is present in each selector. Then track the - // selector. - else { - // A rule with just `&` can be replaced by its children. Let's - // ignore this node and keep walking its children. - if (node.selector === '&') { - skipExit.add(node) - return - } - - let lastSelector = selectorStack[selectorStack.length - 1][0] - let selector = segment(node.selector, ',') - .map((selector) => substituteNestingSelector(selector, lastSelector)) - .join(', ') - selectorStack.push([selector, node.src, node.dst]) - } - - // Once we hit a rule that has at least one declaration, then we can - // stop handling the nested selectors. - // - // This ensures that browser devtools can at least show _something_ - // for a given rule. This also means that we can leverage CSS nesting - // which is better for gzip results due to increased repetition. - // - // E.g.: - // - // ```css - // .a { - // &[b] { - // color: red; - // &:hover { - // color: blue; - // } - // } - // } - // ``` - // - // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ - // - // ```css - // .a[b] { /* ← flattened */ - // color: red; /* ← saw a declaration, therefore */ - // &:hover { /* ← we keep this nested syntax */ - // color: blue; - // } - // } - // ``` - // - // The exception is when the rule contains an `@scope` rule somewhere, - // because it must go through the normal hoisting machinery instead. - // See `containsAtScope` for more details. - if ( - node.nodes.some((child) => child.kind === 'declaration') && - !containsAtScope.has(node) - ) { - // Emitting each child instead of the node itself because we do want - // to flatten the current node and its selector that is already - // pushed to the stack. - for (let child of node.nodes) emit(child) - return WalkAction.Skip - } - break - } - - case 'at-rule': { - nodes = null // Start a new level - - // `@layer` is hoistable, but when it's empty then we have to make - // sure that we still emit it because this might influence the layer - // order. We can't just get rid of it. - if (node.nodes.length === 0 && !DROPPABLE_IF_EMPTY_AT_RULES.has(node.name)) { - emit(node) - skipExit.add(node) - return WalkAction.Skip - } - - // Hoist at-rules - else if (HOISTABLE_AT_RULES.has(node.name)) { - let params = node.params - - // `@scope` is the only hoistable at-rule with selectors in its - // prelude, and the only one that changes meaning when it stays - // nested inside a style rule. Resolve the prelude and its contents - // before hoisting, using one of two composition modes: - // - // 1. Variant-generated `@scope` rules attach the parent selector - // (the utility) to the rules _inside_ the scope: - // - // ```css - // .scoped\:underline { - // @scope (.from) to (.to) { - // text-decoration-line: underline; - // } - // } - // ``` - // - // becomes: - // - // ```css - // @scope (.from) to (.to) { - // .scoped\:underline { - // text-decoration-line: underline; - // } - // } - // ``` - // This way the utility is properly sandwiched between the - // `.from` and `.to` boundaries. - // - // 2. User-authored `@scope` rules keep their native CSS nesting - // semantics: the `` selector is relative to the parent - // rule, and the rules inside are relative to the scoping root: - // - // ```css - // .parent { - // @scope (.from) { - // color: red; - // } - // } - // ``` - // - // becomes: - // - // ```css - // @scope (.parent .from) { - // :where(:scope) { - // color: red; - // } - // } - // ``` - // - // In a perfect world we leave this alone, but Lightning CSS - // seems to incorrectly transform this into a version where the - // `.parent` class is gone entirely: - // - // ```css - // @scope (.from) { - // color: red; - // } - // ``` - // - // See: https://drafts.csswg.org/css-nesting-1/#nesting-at-scope - if (node.name === '@scope') { - let isVariant = sourceStack[sourceStack.length - 1] === 'variant' - let hasParent = selectorStack.length > 0 - - // Lightning CSS bug: This snippet will error - // - // ```css - // @scope { - // --x: 1; - // } - // ``` - // - // We will wrap these bare-declarations in a `&` rule to fix the crash: - // ```css - // @scope { - // & { - // --x: 1; - // } - // } - // ``` - { - let queue: (AtRule | Context)[] = [node] - - for (let parent of queue) { - let newNodes: AstNode[] = [] - let group: AstNode[] | null = null - - for (let child of parent.nodes) { - if (child.kind === 'context') queue.push(child) - - if (child.kind === 'declaration' || child.kind === 'comment') { - if (group === null) { - group = [] - newNodes.push(styleRule('&', group)) - } - group.push(child) - } else { - group = null - newNodes.push(child) - } - } - - parent.nodes = newNodes - } - } - - if (params.includes('&') || (!isVariant && hasParent)) { - // The selector to substitute for `&` in the `` - // selector: the elements matched by the nearest ancestor style - // rule, or `:scope` when there is none. - let parentSelector = hasParent ? selectorStack[selectorStack.length - 1][0] : null - - let paramsAst = ValueParser.parse(params) - - // Whether the current `(…)` group is a `` selector. - // - // ```css - // @scope (.from) to (.to) - // ^^^^^^^ scope-start (optional) - // ^^^^^ scope-end - // - // @scope to (.to) - // ^^^^^ scope-end - // ``` - let isScopeEnd = false - - // Whether the `` selector consumed the parent - // selector - let consumedParent = false - - for (let paramNode of paramsAst) { - if (paramNode.kind === 'word' && paramNode.value === 'to') { - isScopeEnd = true - continue - } - - // A `(…)` group parses as a function call without a name - if (paramNode.kind !== 'function' || paramNode.value !== '') continue - - let selector = ValueParser.toCss(paramNode.nodes) - - // A user-authored `` selector is relative to the - // parent rule, and resolves exactly like nested style rule - // selectors do: `&` refers to the elements matched by the - // parent rule, and selectors without `&` are prefixed with - // it. - if (!isScopeEnd && !isVariant && parentSelector !== null) { - paramNode.nodes = [ - ValueParser.word( - segment(selector, ',') - .map((part) => substituteNestingSelector(part.trim(), parentSelector)) - .join(', '), - ), - ] - consumedParent = true - } - - // Otherwise, only an explicit `&` is resolved. In the - // `` selector, `&` refers to the elements - // matched by the nearest ancestor style rule (or `:scope` - // when there is none). In the `` selector, `&` - // refers to the scoping root and behaves like - // `:where(:scope)`. - else if (selector.includes('&')) { - let replacement = isScopeEnd ? ':where(:scope)' : (parentSelector ?? ':scope') - - paramNode.nodes = [ - ValueParser.word(substituteNestingSelector(selector, replacement)), - ] - if (!isScopeEnd && hasParent) consumedParent = true - } - } - - params = ValueParser.toCss(paramsAst) - - // Mask the selector stack for the `@scope` rule's subtree, so - // its rules resolve like top-level rules within the scope (and - // `&` resolves to `:where(:scope)`). The stack is restored in - // the exit phase. See `maskedSelectorStacks` for more details. - if (consumedParent) { - maskedSelectorStacks.set(node, selectorStack) - selectorStack = [] - } - } - } - - atRuleStack.push([node.name, params, node.src, node.dst]) - } - - // If we can't hoist them, emit them immediately as-is - else { - emit(node) - skipExit.add(node) - return WalkAction.Skip - } - break - } - - case 'declaration': - case 'comment': { - emit(node) - break - } - - case 'context': - if (node.context.source) sourceStack.push(node.context.source) - break - - case 'at-root': - break - - default: - node satisfies never - break - } - }, - exit(node) { - if (skipExit.delete(node)) return - - switch (node.kind) { - case 'rule': { - nodes = null - selectorStack.pop() - break - } - - case 'at-rule': { - nodes = null - atRuleStack.pop() - - // Restore the selector stack that was masked by this `@scope` rule - let maskedSelectorStack = maskedSelectorStacks.get(node) - if (maskedSelectorStack) { - selectorStack = maskedSelectorStack - maskedSelectorStacks.delete(node) - } - break - } - - case 'context': - if (node.context.source) sourceStack.pop() - break - - case 'declaration': - case 'comment': - case 'at-root': - break - - default: - node satisfies never - break - } - }, - }) - - // Dedupe declarations that we've already seen before if they match the - // `property`, `value` and `important` information. - { - for (let nodes of dedupeDeclarationsInNodes) { - let seen = new Set() - for (let i = nodes.length - 1; i >= 0; --i) { - let node = nodes[i] - if (node.kind !== 'declaration') continue - - let id = `${node.property}\0${node.value}\0${node.important}` - - if (seen.has(id)) nodes.splice(i, 1) - else seen.add(id) - } - } - } - - return result - - function emit(node: AstNode) { - // Existing nodes are available, emit into those nodes - if (nodes) { - // Optimization: track used declarations in the current node. - if (node.kind === 'declaration') { - if (seenDeclarationProperties.has(node.property)) { - dedupeDeclarationsInNodes.add(nodes) - } else { - seenDeclarationProperties.add(node.property) - } - } - - nodes.push(node) - return - } - - // Nothing available, setup a fresh node - { - // There are no parent rules or at-rules available, which means that we - // can emit the node as-is. - if (selectorStack.length === 0 && atRuleStack.length === 0) { - let target = result - let lastNode = target[target.length - 1] - - // Optimization: when the current and last node are the same, ignore the - // new node entirely otherwise we will get unnecessary duplicate - // results. - // - // We only care about at-rules with no body because some of them (such - // as `@charset` or `@layer`) need to be emitted. A normal rule that's - // empty doesn't need to be emitted. At-rules _with_ a body (such as - // `@font-face` or `@keyframes`) can share the same prelude while - // containing different bodies, so they must all be emitted. - if ( - lastNode && - lastNode.kind === 'at-rule' && - node.kind === 'at-rule' && - lastNode.nodes.length === 0 && - node.nodes.length === 0 && - lastNode.name === node.name && - lastNode.params === node.params - ) { - return - } - - result.push(node) - return - } - - // Track the new "parent" nodes - { - nodes = [node] - - // Clear out seen declarations from the previous work in progress nodes - seenDeclarationProperties.clear() - - // Track new declaration - if (node.kind === 'declaration') { - seenDeclarationProperties.add(node.property) - } - } - - // Track the new root node that we build up to store in the final AST - let root = null as AstNode | null - - let target = result - let atRuleOffset = 0 - - // Optimization: merge adjacent at-rules - // - // Figure out whether we can push our new node into a previous node that - // was already emitted. - // - // We have to make sure that the order stays the same, so therefore we - // only ever have to look at the last node that was emitted. - { - let lastNode = target[target.length - 1] - if (lastNode && lastNode.kind === 'at-rule') { - for (let i = 0; i < atRuleStack.length; i++) { - let atRule = atRuleStack[i] - if (!lastNode) break - if (lastNode.kind !== 'at-rule') break - if (lastNode.name !== atRule[0]) break - if (lastNode.params !== atRule[1]) break - - atRuleOffset++ - target = lastNode.nodes - lastNode = lastNode.nodes[lastNode.nodes.length - 1] - } - } - } - - // Build up the rule - if (selectorStack.length > 0) { - let [selector, src, dst] = selectorStack[selectorStack.length - 1] - - // Optimization: merge adjacent rules with the same selector - // - // Figure out whether we can push into an existing rule. - // - // If we have some at-rules that we have to keep into account, then we - // definitely can't. - if (atRuleStack.length - atRuleOffset <= 0) { - let lastNode = target[target.length - 1] - if (lastNode && lastNode.kind === 'rule' && lastNode.selector === selector) { - lastNode.nodes.push(...nodes) - - // Ensure that our current nodes points to the nodes of the - // `lastNode`, otherwise we will lose information. - nodes = lastNode.nodes - - // We appended a group that could contain declarations already in the - // existing rule, so let the final dedupe pass handle it once. - // - // Note: we could loop over _all_ previous nodes to figure out if we - // really want to dedupe this. But this could result in a bunch of - // duplicate work if we have `n` nodes that we want to merge - // together. - dedupeDeclarationsInNodes.add(nodes) - - // We know that we don't have to handle any more at-rules, so we can - // bail early since we just merged the nodes with the same selector. - return - } - } - - // Can't push into existing node, create a new node - root = rule(selector, nodes) - if (src || dst) Object.assign(root, { src, dst }) - } - - // Wrap in at-rules, if we can push into an existing node then we can - // ignore `offset` amount of nodes since the `root`/`nodes` will already - // point to a nested node. - for (let i = atRuleStack.length - 1; i >= atRuleOffset; --i) { - let [name, params, src, dst] = atRuleStack[i] - - root = atRule(name, params, root ? [root] : nodes) - if (src || dst) Object.assign(root, { src, dst }) - } - - // Track the root node in the AST - if (root) { - target.push(root) - } - - // We didn't build up any new root, so we can move our node directly into - // the target. This can happen when we emit a node that is not a - // declaration or a comment. - else { - target.push(...nodes) - } - } - } - - // Substitutes the `&` selector with the parent selector with `:is(…)` semantics. - // - // If the `&` is not found, we will prepend it using a descendant combinator. - // E.g.: `& ${selector}` → `${parentSelector} ${selector}` - // - // In some situations we can get rid of the `:is(…)` wrapper, but only if it's safe to do so. - function substituteNestingSelector(selector: string, parentSelector: string): string { - // Slow path: we need to replace the `&` with the parent selector. A simple - // `replaceAll(…)` won't work because a `&` could be escaped, or could be - // part of an attribute selector. - // - // Much safer to parse the selector and replace the `&` that way - if (selector.includes('&')) { - let ast = SelectorParser.parse(selector) - let changed = false - walk(ast, { - enter(node, ctx) { - if (node.kind !== 'selector' || node.value !== '&') return - - changed = true - - // Safest option: use `is(…)` semantics when substituting `&` for the - // parent selector. - node.value = `:is(${parentSelector})` - - // We should always have a parent, so this shouldn't happen - if (ctx.parent === null) return - - // Optimizations: - let parentAst = parseSelectorCache.get(parentSelector) - - // 1. If we're dealing with multiple selectors, then we know that the - // `:is(…)` needs to stay. Nothing to optimize. - if (parentAst.length === 1 && parentAst[0].kind === 'list') { - return // Keep `:is(…)` semantics - } - - // 2. We know that `&` is standalone when it's inside of a complex - // selector. E.g. `[before] & [after]` - if (ctx.parent.kind === 'complex') { - // `& [after]` - // - // `:is(…)` semantics are not required, because these are equivalent: - // - // - `:is(div) [after]` → `div [after]` - // - `:is(.x) [after]` → `.x [after]` - // - `:is([before]) [after]` → `[before] [after]` - // - `:is(.a > .b) [after]` → `.a > .b [after]` - if (ctx.index === 0) { - node.value = parentSelector - return - } - - // `[before] &` - // - // `:is(…)` semantics are required if we're dealing with a complex - // parent selector. Otherwise the meaning of the selector could - // change: - // - // - `[before] :is(div)` → `[before] div` - // - `[before] :is(.x)` → `[before] .x` - // - `[before] :is([after])` → `[before] [after]` - // - `[before] :is(.a > .b)` → `[before] :is(.a > .b)` (!) - else if (ctx.index === ctx.siblings.length - 1) { - if (parentAst[0].kind === 'complex') { - return // Keep `:is(…)` semantics - } - - node.value = parentSelector - return - } - - // `[before] & [after]` - // - // `:is(…)` semantics are required if we're dealing with a complex - // parent selector. Otherwise the meaning of the selector could - // change: - // - // - `[before] :is(div) [after]` → `[before] div [after]` - // - `[before] :is(.x) [after]` → `[before] .x [after]` - // - `[before] :is(.a > .b) [after]` → `[before] :is(.a > .b) [after]` (!) - else { - if (parentAst[0].kind === 'complex') { - return // Keep `:is(…)` semantics - } - - node.value = parentSelector - return - } - } - - // 3. We know that `&` is attached to some other selector when it's - // inside of a compound selector. - // - // E.g. `[before]&[after]` - // - // We have to be careful that our parent, when it's part of a complex - // selector, that the same rules apply - // - // E.g.: `[before] &[after]` - // ^ current node - // ^^^^^^^^ compound selector - // ^^^^^^^^^^^^^^^^^ complex selector - // - else if (ctx.parent.kind === 'compound') { - if (parentAst[0].kind === 'complex') { - let path = ctx.path() - let grandParent = path[path.length - 2] - - // When our compound parent is part of a complex selector, and it's - // not the very first node, then we can't safely get rid of the - // `:is(…)` if the last selector is a complex selector as well, - // unless the `&` maps to a single selector or compound selector. - // - // ```css - // .foo .bar { /* Complex selector */ - // .system &:focus { /* Complex selector + compound selecto*/ - // --x: 1; - // } - // } - // .foo:hover { /* Compound selector */ - // .system &:focus { /* Complex selector + compound selector */ - // --x: 2; - // } - // } - // ``` - // - // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ - // - // ```css - // .system :is(.foo .bar):focus { /* Cannot drop the `:is(…)`, otherwise `.system` and `.foo` can be swapped in the DOM */ - // --x: 1; - // } - // .system .foo:hover:focus { - // --x: 2; - // } - // ``` - if ( - grandParent && - grandParent.kind === 'complex' && - grandParent.nodes[0] !== ctx.parent - ) { - return // Keep `:is(…)` semantics - } - } - - // `&*` and `&div` are invalid CSS so these should stay invalid. They - // should be written as `*&` and `div&` instead. - if ( - ctx.siblings - .slice(ctx.index + 1) - .some( - (sibling) => - SelectorParser.isUniversalSelector(sibling) || - SelectorParser.isTypeSelector(sibling), - ) - ) { - return // Keep `:is(…)` semantics - } - - // `&[after]` - // - // `:is(…)` semantics are not required, because these are equivalent: - // - // - `:is(div)[after]` → `div[after]` - // - `:is(.x)[after]` → `.x[after]` - // - `:is([before])[after]` → `[before][after]` - // - `:is(.a > .b)[after]` → `.a > .b[after]` - if (ctx.index === 0) { - node.value = parentSelector - return - } - - // `[before]&` - // - // `:is(…)` semantics are required if we're dealing with a - // - // - complex parent selector, to prevent changing the meaning of the selector - // - a universal selector, because `*` needs to be first - // - a type selector, because `div` needs to be first - // - // - `[before]:is(div)` → `[before]:is(div)` (!) - // - `[before]:is(*)` → `[before]:is(*)` (!) - // - `[before]:is(.a > .b)` → `[before]:is(.a > .b)` (!) - // - `[before]:is(.x)` → `[before].x` - else if (ctx.index === ctx.siblings.length - 1) { - if ( - parentAst[0].kind === 'complex' || - SelectorParser.isUniversalSelector(parentAst[0]) || - SelectorParser.isTypeSelector(parentAst[0]) - ) { - return // Keep `:is(…)` semantics - } - - node.value = parentSelector - return - } - - // `[before]&[after]` - // - // `:is(…)` semantics are required if we're dealing with a - // - // - complex parent selector, to prevent changing the meaning of the selector - // - a universal selector, because `*` needs to be first - // - a type selector, because `div` needs to be first - // - // - `[before]:is(div)[after]` → `[before]:is(div)[after]` (!) - // - `[before]:is(*)[after]` → `[before]:is(*)[after]` (!) - // - `[before]:is(.a > .b)[after]` → `[before]:is(.a > .b)[after]` (!) - // - `[before]:is(.x)[after]` → `[before].x[after]` - else { - if ( - parentAst[0].kind === 'complex' || - SelectorParser.isUniversalSelector(parentAst[0]) || - SelectorParser.isTypeSelector(parentAst[0]) - ) { - return // Keep `:is(…)` semantics - } - - node.value = parentSelector - return - } - } - - // 4. When the current node is a function argument (e.g. `:not(&))`, - // then we can drop the `:is(…)` entirely. - // - // The only exception is when the parent has multiple selectors - // because then we would introduce multiple arguments. Multiple - // selectors are already handled. - else if (ctx.parent.kind === 'function') { - node.value = parentSelector - return - } - }, - exit(node, ctx) { - // Optimization: We can remove the universal selector `*` if they are - // part of a compound selector. E.g.: - // - // - `*:hover` → `:hover` - // - `*[attribute]` → `[attribute]` - // - // Except when the `*` is a namespace, e.g.: `*|div`, because `*|div` - // (any namespace) and `|div` (no namespace) have different meanings. - if ( - ctx.index === 0 && - ctx.siblings.length > 1 && - ctx.parent?.kind === 'compound' && - SelectorParser.isUniversalSelector(node) - ) { - let next = ctx.siblings[1] - if (next.kind === 'selector' && next.value.charCodeAt(0) === PIPE) { - return - } - return WalkAction.ReplaceSkip([]) - } - }, - }) - - if (changed) return SelectorParser.toCss(ast) - } - - // Fast path: we know there isn't an `&` so we can prepend the parent - // selector immediately. - let parentAst = parseSelectorCache.get(parentSelector) - return `${parentAst.length === 1 && parentAst[0].kind === 'list' ? `:is(${parentSelector})` : parentSelector} ${selector}` - } -} - -// A set of at-rules that can be hoisted to the top without any repercussions. -// Typically at-rules that rely on the environment, not parent information and -// contain other rules/declarations. -const HOISTABLE_AT_RULES = new Set([ - '@container', - '@layer', - '@media', - '@page', - '@scope', - '@starting-style', - '@supports', - '@view-transition', -]) - -// A set of at-rules that can be dropped if they don't contain any nodes. We -// don't have the distinction between an at-rule with no body, or an at-rule -// with a body that is empty right now. -const DROPPABLE_IF_EMPTY_AT_RULES = new Set([ - '@container', - '@media', - '@page', - '@scope', - '@starting-style', - '@supports', - '@view-transition', -]) - export function toCss(ast: AstNode[], track?: boolean) { let pos = 0 diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index a19123105..49fbbc043 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -1,13 +1,40 @@ import dedent from 'dedent' import { expect, test, vi } from 'vitest' +import type { Plugin } from './compat/plugin-api' import { compile, type Config } from './index' import plugin from './plugin' -import { compileCss, run } from './test-utils/run' +import { optimizeCss } from './test-utils/run' const css = dedent +async function run( + css: string, + { + loadStylesheet = () => Promise.reject(new Error('Unexpected stylesheet')), + loadModule = () => Promise.reject(new Error('Unexpected module')), + candidates = [], + optimize = true, + }: { + loadStylesheet?: ( + id: string, + base: string, + ) => Promise<{ content: string; base: string; path: string }> + loadModule?: ( + id: string, + base: string, + resourceHint: 'plugin' | 'config', + ) => Promise<{ module: Config | Plugin; base: string; path: string }> + candidates?: string[] + optimize?: boolean + }, +) { + let compiler = await compile(css, { base: '/root', loadStylesheet, loadModule }) + let result = compiler.build(candidates) + return optimize ? optimizeCss(result) : result +} + test('can resolve relative @imports', async () => { - async function loadStylesheet(id: string, base: string) { + let loadStylesheet = async (id: string, base: string) => { expect(base).toBe('/root') expect(id).toBe('./foo/bar.css') return { @@ -21,16 +48,15 @@ test('can resolve relative @imports', async () => { } } - expect( - await compileCss( + await expect( + run( css` @import './foo/bar.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - .foo { + ).resolves.toMatchInlineSnapshot(` + ".foo { color: red; } " @@ -38,7 +64,7 @@ test('can resolve relative @imports', async () => { }) test('can recursively resolve relative @imports', async () => { - async function loadStylesheet(id: string, base: string) { + let loadStylesheet = async (id: string, base: string) => { if (base === '/root' && id === './foo/bar.css') { return { content: css` @@ -62,16 +88,15 @@ test('can recursively resolve relative @imports', async () => { throw new Error(`Unexpected import: ${id}`) } - expect( - await compileCss( + await expect( + run( css` @import './foo/bar.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - .baz { + ).resolves.toMatchInlineSnapshot(` + ".baz { color: #00f; } " @@ -83,7 +108,7 @@ let exampleCSS = css` color: red; } ` -async function loadStylesheet(id: string) { +let loadStylesheet = async (id: string) => { if (!id.endsWith('example.css')) throw new Error('Unexpected import: ' + id) return { content: exampleCSS, @@ -93,46 +118,43 @@ async function loadStylesheet(id: string) { } test('extracts path from @import nodes', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - a { + ).resolves.toMatchInlineSnapshot(` + "a { color: red; } " `) - expect( - await compileCss( + await expect( + run( css` @import './example.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - a { + ).resolves.toMatchInlineSnapshot(` + "a { color: red; } " `) - expect( - await compileCss( + await expect( + run( css` @import '/example.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - a { + ).resolves.toMatchInlineSnapshot(` + "a { color: red; } " @@ -140,96 +162,71 @@ test('extracts path from @import nodes', async () => { }) test('url() imports are passed-through', async () => { - expect( - await compileCss( + await expect( + run( css` @import url('example.css'); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "example.css"; - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url('example.css');"`) - expect( - await compileCss( + await expect( + run( css` @import url('./example.css'); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "./example.css"; - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url('./example.css');"`) - expect( - await compileCss( + await expect( + run( css` @import url('/example.css'); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "/example.css"; - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url('/example.css');"`) - expect( - await compileCss( + await expect( + run( css` @import url(example.css); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "example.css"; - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url(example.css);"`) - expect( - await compileCss( + await expect( + run( css` @import url(./example.css); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "./example.css"; - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url(./example.css);"`) - expect( - await compileCss( + await expect( + run( css` @import url(/example.css); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "/example.css"; - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url(/example.css);"`) }) test('handles case-insensitive @import directive', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - a { + ).resolves.toMatchInlineSnapshot(` + "a { color: red; } " @@ -237,16 +234,15 @@ test('handles case-insensitive @import directive', async () => { }) test('@media', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css' print; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @media print { + ).resolves.toMatchInlineSnapshot(` + "@media print { a { color: red; } @@ -254,16 +250,15 @@ test('@media', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' print, screen; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @media print, screen { + ).resolves.toMatchInlineSnapshot(` + "@media print, screen { a { color: red; } @@ -271,16 +266,15 @@ test('@media', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' screen and (orientation: landscape); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @media screen and (orientation: landscape) { + ).resolves.toMatchInlineSnapshot(` + "@media screen and (orientation: landscape) { a { color: red; } @@ -288,16 +282,15 @@ test('@media', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' foo(bar); `, - { base: '/root', loadStylesheet }, + { loadStylesheet, optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @media foo(bar) { + ).resolves.toMatchInlineSnapshot(` + "@media foo(bar) { a { color: red; } @@ -307,16 +300,15 @@ test('@media', async () => { }) test('@supports', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css' supports(display: grid); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @supports (display: grid) { + ).resolves.toMatchInlineSnapshot(` + "@supports (display: grid) { a { color: red; } @@ -324,16 +316,15 @@ test('@supports', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' supports(display: grid) screen and (max-width: 400px); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @supports (display: grid) { + ).resolves.toMatchInlineSnapshot(` + "@supports (display: grid) { @media screen and (max-width: 400px) { a { color: red; @@ -343,17 +334,16 @@ test('@supports', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' supports((not (display: grid)) and (display: flex)) screen and (max-width: 400px); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @supports (not (display: grid)) and (display: flex) { + ).resolves.toMatchInlineSnapshot(` + "@supports (not (display: grid)) and (display: flex) { @media screen and (max-width: 400px) { a { color: red; @@ -363,18 +353,17 @@ test('@supports', async () => { " `) - expect( - await compileCss( + await expect( + run( // prettier-ignore css` @import 'example.css' supports((selector(h2 > p)) and (font-tech(color-COLRv1))); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @supports selector(h2 > p) and font-tech(color-COLRv1) { + ).resolves.toMatchInlineSnapshot(` + "@supports selector(h2 > p) and font-tech(color-COLRv1) { a { color: red; } @@ -384,16 +373,15 @@ test('@supports', async () => { }) test('@layer', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css' layer(utilities); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + ).resolves.toMatchInlineSnapshot(` + "@layer utilities { a { color: red; } @@ -401,16 +389,15 @@ test('@layer', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' layer(); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @layer { + ).resolves.toMatchInlineSnapshot(` + "@layer { a { color: red; } @@ -420,15 +407,13 @@ test('@layer', async () => { }) test('supports theme(reference) imports', async () => { - expect( - await run( - ['text-red-500'], + await expect( + run( css` @tailwind utilities; @import 'example.css' theme(reference); `, { - base: '/root', loadStylesheet: () => Promise.resolve({ content: css` @@ -439,11 +424,11 @@ test('supports theme(reference) imports', async () => { base: '', path: '', }), + candidates: ['text-red-500'], }, ), - ).toMatchInlineSnapshot(` - " - .text-red-500 { + ).resolves.toMatchInlineSnapshot(` + ".text-red-500 { color: var(--color-red-500, red); } " @@ -451,27 +436,28 @@ test('supports theme(reference) imports', async () => { }) test('updates the base when loading modules inside nested files', async () => { - async function loadStylesheet() { - return { + let loadStylesheet = () => + Promise.resolve({ content: css` @config './nested-config.js'; @plugin './nested-plugin.js'; `, base: '/root/foo', path: '', - } - } - using loadModule = vi.fn().mockResolvedValue({ base: '', path: '', module: () => {} }) + }) + let loadModule = vi.fn().mockResolvedValue({ base: '', path: '', module: () => {} }) expect( - await compileCss( - css` - @import './foo/bar.css'; - @config './root-config.js'; - @plugin './root-plugin.js'; - `, - { base: '/root', loadStylesheet, loadModule }, - ), + ( + await run( + css` + @import './foo/bar.css'; + @config './root-config.js'; + @plugin './root-plugin.js'; + `, + { loadStylesheet, loadModule }, + ) + ).trim(), ).toBe('') expect(loadModule).toHaveBeenNthCalledWith(1, './nested-config.js', '/root/foo', 'config') @@ -481,15 +467,14 @@ test('updates the base when loading modules inside nested files', async () => { }) test('emits the right base for @source directives inside nested files', async () => { - async function loadStylesheet() { - return { + let loadStylesheet = () => + Promise.resolve({ content: css` @source './nested/**/*.css'; `, base: '/root/foo', path: '', - } - } + }) let compiler = await compile( css` @@ -506,18 +491,16 @@ test('emits the right base for @source directives inside nested files', async () }) test('emits the right base for @source found inside JS configs and plugins from nested imports', async () => { - async function loadStylesheet() { - return { + let loadStylesheet = () => + Promise.resolve({ content: css` @config './nested-config.js'; @plugin './nested-plugin.js'; `, base: '/root/foo', path: '', - } - } - - using loadModule = vi.fn().mockImplementation((id: string) => { + }) + let loadModule = vi.fn().mockImplementation((id: string) => { let base = id.includes('nested') ? '/root/foo' : '/root' if (id.includes('config')) { let glob = id.includes('nested') ? './nested-config/*.html' : './root-config/*.html' @@ -564,25 +547,29 @@ test('emits the right base for @source found inside JS configs and plugins from }) test('it crashes when inside a cycle', async () => { - let input = css` - @import 'foo.css'; - ` - - async function loadStylesheet() { - return { - content: input, + let loadStylesheet = () => + Promise.resolve({ + content: css` + @import 'foo.css'; + `, base: '/root', path: '', - } - } + }) - await expect(compileCss(input, { base: '/root', loadStylesheet })).rejects.toMatchInlineSnapshot( + await expect( + run( + css` + @import 'foo.css'; + `, + { loadStylesheet }, + ), + ).rejects.toMatchInlineSnapshot( `[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`, ) }) test('resolves @reference as `@import "…" reference`', async () => { - async function loadStylesheet(id: string, base: string) { + let loadStylesheet = async (id: string, base: string) => { expect(base).toBe('/root') expect(id).toBe('./foo/bar.css') return { @@ -599,18 +586,16 @@ test('resolves @reference as `@import "…" reference`', async () => { } } - expect( - await run( - ['text-red-500'], + await expect( + run( css` @reference './foo/bar.css'; @tailwind utilities; `, - { base: '/root', loadStylesheet }, + { loadStylesheet, candidates: ['text-red-500'] }, ), - ).toMatchInlineSnapshot(` - " - .text-red-500 { + ).resolves.toMatchInlineSnapshot(` + ".text-red-500 { color: var(--color-red-500, red); } " @@ -618,7 +603,7 @@ test('resolves @reference as `@import "…" reference`', async () => { }) test('resolves `@variant` used as `@custom-variant` inside `@reference`', async () => { - async function loadStylesheet(id: string, base: string) { + let loadStylesheet = async (id: string, base: string) => { expect(base).toBe('/root') expect(id).toBe('./foo/bar.css') return { @@ -634,18 +619,16 @@ test('resolves `@variant` used as `@custom-variant` inside `@reference`', async } } - expect( - await run( - ['dark:flex'], + await expect( + run( css` @reference './foo/bar.css'; @tailwind utilities; `, - { base: '/root', loadStylesheet }, + { loadStylesheet, candidates: ['dark:flex'] }, ), - ).toMatchInlineSnapshot(` - " - .dark\\:flex:where([data-theme="dark"] *) { + ).resolves.toMatchInlineSnapshot(` + ".dark\\:flex:where([data-theme="dark"] *) { display: flex; } " diff --git a/packages/tailwindcss/src/attribute-selector-parser.test.ts b/packages/tailwindcss/src/attribute-selector-parser.test.ts index 87f5f3013..140336ab0 100644 --- a/packages/tailwindcss/src/attribute-selector-parser.test.ts +++ b/packages/tailwindcss/src/attribute-selector-parser.test.ts @@ -28,11 +28,6 @@ describe('parse', () => { '[ data-foo ]', { attribute: 'data-foo', operator: null, quote: null, value: null, sensitivity: null }, ], - [ - // Non-ASCII code points are valid in a CSS ident - '[data-état]', - { attribute: 'data-état', operator: null, quote: null, value: null, sensitivity: null }, - ], [ '[data-state=expanded]', { attribute: 'data-state', operator: '=', quote: null, value: 'expanded', sensitivity: null }, diff --git a/packages/tailwindcss/src/attribute-selector-parser.ts b/packages/tailwindcss/src/attribute-selector-parser.ts index 624043d59..e732b2979 100644 --- a/packages/tailwindcss/src/attribute-selector-parser.ts +++ b/packages/tailwindcss/src/attribute-selector-parser.ts @@ -23,7 +23,6 @@ const ZERO = 48 const NINE = 57 const DASH = 45 const UNDERSCORE = 95 -const NON_ASCII = 128 interface AttributeSelector { attribute: string @@ -62,8 +61,6 @@ export function parse(input: string): AttributeSelector | null { if (currentChar >= LOWER_A && currentChar <= LOWER_Z) continue if (currentChar >= ZERO && currentChar <= NINE) continue if (currentChar === DASH || currentChar === UNDERSCORE) continue - // Non-ASCII code points are valid in a CSS ident, e.g.: `[data-état]` - if (currentChar >= NON_ASCII) continue break } diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index b8f914e1b..53d21dda6 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -516,8 +516,6 @@ it('should not parse functional utilities with multiple modifiers', () => { utilities.functional('bg', () => []) expect(run('bg-red-1/2/3', { utilities })).toMatchInlineSnapshot(`[]`) - expect(run('bg-red-500/50/', { utilities })).toMatchInlineSnapshot(`[]`) - expect(run('bg-red-500/50//foo', { utilities })).toMatchInlineSnapshot(`[]`) }) it('should parse a utility with an arbitrary value', () => { @@ -1418,19 +1416,6 @@ it('should parse a functional variant with a modifier', () => { `) }) -it('should not parse variants with multiple modifiers', () => { - let utilities = new Utilities() - utilities.static('flex', () => []) - - let variants = new Variants() - variants.static('hover', () => {}) - variants.compound('group', Compounds.StyleRules, () => {}) - - expect(run('group-hover/foo:flex', { utilities, variants })).toHaveLength(1) - expect(run('group-hover/foo/:flex', { utilities, variants })).toEqual([]) - expect(run('group-hover/foo//bar:flex', { utilities, variants })).toEqual([]) -}) - it('should parse a functional variant starting with @', () => { let utilities = new Utilities() utilities.static('flex', () => []) @@ -2012,12 +1997,7 @@ it.each([ // Arbitrary variant values that end the block 'data-[a]{color:red}foo[a]:flex', - - // Named values contain invalid characters - 'data-foo=bar:flex', - 'bg-foo=bar', - 'bg-red-500/foo=bar', -])('should not parse invalid values: %s', (rawCandidate) => { +])('should not parse invalid arbitrary values: %s', (rawCandidate) => { let utilities = new Utilities() utilities.static('flex', () => []) utilities.functional('bg', () => []) @@ -2058,6 +2038,8 @@ const candidates = [ ['bg-[var(--spacing)_-_1px]', 'bg-[var(--spacing)-1px]'], ['bg-[var(--_spacing)]', 'bg-(--_spacing)'], ['bg-(--_spacing)', 'bg-(--_spacing)'], + ['bg-[var(--\_spacing)]', 'bg-(--_spacing)'], + ['bg-(--\_spacing)', 'bg-(--_spacing)'], ['bg-[-1px_-1px]', 'bg-[-1px_-1px]'], ['p-[round(to-zero,1px)]', 'p-[round(to-zero,1px)]'], ['w-1/2', 'w-1/2'], diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index a389af365..fa8cd3047 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -3,7 +3,6 @@ import { decodeArbitraryValue } from './utils/decode-arbitrary-value' import { DefaultMap } from './utils/default-map' import { isValidArbitrary } from './utils/is-valid-arbitrary' import { segment } from './utils/segment' -import type { ValueFunctionNode } from './value-parser' import * as ValueParser from './value-parser' import { walk, WalkAction } from './walk' @@ -11,7 +10,6 @@ const COLON = 0x3a const DASH = 0x2d const LOWER_A = 0x61 const LOWER_Z = 0x7a -const IS_VALID_NAMED_VALUE = /^[a-zA-Z0-9_.%-]+$/ export type ArbitraryUtilityValue = { kind: 'arbitrary' @@ -372,14 +370,6 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter } } - // If there's more than one modifier, the utility is invalid. - // - // E.g.: - // - // - `bg-red-500/50/50` - let parts = segment(base, '/') - if (parts.length > 2) return - // Figure out the new base and the modifier segment if present. // // E.g.: @@ -389,7 +379,14 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // ^^^^^^^^^^ -> Base without modifier // ^^ -> Modifier segment // ``` - let [baseWithoutModifier, modifierSegment = null] = parts + let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/') + + // If there's more than one modifier, the utility is invalid. + // + // E.g.: + // + // - `bg-red-500/50/50` + if (additionalModifier) return let parsedModifier = modifierSegment === null ? null : parseModifier(modifierSegment) @@ -599,8 +596,6 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter ? null : `${value}/${modifierSegment}` - if (!IS_VALID_NAMED_VALUE.test(value)) continue - candidate.value = { kind: 'named', value, @@ -652,8 +647,6 @@ function parseModifier(modifier: string): CandidateModifier | null { } } - if (!IS_VALID_NAMED_VALUE.test(modifier)) return null - return { kind: 'named', value: modifier, @@ -708,18 +701,17 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia // Static, functional and compound variants { + // group-hover/group-name + // ^^^^^^^^^^^ -> Variant without modifier + // ^^^^^^^^^^ -> Modifier + let [variantWithoutModifier, modifier = null, additionalModifier] = segment(variant, '/') + // If there's more than one modifier, the variant is invalid. // // E.g.: // // - `group-hover/foo/bar` - let parts = segment(variant, '/') - if (parts.length > 2) return null - - // group-hover/group-name - // ^^^^^^^^^^^ -> Variant without modifier - // ^^^^^^^^^^ -> Modifier - let [variantWithoutModifier, modifier = null] = parts + if (additionalModifier) return null let roots = findRoots(variantWithoutModifier, (root) => { return designSystem.variants.has(root) @@ -806,8 +798,6 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia } } - if (!IS_VALID_NAMED_VALUE.test(value)) continue - return { kind: 'functional', root, @@ -1038,56 +1028,33 @@ const printArbitraryValueCache = new DefaultMap((input) => { let drop = new Set() - let symbols = new Set([ - // Selectors - '~', // Subsequent sibling combinator - '>', // Child combinator - - // Math operators - '+', // or next sibling combinator - '-', - '*', // or universal selector - '/', - ]) walk(ast, (node, ctx) => { + let parentArray = ctx.parent === null ? ast : (ctx.parent.nodes ?? []) + // Handle operators (e.g.: inside of `calc(…)`) - if (node.kind === 'word' && symbols.has(node.value)) { - let idx = ctx.index + if ( + node.kind === 'word' && + // Operators + (node.value === '+' || node.value === '-' || node.value === '*' || node.value === '/') + ) { + let idx = parentArray.indexOf(node) ?? -1 // This should not be possible if (idx === -1) return - // a + b - // ^ node - // ^ previous (whitespace) - let previous = ctx.siblings[idx - 1] + let previous = parentArray[idx - 1] if (previous?.kind !== 'separator' || previous.value !== ' ') return - // a + b - // ^ node - // ^ next (whitespace) - let next = ctx.siblings[idx + 1] + let next = parentArray[idx + 1] if (next?.kind !== 'separator' || next.value !== ' ') return - // a + b - // ^ node - // ^ previous (node) - let previousPrevious = ctx.siblings[idx - 2] - if (previousPrevious && symbols.has(previousPrevious.value)) return - - // a + b - // ^ node - // ^ next (node) - let nextNext = ctx.siblings[idx + 2] - if (nextNext && symbols.has(nextNext.value)) return - drop.add(previous) drop.add(next) } // Leading and trailing whitespace else if (node.kind === 'separator' && node.value.length > 0 && node.value.trim() === '') { - if (ctx.siblings[0] === node || ctx.siblings[ctx.siblings.length - 1] === node) { + if (parentArray[0] === node || parentArray[parentArray.length - 1] === node) { drop.add(node) } } @@ -1097,41 +1064,6 @@ const printArbitraryValueCache = new DefaultMap((input) => { else if (node.kind === 'separator' && node.value.trim() === ',') { node.value = ',' } - - // Wrap custom functions starting with `--`, in parentheses if preceded by - // a symbol. E.g.: `calc(100%---spacing(2))` → `calc(100%-(--spacing(2)))` - else if (node.kind === 'function' && node.value.startsWith('--')) { - let idx = ctx.index - - // When it's the first argument, then we don't have to wrap it in `(…)` - // - // E.g.: `--spacing(…)`, no need to wrap - // `calc(100%---spacing(…))`, we want to wrap - // - if (idx <= 0) return - - // When it's not the first argument, we likely want to wrap it. However, - // when it's separated by a `,` then it's readable enough. - // - // E.g.: `min(100%,--spacing(2))` is readable, in fact - // `min(100%,(--spacing(2)))` would make it worse - let previous = ctx.siblings[idx - 1] - if (previous?.kind === 'separator' && previous.value === ',') return - - // When it's part of a bigger list, aka no special symbols were used, then - // we don't have to wrap it either. - // - // E.g.: `shadow-[inset_0px_1px_--theme(--color-white/15%)]`, wrapping would look unnecessary: - // `shadow-[inset_0px_1px_(--theme(--color-white/15%))]` - let previousPrevious = ctx.siblings[idx - 2] - if (previousPrevious && !symbols.has(previousPrevious.value)) return - - return WalkAction.ReplaceSkip({ - kind: 'function', - value: '', // Unnamed, so will result in `(…)` - nodes: [node], - } satisfies ValueFunctionNode) - } }) if (drop.size > 0) { diff --git a/packages/tailwindcss/src/canonicalize-calc-expressions.test.ts b/packages/tailwindcss/src/canonicalize-calc-expressions.test.ts deleted file mode 100644 index f612e5f7b..000000000 --- a/packages/tailwindcss/src/canonicalize-calc-expressions.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect, it } from 'vitest' -import { canonicalizeCalcExpressions } from './canonicalize-calc-expressions' - -it.each([ - ['calc(-1 * var(--foo))', 'calc(var(--foo) * -1)'], - ['calc(1rem + var(--foo))', 'calc(var(--foo) + 1rem)'], - ['calc(2rem * calc(3px * var(--foo)))', 'calc(calc(var(--foo) * 3px) * 2rem)'], - ['calc(var(--b) + var(--a))', 'calc(var(--a) + var(--b))'], - ['calc(3px * 2rem)', 'calc(2rem * 3px)'], - ['calc(5px * 3px)', 'calc(3px * 5px)'], - ['calc(1 * 1rem)', 'calc(1rem * 1)'], - ['calc(10 + 2)', 'calc(2 + 10)'], -])('`%s` → `%s` (%#)', (input, expected) => { - expect(canonicalizeCalcExpressions(input)).toBe(expected) -}) - -it.each([ - ['calc(1rem - var(--foo))'], - ['calc(1rem / 2)'], - ['calc(var(--a) + 1rem)'], - ['calc(2rem * 3px)'], -])('should keep `%s` (%#)', (input) => { - expect(canonicalizeCalcExpressions(input)).toBe(input) -}) diff --git a/packages/tailwindcss/src/canonicalize-calc-expressions.ts b/packages/tailwindcss/src/canonicalize-calc-expressions.ts deleted file mode 100644 index 7e08f8622..000000000 --- a/packages/tailwindcss/src/canonicalize-calc-expressions.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { dimensions } from './utils/dimensions' -import * as ValueParser from './value-parser' -import { walk, WalkAction } from './walk' - -// Assumption: We already assume that we receive somewhat valid `calc()` -// expressions. So we will see `calc(1 + 1)` and not `calc(1+1)` -export function canonicalizeCalcExpressions(input: string): string { - let [canonicalized, valueAst] = canonicalizeCalcExpressionsAst(ValueParser.parse(input)) - - return canonicalized ? ValueParser.toCss(valueAst) : input -} - -export function canonicalizeCalcExpressionsAst( - ast: ValueParser.ValueAstNode[], -): [canonicalized: boolean, ast: ValueParser.ValueAstNode[]] { - let canonicalized = false - - walk(ast, { - exit(valueNode) { - // We are only interested in binary expressions in `calc(…)` and `(…)`, - // and only with the `*` or `+` operators. - if (valueNode.kind !== 'function') return - if (valueNode.value !== 'calc' && valueNode.value !== '') return - if (valueNode.nodes.length !== 5) return - if (valueNode.nodes[2].kind !== 'word') return - if (valueNode.nodes[2].value !== '*' && valueNode.nodes[2].value !== '+') return - - let lhs = valueNode.nodes[0] - let rhs = valueNode.nodes[4] - - if (shouldSwap(lhs, rhs)) { - canonicalized = true - - let replacement: ValueParser.ValueFunctionNode = { - kind: 'function', - value: valueNode.value, - nodes: [ - rhs, // Now lhs - valueNode.nodes[1], // Separator - valueNode.nodes[2], // Operator - valueNode.nodes[3], // Separator - lhs, // Now rhs - ], - } - - return WalkAction.ReplaceSkip(replacement) - } - }, - }) - - return [canonicalized, ast] -} - -function shouldSwap(lhs: ValueParser.ValueAstNode, rhs: ValueParser.ValueAstNode): boolean { - let lhsDimension = lhs.kind === 'word' ? dimensions.get(lhs.value) : null - let rhsDimension = rhs.kind === 'word' ? dimensions.get(rhs.value) : null - - if (lhsDimension !== null && rhsDimension === null) return true - if (lhsDimension === null && rhsDimension !== null) return false - - if (lhsDimension !== null && rhsDimension !== null) { - let [lhsValue, lhsUnit] = lhsDimension - let [rhsValue, rhsUnit] = rhsDimension - - // Within dimensions, keep unit-bearing values ahead of unitless numbers so - // `1rem` sorts before `1`. - if (lhsUnit === null && rhsUnit !== null) return true - if (lhsUnit !== null && rhsUnit === null) return false - - // Then sort dimensions numerically, and finally by unit for ties. - if (lhsValue !== rhsValue) { - return lhsValue - rhsValue > 0 - } - - if (lhsUnit !== rhsUnit) { - return (lhsUnit ?? '').localeCompare(rhsUnit ?? '') > 0 - } - } - - // Both nodes are unknown values (not dimensions), sort them as strings - return ValueParser.toCss([lhs]).localeCompare(ValueParser.toCss([rhs])) > 0 -} diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 567f0ad0d..c5599d181 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -1,15 +1,11 @@ -import { randomUUID } from 'node:crypto' import fs from 'node:fs' import path from 'node:path' import { describe, expect, test } from 'vitest' import { __unstable__loadDesignSystem } from '.' -import { cartesian } from './cartesian' import type { CanonicalizeOptions } from './intellisense' -import plugin from './plugin' import { DefaultMap } from './utils/default-map' const css = String.raw -const timeout = 25_000 const defaultTheme = fs.readFileSync(path.resolve(__dirname, '../theme.css'), 'utf8') const designSystems = new DefaultMap((base: string) => { @@ -24,25 +20,6 @@ const designSystems = new DefaultMap((base: string) => { @tailwind utilities; ${defaultTheme} - - /* TODO(perf): Only here to speed up the tests */ - @theme { - --*: initial; - --breakpoint-lg: 64rem; - --breakpoint-md: 48rem; - --color-blue-200: oklch(88.2% 0.059 254.128); - --color-blue-500: oklch(62.3% 0.214 259.815); - --color-red-500: oklch(63.7% 0.237 25.331); - --color-white: #fff; - --container-md: 28rem; - --font-weight-normal: 400; - --leading-relaxed: 1.625; - --spacing: 0.25rem; - --text-sm--line-height: calc(1.25 / 0.875); - --text-sm: 0.875rem; - --text-xs--line-height: calc(1 / 0.75); - --text-xs: 0.75rem; - } `, } }, @@ -57,7 +34,7 @@ const DEFAULT_CANONICALIZATION_OPTIONS: CanonicalizeOptions = { } describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', (strategy) => { - let testName = '%s → %s (%#)' + let testName = '`%s` → `%s` (%#)' if (strategy === 'with-variant') { testName = testName.replaceAll('%s', 'focus:%s') } else if (strategy === 'important') { @@ -94,6 +71,30 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', } async function expectCanonicalization( + input: string, + candidate: string, + expected: string, + options: CanonicalizeOptions = DEFAULT_CANONICALIZATION_OPTIONS, + ) { + candidate = prepare(candidate) + expected = prepare(expected) + + if (strategy === 'prefix') { + input = input.replace("@import 'tailwindcss';", "@import 'tailwindcss' prefix(tw);") + } + + let designSystem = await designSystems.get(__dirname).get(input) + let [actual] = designSystem.canonicalizeCandidates([candidate], options) + + try { + expect(actual).toBe(expected) + } catch (err) { + if (err instanceof Error) Error.captureStackTrace(err, expectCanonicalization) + throw err + } + } + + async function expectCombinedCanonicalization( input: string, candidates: string, expected: string, @@ -112,196 +113,165 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', try { expect(actual).toEqual(preparedExpected) } catch (err) { - if (err instanceof Error) Error.captureStackTrace(err, expectCanonicalization) + if (err instanceof Error) Error.captureStackTrace(err, expectCombinedCanonicalization) throw err } } /// ---------------------------------- - describe('deprecated utilities', () => { - test.each([ - /// Legacy bg-gradient-* → bg-linear-* - ['bg-gradient-to-t', 'bg-linear-to-t'], - ['bg-gradient-to-tr', 'bg-linear-to-tr'], - ['bg-gradient-to-r', 'bg-linear-to-r'], - ['bg-gradient-to-br', 'bg-linear-to-br'], - ['bg-gradient-to-b', 'bg-linear-to-b'], - ['bg-gradient-to-bl', 'bg-linear-to-bl'], - ['bg-gradient-to-l', 'bg-linear-to-l'], - ['bg-gradient-to-tl', 'bg-linear-to-tl'], - ])(testName, { timeout }, async (candidate, expected) => { - let input = css` + test.each([ + /// Legacy bg-gradient-* → bg-linear-* + ['bg-gradient-to-t', 'bg-linear-to-t'], + ['bg-gradient-to-tr', 'bg-linear-to-tr'], + ['bg-gradient-to-r', 'bg-linear-to-r'], + ['bg-gradient-to-br', 'bg-linear-to-br'], + ['bg-gradient-to-b', 'bg-linear-to-b'], + ['bg-gradient-to-bl', 'bg-linear-to-bl'], + ['bg-gradient-to-l', 'bg-linear-to-l'], + ['bg-gradient-to-tl', 'bg-linear-to-tl'], + + /// theme(…) to `var(…)` + // Keep candidates that don't contain `theme(…)` or `theme(…, …)` + ['[color:red]', 'text-[red]'], + + // Handle special cases around `.1` in the `theme(…)` + ['[--value:theme(spacing.1)]', '[--value:--spacing(1)]'], + ['[--value:theme(fontSize.xs.1.lineHeight)]', '[--value:var(--text-xs--line-height)]'], + ['[--value:theme(spacing[1.25])]', '[--value:--spacing(1.25)]'], + + // Should not convert invalid spacing values to calc + ['[--value:theme(spacing[1.1])]', '[--value:theme(spacing[1.1])]'], + + // Convert to `var(…)` if we can resolve the path + ['[color:theme(colors.red.500)]', 'text-red-500'], // Arbitrary property + ['[color:theme(colors.red.500)]/50', 'text-red-500/50'], // Arbitrary property + modifier + ['bg-[theme(colors.red.500)]', 'bg-red-500'], // Arbitrary value + ['bg-[size:theme(spacing.4)]', 'bg-size-[--spacing(4)]'], // Arbitrary value + data type hint + + // Pretty print CSS functions preceded by an operator to prevent consecutive + // operator characters. + ['w-[calc(100dvh-theme(spacing.2))]', 'w-[calc(100dvh-(--spacing(2)))]'], + ['w-[calc(100dvh+theme(spacing.2))]', 'w-[calc(100dvh+(--spacing(2)))]'], + ['w-[calc(100dvh/theme(spacing.2))]', 'w-[calc(100dvh/(--spacing(2)))]'], + ['w-[calc(100dvh*theme(spacing.2))]', 'w-[calc(100dvh*(--spacing(2)))]'], + + // Convert to `var(…)` if we can resolve the path, but keep fallback values + ['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'], + + // Keep `theme(…)` if we can't resolve the path + ['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'], + + // Keep `theme(…)` if we can't resolve the path, but still try to convert the + // fallback value. + ['bg-[theme(colors.foo.1000,theme(colors.red.500))]', 'bg-red-500'], + + // Use `theme(…)` (deeply nested) inside of a `calc(…)` function + ['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--text-xs)*2)]'], + + // Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)` + // - Can't convert to `var(…)` because that would lose the modifier. + // - Can't convert to a candidate modifier because there are multiple + // `theme(…)` calls. + // + // If we really want to, we can make a fancy migration that tries to move it + // to a candidate modifier _if_ all `theme(…)` calls use the same modifier. + [ + '[color:theme(colors.red.500/50,theme(colors.blue.500/50))]', + 'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]', + ], + [ + '[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50', + 'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]/50', + ], + + // Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`), + // to a candidate modifier. + // Arbitrary property, with simple percentage modifier + ['[color:theme(colors.red.500/75%)]', 'text-red-500/75'], + + // Arbitrary property, with numbers (0-1) without a unit + ['[color:theme(colors.red.500/.12)]', 'text-red-500/12'], + ['[color:theme(colors.red.500/0.12)]', 'text-red-500/12'], + + // Arbitrary property, with more complex modifier (we only allow whole numbers + // as bare modifiers). Convert the complex numbers to arbitrary values instead. + ['[color:theme(colors.red.500/12.34%)]', 'text-red-500/[12.34%]'], + ['[color:theme(colors.red.500/var(--opacity))]', 'text-red-500/(--opacity)'], + ['[color:theme(colors.red.500/.12345)]', 'text-red-500/1234.5'], + ['[color:theme(colors.red.500/50.25%)]', 'text-red-500/50.25'], + + // Arbitrary value + ['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'], + ['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'], + + // Arbitrary property that already contains a modifier + ['[color:theme(colors.red.500/50%)]/50', 'text-[--theme(--color-red-500/50%)]/50'], + + // Values that don't contain only `theme(…)` calls should not be converted to + // use a modifier since the color is not the whole value. + [ + 'shadow-[shadow:inset_0px_1px_theme(colors.white/15%)]', + 'shadow-[inset_0px_1px_--theme(--color-white/15%)]', + ], + + // Arbitrary value, where the candidate already contains a modifier + // This should still migrate the `theme(…)` syntax to the modern syntax. + ['bg-[theme(colors.red.500/50%)]/50', 'bg-[--theme(--color-red-500/50%)]/50'], + + // Variants, we can't use `var(…)` especially inside of `@media(…)`. We can + // still upgrade the `theme(…)` to the modern syntax. + ['max-[theme(screens.lg)]:flex', 'max-[--theme(--breakpoint-lg)]:flex'], + // There are no variables for `--spacing` multiples, so we can't convert this + ['max-[theme(spacing.4)]:flex', 'max-[theme(spacing.4)]:flex'], + + // This test in itself doesn't make much sense. But we need to make sure + // that this doesn't end up as the modifier in the candidate itself. + ['max-[theme(spacing.4/50)]:flex', 'max-[theme(spacing.4/50)]:flex'], + + // `theme(…)` calls in another CSS function is replaced correctly. + // Additionally we remove unnecessary whitespace. + ['grid-cols-[min(50%_,_theme(spacing.80))_auto]', 'grid-cols-[min(50%,--spacing(80))_auto]'], + + // `theme(…)` calls valid in v3, but not in v4 should still be converted. + ['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'], + + // Renamed theme keys + ['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'], + ['w-[theme(maxWidth.md)]', 'w-md'], + + // Invalid cases + ['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'], + ['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'], + + // Partially invalid cases + [ + '[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]', + '[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]', + ], + [ + '[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50', + '[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50', + ], + + // If a utility sets `property` and `--tw-{property}` with the same value, + // we can ignore the `--tw-{property}`. This is just here for composition. + // This means that we should be able to upgrade the one _without_ to the one + // _with_ the variable + ['[font-weight:400]', 'font-normal'], + ['[line-height:0]', 'leading-0'], + ['[border-style:solid]', 'border-solid'], + ])(testName, async (candidate, expected) => { + await expectCanonicalization( + css` @import 'tailwindcss'; - ` - - await expectCanonicalization(input, candidate, expected) - }) - - let deprecated: [string, string][] = [ - ['order-none', 'order-0'], - ['break-words', 'wrap-break-word'], - ['overflow-ellipsis', 'text-ellipsis'], - - ['start-full', 'inset-s-full'], - ['-start-full', '-inset-s-full'], - ['start-auto', 'inset-s-auto'], - ['start-px', 'inset-s-px'], - ['-start-px', '-inset-s-px'], - ['start-8', 'inset-s-8'], // Within default spacing scale - ['-start-8', '-inset-s-8'], // Within default spacing scale - ['start-123', 'inset-s-123'], // Outside of default spacing scale - ['-start-123', '-inset-s-123'], // Outside of default spacing scale - - ['end-full', 'inset-e-full'], - ['-end-full', '-inset-e-full'], - ['end-auto', 'inset-e-auto'], - ['end-px', 'inset-e-px'], - ['-end-px', '-inset-e-px'], - ['end-8', 'inset-e-8'], // Within default spacing scale - ['-end-8', '-inset-e-8'], // Within default spacing scale - ['end-123', 'inset-e-123'], // Outside of default spacing scale - ['-end-123', '-inset-e-123'], // Outside of default spacing scale - ] - - test.each(deprecated)(testName, { timeout }, async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - ` - - await expectCanonicalization(input, candidate, expected) - }) - - describe('With custom implementation', () => { - // Creating a shared CSS file such that we can re-use the same design - // system for all of these. - let customImplementations = deprecated - .map( - ([candidate]) => css` - @utility ${candidate} { - --custom-${randomUUID()}: implementation; - } - `, - ) - .join('\n') - - // Keep the current utility because of the custom implementation - test.each(deprecated.map(([candidate]) => [candidate, candidate]))( - testName, - { timeout }, - async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - - ${customImplementations} - ` - - await expectCanonicalization(input, candidate, expected) - }, - ) - }) + `, + candidate, + expected, + ) }) - describe('arbitrary properties', () => { - test.each([ - /// theme(…) to `var(…)` - // Keep candidates that don't contain `theme(…)` or `theme(…, …)` - ['[color:red]', 'text-[red]'], - - // Handle special cases around `.1` in the `theme(…)` - ['[--value:theme(spacing.1)]', '[--value:--spacing(1)]'], - ['[--value:theme(fontSize.xs.1.lineHeight)]', '[--value:var(--text-xs--line-height)]'], - ['[--value:theme(spacing[1.25])]', '[--value:--spacing(1.25)]'], - - // Should not convert invalid spacing values to calc - ['[--value:theme(spacing[1.1])]', '[--value:theme(spacing[1.1])]'], - - // Convert to `var(…)` if we can resolve the path - ['[color:theme(colors.red.500)]', 'text-red-500'], // Arbitrary property - ['[color:theme(colors.red.500)]/50', 'text-red-500/50'], // Arbitrary property + modifier - - // Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)` - // - Can't convert to `var(…)` because that would lose the modifier. - // - Can't convert to a candidate modifier because there are multiple - // `theme(…)` calls. - // - // If we really want to, we can make a fancy migration that tries to move it - // to a candidate modifier _if_ all `theme(…)` calls use the same modifier. - [ - '[color:theme(colors.red.500/50,theme(colors.blue.500/50))]', - 'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]', - ], - [ - '[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50', - 'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]/50', - ], - - // Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`), - // to a candidate modifier. - // Arbitrary property, with simple percentage modifier - ['[color:theme(colors.red.500/75%)]', 'text-red-500/75'], - - // Arbitrary property, with numbers (0-1) without a unit - ['[color:theme(colors.red.500/.12)]', 'text-red-500/12'], - ['[color:theme(colors.red.500/0.12)]', 'text-red-500/12'], - - // Arbitrary property, with more complex modifier (we only allow whole numbers - // as bare modifiers). Convert the complex numbers to arbitrary values instead. - ['[color:theme(colors.red.500/12.34%)]', 'text-red-500/[12.34%]'], - ['[color:theme(colors.red.500/var(--opacity))]', 'text-red-500/(--opacity)'], - ['[color:theme(colors.red.500/.12345)]', 'text-red-500/1234.5'], - ['[color:theme(colors.red.500/50.25%)]', 'text-red-500/50.25'], - - // Arbitrary property that already contains a modifier - ['[color:theme(colors.red.500/50%)]/50', 'text-[--theme(--color-red-500/50%)]/50'], - - // `calc(var(--spacing)*…)` to `--spacing(…)` - ['[padding-top:min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'], - [ - '[padding-top:min(20%,calc(var(--spacing)*var(--other)))]', - 'pt-[min(20%,--spacing(var(--other)))]', - ], - ['[padding-top:calc(var(--spacing)*8)]', 'pt-8'], - ['[padding-top:calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'], - - // `theme(…)` calls valid in v3, but not in v4 should still be converted. - ['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'], - - // Invalid cases - ['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'], - ['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'], - - // Partially invalid cases - [ - '[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]', - '[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]', - ], - [ - '[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50', - '[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50', - ], - - // If a utility sets `property` and `--tw-{property}` with the same value, - // we can ignore the `--tw-{property}`. This is just here for composition. - // This means that we should be able to upgrade the one _without_ to the one - // _with_ the variable - ['[font-weight:400]', 'font-normal'], - ['[line-height:0]', 'leading-0'], - ['[border-style:solid]', 'border-solid'], - - // Do not constant fold `0` to `0` when the type is unknown (which - // is often the case with CSS variables) - ['[--foo:0px]', '[--foo:0px]'], - ['[--foo:calc(0px*1)]', '[--foo:calc(0px*1)]'], - ['[--foo:calc(0*1rem)]', '[--foo:calc(0*1rem)]'], - ])(testName, { timeout }, async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - ` - - await expectCanonicalization(input, candidate, expected) - }) - + describe('arbitrary utilities', () => { test.each([ // Arbitrary property to static utility ['[text-wrap:balance]', 'text-balance'], @@ -312,14 +282,14 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['[display:_flex]', 'flex'], ['[display:flex_]', 'flex'], + // Arbitrary property to static utility + // Map number to keyword-like value + ['leading-[1]', 'leading-none'], + // Arbitrary property to named functional utility ['[color:var(--color-red-500)]', 'text-red-500'], ['[background-color:var(--color-red-500)]', 'bg-red-500'], - // Arbitrary property to named utility (case insensitive) - ['[color:#fff]', 'text-white'], - ['[color:#FFF]', 'text-white'], - // Arbitrary property with modifier to named functional utility with modifier ['[color:var(--color-red-500)]/25', 'text-red-500/25'], @@ -343,107 +313,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['[grid-column:2]', 'col-2'], ['[grid-column:1234]', 'col-1234'], - // Complex arbitrary property to arbitrary value - [ - '[grid-template-columns:repeat(2,minmax(100px,1fr))]', - 'grid-cols-[repeat(2,minmax(100px,1fr))]', - ], - // Complex arbitrary property to bare value - ['[grid-template-columns:repeat(2,minmax(0,1fr))]', 'grid-cols-2'], - ])(testName, { timeout }, async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - - @theme { - --*: initial; - --spacing: 0.25rem; - --color-red-500: red; - --color-white: #fff; - - /* Equivalent of blue-500/50 */ - --color-primary: color-mix(in oklab, oklch(62.3% 0.214 259.815) 50%, transparent); - } - ` - - await expectCanonicalization(input, candidate, expected) - }) - }) - - describe('arbitrary values', () => { - test.each([ - // Convert to `var(…)` if we can resolve the path - ['bg-[theme(colors.red.500)]', 'bg-red-500'], // Arbitrary value - ['bg-[size:theme(spacing.4)]', 'bg-size-[--spacing(4)]'], // Arbitrary value + data type hint - - // Pretty print CSS functions preceded by an operator to prevent consecutive - // operator characters. - ['w-[calc(100dvh-theme(spacing.2))]', 'w-[calc(100dvh-(--spacing(2)))]'], - ['w-[calc(100dvh+theme(spacing.2))]', 'w-[calc(100dvh+(--spacing(2)))]'], - ['w-[calc(100dvh/theme(spacing.2))]', 'w-[calc(100dvh/(--spacing(2)))]'], - ['w-[calc(100dvh*theme(spacing.2))]', 'w-[calc(100dvh*(--spacing(2)))]'], - - // Convert to `var(…)` if we can resolve the path, but keep fallback values - ['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'], - - // Keep `theme(…)` if we can't resolve the path - ['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'], - - // Keep `theme(…)` if we can't resolve the path, but still try to convert the - // fallback value. - ['bg-[theme(colors.foo.1000,theme(colors.red.500))]', 'bg-red-500'], - - // Use `theme(…)` (deeply nested) inside of a `calc(…)` function - ['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--text-xs)*2)]'], - - // Arbitrary value - ['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'], - ['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'], - - // Arbitrary value with different casing - ['bg-[#fff]', 'bg-white'], - ['bg-[#FFF]', 'bg-white'], - - // Values that don't contain only `theme(…)` calls should not be converted to - // use a modifier since the color is not the whole value. - [ - 'shadow-[shadow:inset_0px_1px_theme(colors.white/15%)]', - 'shadow-[inset_0px_1px_--theme(--color-white/15%)]', - ], - - // Arbitrary value, where the candidate already contains a modifier - // This should still migrate the `theme(…)` syntax to the modern syntax. - ['bg-[theme(colors.red.500/50%)]/50', 'bg-[--theme(--color-red-500/50%)]/50'], - - // Variants, we can't use `var(…)` especially inside of `@media(…)`. But - // since the value matches the `lg` breakpoint exactly, we can migrate to - // the `max-lg` variant. - ['max-[theme(screens.lg)]:flex', 'max-lg:flex'], - ['min-[theme(screens.lg)]:flex', 'lg:flex'], // We could also map to `min-lg:flex` - // There are no variables for `--spacing` multiples, so we can't convert this - ['max-[theme(spacing.4)]:flex', 'max-[theme(spacing.4)]:flex'], - - // This test in itself doesn't make much sense. But we need to make sure - // that this doesn't end up as the modifier in the candidate itself. - ['max-[theme(spacing.4/50)]:flex', 'max-[theme(spacing.4/50)]:flex'], - - // `theme(…)` calls in another CSS function is replaced correctly. - // Additionally we remove unnecessary whitespace. - ['grid-cols-[min(50%_,_theme(spacing.80))_auto]', 'grid-cols-[min(50%,--spacing(80))_auto]'], - - // `calc(var(--spacing)*…)` to `--spacing(…)` - ['pt-[min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'], - ['pt-[min(20%,calc(var(--spacing)*var(--other)))]', 'pt-[min(20%,--spacing(var(--other)))]'], - ['pt-[calc(var(--spacing)*8)]', 'pt-8'], - ['pt-[calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'], - - // Renamed theme keys - ['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'], - ['w-[theme(maxWidth.md)]', 'w-md'], - - // Arbitrary property to static utility - // Map number to keyword-like value - ['leading-[1]', 'leading-none'], - // Arbitrary value to bare value ['border-[2px]', 'border-2'], ['border-[1234px]', 'border-1234'], @@ -458,48 +327,38 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', // Arbitrary value with spacing mul ['w-[64rem]', 'w-256'], + // Complex arbitrary property to arbitrary value + [ + '[grid-template-columns:repeat(2,minmax(100px,1fr))]', + 'grid-cols-[repeat(2,minmax(100px,1fr))]', + ], + // Complex arbitrary property to bare value + ['[grid-template-columns:repeat(2,minmax(0,1fr))]', 'grid-cols-2'], + // Arbitrary value to bare value with percentage ['from-[25%]', 'from-25%'], // Arbitrary percentage value must be a whole number. Should not migrate to // a bare value. ['from-[2.5%]', 'from-[2.5%]'], - - // Negative arbitrary values can be simplified - // 1. Try to move the sign _inside_ the arbitrary value - // 2. Try to move the sign _out_ of the arbitrary value - ['-mt-[12rem]', '-mt-48'], // Arbitrary value → bare value - ['-mt-[-12rem]', 'mt-48'], // Double negation - ['-mt-[12.34rem]', 'mt-[-12.34rem]'], // Move `-` inside - ['-mt-[-12.34rem]', 'mt-[12.34rem]'], // Move `-` inside, double negation - ['-mt-[12.34px]', 'mt-[-12.34px]'], - ['-mt-[-12.34px]', 'mt-[12.34px]'], - ['-mt-[492px]', '-mt-123'], // Moving inside, allows us to migrate to a bare value - ['-mt-[calc(-1*492px)]', 'mt-123'], // Double negation and constant folding into bare value - ['-mt-[-492px]', 'mt-123'], - ['-mt-[calc(-1*-492px)]', '-mt-123'], // Constant folding with calc expressions - ['-mt-(--my-var)', '-mt-(--my-var)'], // Keep as-is - ['-mt-[var(--my-var)]', '-mt-(--my-var)'], // Keep as-is, but convert to shorthand - ['mt-[calc(var(--my-var)*-1)]', '-mt-(--my-var)'], // Move `-` out - ['mt-[calc(-1*var(--my-var))]', '-mt-(--my-var)'], // Move `-` out - ['-mt-[calc(var(--my-var)*-1)]', 'mt-(--my-var)'], // Move `-` out - ['-mt-[calc(-1*var(--my-var))]', 'mt-(--my-var)'], // Move `-` out - ['mt-[calc(-1*calc(-1*var(--my-var)))]', 'mt-(--my-var)'], // Move `-` out - ['-mt-[calc(-1*calc(-1*var(--my-var)))]', '-mt-(--my-var)'], // Move `-` out - ])(testName, { timeout }, async (candidate, expected) => { + ])(testName, async (candidate, expected) => { let input = css` @import 'tailwindcss'; + @theme { - --color-white: #fff; + --*: initial; + --spacing: 0.25rem; + --color-red-500: red; + + /* Equivalent of blue-500/50 */ + --color-primary: color-mix(in oklab, oklch(62.3% 0.214 259.815) 50%, transparent); } ` await expectCanonicalization(input, candidate, expected) }) - }) - describe('arbitrary utilities', () => { - test('migrate with custom static utility `@utility custom {…}`', { timeout }, async () => { + test('migrate with custom static utility `@utility custom {…}`', async () => { let candidate = '[--key:value]' let expected = 'custom' @@ -516,55 +375,47 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', await expectCanonicalization(input, candidate, expected) }) - test( - 'migrate with custom functional utility `@utility custom-* {…}`', - { timeout }, - async () => { - let candidate = '[--key:value]' - let expected = 'custom-value' + test('migrate with custom functional utility `@utility custom-* {…}`', async () => { + let candidate = '[--key:value]' + let expected = 'custom-value' - let input = css` - @import 'tailwindcss'; - @theme { - --*: initial; - } - @utility custom-* { - --key: --value('value'); - } - ` + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + @utility custom-* { + --key: --value('value'); + } + ` - await expectCanonicalization(input, candidate, expected) - }, - ) + await expectCanonicalization(input, candidate, expected) + }) - test( - 'migrate with custom functional utility `@utility custom-* {…}` that supports bare values', - { timeout }, - async () => { - let candidate = '[--resolved-value:4]' - let expected = 'example-4' + test('migrate with custom functional utility `@utility custom-* {…}` that supports bare values', async () => { + let candidate = '[tab-size:4]' + let expected = 'tab-4' - let input = css` - @import 'tailwindcss'; - @theme { - --*: initial; - } - @utility example-* { - --resolved-value: --value(integer); - } - ` + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + @utility tab-* { + tab-size: --value(integer); + } + ` - await expectCanonicalization(input, candidate, expected) - }, - ) + await expectCanonicalization(input, candidate, expected) + }) test.each([ - ['[--resolved-value:0]', 'example-0'], - ['[--resolved-value:4]', 'example-4'], - ['[--resolved-value:8]', 'example-a'], - ['example-[0]', 'example-0'], - ['example-[4]', 'example-4'], - ['example-[8]', 'example-a'], + ['[tab-size:0]', 'tab-0'], + ['[tab-size:4]', 'tab-4'], + ['[tab-size:8]', 'tab-github'], + ['tab-[0]', 'tab-0'], + ['tab-[4]', 'tab-4'], + ['tab-[8]', 'tab-github'], ])( 'migrate custom @utility from arbitrary values to bare values and named values (based on theme)', async (candidate, expected) => { @@ -572,11 +423,11 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', @import 'tailwindcss'; @theme { --*: initial; - --example-a: 8; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value(--example, integer, [integer]); + @utility tab-* { + tab-size: --value(--tab-size, integer, [integer]); } ` @@ -611,64 +462,40 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', // the semantics of the value. ['max-w-(--breakpoint-md)', 'max-w-(--breakpoint-md)'], ['max-w-(--container-3xl)', 'max-w-3xl'], - ])( - `migrate arbitrary value to theme value ${testName}`, - { timeout }, - async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - ${theme} { - --*: initial; - --breakpoint-md: 48rem; - --container-3xl: 48rem; - } - ` - - await expectCanonicalization(input, candidate, expected) - }, - ) - }) - - test( - 'migrate an arbitrary property without spaces, to a theme value with spaces (canonicalization)', - { timeout }, - async () => { - let candidate = 'font-[foo,bar,baz]' - let expected = 'font-example' + ])(`migrate arbitrary value to theme value ${testName}`, async (candidate, expected) => { let input = css` @import 'tailwindcss'; - @theme { + ${theme} { --*: initial; - --font-example: foo, bar, baz; + --breakpoint-md: 48rem; + --container-3xl: 48rem; } ` await expectCanonicalization(input, candidate, expected) - }, - ) + }) + }) + + test('migrate an arbitrary property without spaces, to a theme value with spaces (canonicalization)', async () => { + let candidate = 'font-[foo,bar,baz]' + let expected = 'font-example' + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + --font-example: foo, bar, baz; + } + ` + + await expectCanonicalization(input, candidate, expected) + }) test.each([ // Default spacing scale ['w-[64rem]', 'w-256', '0.25rem'], - // Non-suggested numbers - ['gap-[7.25rem]', 'gap-29', '0.25rem'], - ['gap-[calc(7rem+0.25rem)]', 'gap-29', '0.25rem'], - ['gap-[116px]', 'gap-29', '0.25rem'], - - // Non-suggested numbers, with the same spacing scale with different - // units - ['gap-[7.25rem]', 'gap-29', '4px'], - ['gap-[calc(7rem+0.25rem)]', 'gap-29', '4px'], - ['gap-[116px]', 'gap-29', '4px'], - - // Non-suggested numbers, with a different spacing scale - ['gap-[7.25rem]', 'gap-116', '1px'], - ['gap-[calc(7rem+0.25rem)]', 'gap-116', '1px'], - ['gap-[116px]', 'gap-116', '1px'], - // Keep arbitrary value if units are different - ['w-[124px]', 'w-31', '0.25rem'], + ['w-[124px]', 'w-[124px]', '0.25rem'], // Keep arbitrary value if bare value doesn't fit in steps of .25 ['w-[0.123rem]', 'w-[0.123rem]', '0.25rem'], @@ -676,7 +503,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', // Custom pixel based spacing scale ['w-[123px]', 'w-123', '1px'], ['w-[256px]', 'w-128', '2px'], - ])(`${testName} (spacing = \`%s\`)`, { timeout }, async (candidate, expected, spacing) => { + ])(`${testName} (spacing = \`%s\`)`, async (candidate, expected, spacing) => { let input = css` @import 'tailwindcss'; @@ -697,11 +524,11 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', --*: initial; --spacing: 0.25rem; --aspect-video: 16 / 9; - --example-a: 8; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value(--example, integer); + @utility tab-* { + tab-size: --value(--tab-size, integer); } ` @@ -710,8 +537,73 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['aspect-16/9', 'aspect-video'], // Custom utility with bare value integer - ['example-8', 'example-a'], - ])(testName, { timeout }, async (candidate, expected) => { + ['tab-8', 'tab-github'], + ])(testName, async (candidate, expected) => { + await expectCanonicalization(input, candidate, expected) + }) + }) + + describe('deprecated utilities', () => { + test('`order-none` → `order-0`', async () => { + let candidate = 'order-none' + let expected = 'order-0' + + let input = css` + @import 'tailwindcss'; + ` + + await expectCanonicalization(input, candidate, expected) + }) + + test('`order-none` → `order-none` with custom implementation', async () => { + let candidate = 'order-none' + let expected = 'order-none' + + let input = css` + @import 'tailwindcss'; + + @utility order-none { + order: none; /* imagine this exists */ + } + ` + + await expectCanonicalization(input, candidate, expected) + }) + + test('`break-words` → `wrap-break-word`', async () => { + let candidate = 'break-words' + let expected = 'wrap-break-word' + + let input = css` + @import 'tailwindcss'; + ` + + await expectCanonicalization(input, candidate, expected) + }) + + test('`[overflow-wrap:break-word]` → `wrap-break-word`', async () => { + let candidate = '[overflow-wrap:break-word]' + let expected = 'wrap-break-word' + + let input = css` + @import 'tailwindcss'; + ` + + await expectCanonicalization(input, candidate, expected) + }) + + test('`break-words` → `break-words` with custom implementation', async () => { + let candidate = 'break-words' + let expected = 'break-words' + + let input = css` + @import 'tailwindcss'; + + @utility break-words { + break: words; /* imagine this exists */ + } + ` + await expectCanonicalization(input, candidate, expected) }) }) @@ -721,7 +613,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', @import 'tailwindcss'; @theme { --*: initial; - --breakpoint-lg: 64rem; } ` @@ -738,22 +629,17 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['[@media(scripting:_none)]:flex', 'noscript:flex'], ['[@media_(scripting:_none)]:flex', 'noscript:flex'], - // Arbitrary variants with media queries cannot use `var(…)`, but if an - // equivalent variant exists we can use that one instead. - ['[@media(width>=theme(screens.lg))]:flex', 'lg:flex'], // `min-lg:flex` would also be valid - ['[@media(width { + ])(testName, async (candidate, expected) => { await expectCanonicalization(input, candidate, expected) }) - test('unsafe migrations keep the candidate as-is', { timeout }, async () => { + test('unsafe migrations keep the candidate as-is', async () => { // `hover:` also includes an `@media` query in addition to the `&:hover` // state. Migration is not safe because the functionality would be different. let candidate = '[&:hover]:flex' @@ -768,7 +654,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', await expectCanonicalization(input, candidate, expected) }) - test('make unsafe migration safe (1)', { timeout }, async () => { + test('make unsafe migration safe (1)', async () => { // Overriding the `hover:` variant to only use a selector will make the // migration safe. let candidate = '[&:hover]:flex' @@ -784,7 +670,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', await expectCanonicalization(input, candidate, expected) }) - test('make unsafe migration safe (2)', { timeout }, async () => { + test('make unsafe migration safe (2)', async () => { // Overriding the `hover:` variant to only use a selector will make the // migration safe. This time with the long-hand `@variant` syntax. let candidate = '[&:hover]:flex' @@ -804,7 +690,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', await expectCanonicalization(input, candidate, expected) }) - test('custom selector-based variants', { timeout }, async () => { + test('custom selector-based variants', async () => { let candidate = '[&.macos]:flex' let expected = 'is-macos:flex' let input = css` @@ -818,7 +704,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', await expectCanonicalization(input, candidate, expected) }) - test('custom @media-based variants', { timeout }, async () => { + test('custom @media-based variants', async () => { let candidate = '[@media(prefers-reduced-transparency:reduce)]:flex' let expected = 'transparency-safe:flex' let input = css` @@ -855,7 +741,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', // A color with a known theme variable migrates to the full utility ['bg-(color:--color-red-500)', 'bg-red-500'], - ])(testName, { timeout }, async (candidate, expected) => { + ])(testName, async (candidate, expected) => { await expectCanonicalization(input, candidate, expected) }) }) @@ -891,15 +777,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['[margin:-0]', 'm-0'], ['[margin:0px]', 'm-0'], - // Limit conversions for big values using the `--spacing` multiplier - ['left-[99999px]', 'left-[99999px]'], // This would otherwise result in `left-24999.75` - ['left-[-99999px]', 'left-[-99999px]'], // This would otherwise result in `-left-24999.75` - ['left-[96rem]', 'left-384'], // Within the limit - ['left-[-96rem]', '-left-384'], // Within the limit - ['left-[calc(96rem+1px)]', 'left-[calc(96rem+1px)]'], // Out of the positive limit - ['left-[calc(-96rem-1px)]', 'left-[calc(-96rem-1px)]'], // Out of the negative limit - ['z-[9999999]', 'z-9999999'], // `--spacing` multiplier is not used - // Not a length-unit, can't safely constant fold ['[margin:0%]', 'm-[0%]'], @@ -951,7 +828,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', 'data-[selected]:aria-[selected="true"]:aspect-[12/34]', 'data-selected:aria-selected:aspect-12/34', ], - ])(testName, { timeout }, async (candidate, expected) => { + ])(testName, async (candidate, expected) => { let input = css` @import 'tailwindcss'; ` @@ -959,67 +836,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', }) }) - // https://github.com/tailwindlabs/tailwindcss/issues/20365 - describe('media query and container query variants', () => { - test.each([ - // Breakpoints (`--breakpoint-lg: 64rem`, `--breakpoint-md: 48rem`) - ['min-[64rem]:flex', 'lg:flex'], - ['max-[64rem]:flex', 'max-lg:flex'], - ['min-[48rem]:flex', 'md:flex'], - ['max-[48rem]:flex', 'max-md:flex'], - - // With the `rem` option set, equivalent `px` values should also be - // canonicalized to the breakpoint variants. - ['min-[1024px]:flex', 'lg:flex'], - ['max-[1024px]:flex', 'max-lg:flex'], - - // Container queries (`--container-md: 28rem`) - ['@min-[28rem]:flex', '@md:flex'], - ['@max-[28rem]:flex', '@max-md:flex'], - ['@min-[448px]:flex', '@md:flex'], - ['@max-[448px]:flex', '@max-md:flex'], - - // Values that don't map to a known breakpoint or container size should - // stay as-is. - ['min-[123px]:flex', 'min-[123px]:flex'], - ['max-[123px]:flex', 'max-[123px]:flex'], - ['@min-[123px]:flex', '@min-[123px]:flex'], - ['@max-[123px]:flex', '@max-[123px]:flex'], - - // Named variants that produce the same CSS should be kept as-is - ['min-lg:flex', 'min-lg:flex'], - ['@min-md:flex', '@min-md:flex'], - ])(testName, { timeout }, async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - ` - - await expectCanonicalization(input, candidate, expected) - }) - - test.each([ - // Without the `rem` option, `px` values cannot be safely converted to - // `rem` based breakpoints. - ['min-[1024px]:flex', 'min-[1024px]:flex'], - ['@min-[448px]:flex', '@min-[448px]:flex'], - - // Same unit still works without the `rem` option - ['min-[64rem]:flex', 'lg:flex'], - ['max-[64rem]:flex', 'max-lg:flex'], - ['@min-[28rem]:flex', '@md:flex'], - ['@max-[28rem]:flex', '@max-md:flex'], - ])(`${testName} (no rem option)`, { timeout }, async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - ` - - await expectCanonicalization(input, candidate, expected, { - collapse: true, - logicalToPhysical: true, - }) - }) - }) - describe('modernize arbitrary variants', () => { test.each([ // Arbitrary variants @@ -1119,12 +935,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['has-[[aria-visible]]:flex', 'has-aria-[visible]:flex'], ['has-[&:not(:nth-child(even))]:flex', 'has-odd:flex'], - - // Arbitrary variant to compound + arbitrary variants - ['[&:has([role=checkbox])]:flex', 'has-[[role=checkbox]]:flex'], - ['[&:has([aria-visible="true"])]:flex', 'has-aria-visible:flex'], - ['[&:has([data-slot=description])]:flex', 'has-data-[slot=description]:flex'], - ])(testName, { timeout }, async (candidate, expected) => { + ])(testName, async (candidate, expected) => { let input = css` @import 'tailwindcss'; ` @@ -1157,189 +968,45 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', // Keep modifiers on classes that don't _really_ exist ['group/name', 'group/name'], - ])(testName, { timeout }, async (candidate, expected) => { + ])(testName, async (candidate, expected) => { await expectCanonicalization(input, candidate, expected) }) }) - describe('combine to shorthand utilities', () => { - test.each([ - // 4 to 1 - ['mt-1 mr-1 mb-1 ml-1', 'm-1'], - ['border-t-123 border-r-123 border-b-123 border-l-123', 'border-123'], - ['border-t-1 border-r-1 border-b-1 border-l-1', 'border'], // `border` is shorter than `border-1` - ['border-t-red-500 border-r-red-500 border-b-red-500 border-l-red-500', 'border-red-500'], - ['scroll-mt-1 scroll-mr-1 scroll-mb-1 scroll-ml-1', 'scroll-m-1'], - ['scroll-pt-1 scroll-pr-1 scroll-pb-1 scroll-pl-1', 'scroll-p-1'], + test.each([ + // 4 to 1 + ['mt-1 mr-1 mb-1 ml-1', 'm-1'], - // 2 to 1 - ['mt-1 mb-1', 'my-1'], - ['border-t-123 border-b-123', 'border-y-123'], - ['border-t-1 border-b-1', 'border-y'], // `border-y` is shorter than `border-y-1` - ['border-t-red-500 border-b-red-500', 'border-y-red-500'], - ['scroll-mt-1 scroll-mb-1', 'scroll-my-1'], - ['scroll-pt-1 scroll-pb-1', 'scroll-py-1'], - ['overflow-x-hidden overflow-y-hidden', 'overflow-hidden'], - ['overscroll-x-contain overscroll-y-contain', 'overscroll-contain'], + // 2 to 1 + ['mt-1 mb-1', 'my-1'], - // Different order as above - ['mb-1 mt-1', 'my-1'], + // Different order as above + ['mb-1 mt-1', 'my-1'], - // To completely different utility - ['w-4 h-4', 'size-4'], + // To completely different utility + ['w-4 h-4', 'size-4'], - // Goes beyond the default spacing scale that's being used in intellisense - // for code completion. Since it's about bare values, we should still be - // able to combine them. - ['w-123 h-123', 'size-123'], - ['w-128 h-128', 'size-128'], // `w-128` on its own would become `w-lg` - ['mt-123 mb-123', 'my-123'], + // Do not touch if not operating on the same variants + ['hover:w-4 h-4', 'hover:w-4 h-4'], - // Collapse duplicates into themselves - ['w-8 w-8', 'w-8'], + // Arbitrary properties to combined class + ['[width:_16px_] [height:16px]', 'size-4'], - // `w-*` and `h-*` would canonicalize to `size-5` - // `size-5` and `size-5` should then canonicalize to `size-5` - ['w-[calc(1rem+0.25rem)] h-[calc(1rem+0.25rem)] size-5', 'size-5'], - - // Same as above, but with an additional unrelated class - ['w-[calc(1rem+0.25rem)] h-[calc(1rem+0.25rem)] size-5 flex', 'size-5 flex'], - - // Do not touch if not operating on the same variants - ['hover:w-4 h-4', 'hover:w-4 h-4'], - - // Arbitrary properties to combined class - ['[width:_16px_] [height:16px]', 'size-4'], - - // Arbitrary properties to combined class with modifier - ['[font-size:14px] [line-height:1.625]', 'text-sm/relaxed'], - ])(testName, { timeout }, async (candidates, expected) => { + // Arbitrary properties to combined class with modifier + ['[font-size:14px] [line-height:1.625]', 'text-sm/relaxed'], + ])( + 'should canonicalize multiple classes `%s` into a shorthand `%s`', + async (candidates, expected) => { let input = css` @import 'tailwindcss'; ` - await expectCanonicalization(input, candidates, expected) - }) - }) - - describe('font-size/line-height to text-{x}/{y}', () => { - test.each([ - ...Array.from( - cartesian( - ['[font-size:14px]', 'text-[14px]', 'text-[14px]/6', 'text-sm', 'text-sm/6'], - ['[line-height:28px]', 'leading-[28px]', 'leading-7'], - ), - ).map((classes) => [classes.join(' ').padEnd(40, ' '), 'text-sm/7']), - ...Array.from( - cartesian( - ['[font-size:15px]', 'text-[15px]', 'text-[15px]/6'], - ['[line-height:28px]', 'leading-[28px]', 'leading-7'], - ), - ).map((classes) => [classes.join(' ').padEnd(40, ' '), 'text-[15px]/7']), - ...Array.from( - cartesian( - ['[font-size:14px]', 'text-[14px]', 'text-[14px]/6', 'text-sm', 'text-sm/6'], - ['[line-height:28.5px]', 'leading-[28.5px]'], - ), - ).map((classes) => [classes.join(' ').padEnd(40, ' '), 'text-sm/[28.5px]']), - ...Array.from( - cartesian( - ['[font-size:15px]', 'text-[15px]', 'text-[15px]/6'], - ['[line-height:28.5px]', 'leading-[28.5px]'], - ), - ).map((classes) => [classes.join(' ').padEnd(40, ' '), 'text-[15px]/[28.5px]']), - ])(testName, { timeout }, async (candidates, expected) => { - let input = css` - @import 'tailwindcss'; - ` - await expectCanonicalization(input, candidates.trim(), expected) - }) - }) - - // https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1558 - test.each([ - ['tracking-[-0.05em]', 'tracking-tighter'], - ['tracking-[-0.025em]', 'tracking-tight'], - ['tracking-[0em]', 'tracking-normal'], - ['tracking-[0.025em]', 'tracking-wide'], - ['tracking-[0.05em]', 'tracking-wider'], - ['tracking-[0.1em]', 'tracking-widest'], - - // Negative values that don't make sense - // See: https://tailwindcss.com/docs/letter-spacing#using-negative-values - ['-tracking-tighter', 'tracking-wider'], - ['-tracking-tight', 'tracking-wide'], - ['-tracking-normal', 'tracking-normal'], - ['-tracking-wide', 'tracking-tight'], - ['-tracking-wider', 'tracking-tighter'], - ])(testName, { timeout }, async (candidate, expected) => { - await expectCanonicalization( - css` - @import 'tailwindcss'; - @theme { - --tracking-tighter: -0.05em; - --tracking-tight: -0.025em; - --tracking-normal: 0em; - --tracking-wide: 0.025em; - --tracking-wider: 0.05em; - --tracking-widest: 0.1em; - } - `, - candidate, - expected, - ) - }) - - test.each([ - // Keep whitespace characters that are significant - ['[&:has(~_*_*:checked)]:flex', '[&:has(~_*_*:checked)]:flex'], - [ - 'shadow-[inset_0px_1px_--theme(--color-white/15%)]', - 'shadow-[inset_0px_1px_--theme(--color-white/15%)]', - ], - - // Improve readability when whitespace was used for readability - ['w-[calc(100%_-_calc(var(--spacing)*60))]', 'w-[calc(100%-(--spacing(60)))]'], - ['w-[calc(100%_-_--spacing(60))]', 'w-[calc(100%-(--spacing(60)))]'], - - // No need to wrap in `(…)` after a `,` - ['m-[min(100%,_--spacing(6))]', 'm-[min(100%,--spacing(6))]'], - ['m-[min(100%_,_--spacing(6))]', 'm-[min(100%,--spacing(6))]'], - ['m-[min(100%,--spacing(6))]', 'm-[min(100%,--spacing(6))]'], - ])(testName, async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - ` - - await expectCanonicalization(input, candidate, expected) - }) - - // https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1573 - test.each([ - ['-mt-[0.04in]', 'mt-[-0.04in]'], - ['mt-[-0.04in]', 'mt-[-0.04in]'], - ['-mt-[-0.04in]', 'mt-[0.04in]'], - ])(testName, { timeout }, async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - ` - - await expectCanonicalization(input, candidate, expected) - }) - - // https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1591 - test.each([ - ['w-[calc(100%/3.5)]', 'w-[calc(100%/3.5)]'], // Not w-[28.571428571428573%] - ])(testName, { timeout }, async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - ` - - await expectCanonicalization(input, candidate, expected) - }) + await expectCombinedCanonicalization(input, candidates, expected) + }, + ) }) describe('theme to var', () => { - test('extended space scale converts to var or calc', { timeout }, async () => { + test('extended space scale converts to var or calc', async () => { let designSystem = await __unstable__loadDesignSystem( css` @tailwind utilities; @@ -1366,7 +1033,7 @@ describe('theme to var', () => { ]) }) - test('custom space scale converts to var', { timeout }, async () => { + test('custom space scale converts to var', async () => { let designSystem = await __unstable__loadDesignSystem( css` @tailwind utilities; @@ -1393,7 +1060,7 @@ describe('theme to var', () => { }) describe('options', () => { - test('normalize `rem` units to `px`', { timeout }, async () => { + test('normalize `rem` units to `px`', async () => { let designSystem = await __unstable__loadDesignSystem( css` @tailwind utilities; @@ -1410,254 +1077,3 @@ describe('options', () => { expect(designSystem.canonicalizeCandidates(['m-[16px]'])).toEqual(['m-[16px]']) // Ensure options don't influence shared state }) }) - -describe('regressions', () => { - // https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/321 - { - test('a subset of classes should be canonicalizable', { timeout }, async () => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - `) - - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, - } - - expect( - designSystem.canonicalizeCandidates(['underline', 'h-4', 'w-4', 'text-sm'], options), - ).toEqual(['underline', 'text-sm', 'size-4']) - }) - - test('collapse canonicalization is not affected by previous calls', { timeout }, async () => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - `) - - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, - } - - let target = ['underline', 'h-4', 'w-4'] - - expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4']) - - designSystem.canonicalizeCandidates(['mb-4', 'text-sm'], options) - designSystem.canonicalizeCandidates(['underline', 'mb-4'], options) - - expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4']) - expect(designSystem.canonicalizeCandidates(target.concat('text-sm'), options)).toEqual([ - 'underline', - 'text-sm', - 'size-4', - ]) - }) - } - - // https://github.com/tailwindlabs/tailwindcss/pull/19727 - test( - 'collapse does not crash when utilities with no standard properties are present', - { timeout }, - async () => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - `) - - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, - } - - // Shadow utilities use CSS custom properties and @property rules but may - // produce empty property maps in the collapse algorithm. This should not - // crash with "Cannot read properties of null" or "X is not iterable". - expect(() => - designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options), - ).not.toThrow() - - expect(() => designSystem.canonicalizeCandidates(['shadow-md', 'p-4'], options)).not.toThrow() - - expect(() => - designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options), - ).not.toThrow() - - // Verify the candidates are returned (not collapsed, since shadows can't - // meaningfully collapse with unrelated utilities) - expect(designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options)).toEqual( - expect.arrayContaining(['shadow-sm', 'border']), - ) - - expect(designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options)).toEqual( - expect.arrayContaining(['shadow-sm', 'shadow-md']), - ) - }, - ) - - // https://github.com/tailwindlabs/tailwindcss/issues/19835 - test.each([ - // Arbitrary values should be collapsed to another arbitrary value - [ - ['px-[1.2rem]', 'py-[1.2rem]', 'text-left'], - ['text-left', 'p-[1.2rem]'], - ], - - // Arbitrary values could also be collapsed into a bare value - [ - ['px-[30.75rem]', 'py-[30.75rem]', 'text-left'], - ['text-left', 'p-123'], - ], - ])( - 'collapse canonicalization works for arbitrary values', - { timeout }, - async (candidates, expected) => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - `) - - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, - } - - expect(designSystem.canonicalizeCandidates(candidates, options)).toEqual(expected) - }, - ) - - // https://github.com/tailwindlabs/tailwindcss/issues/20051 - test( - 'does not crash when plugin matchComponents rejects speculative values during collapse', - { timeout }, - async () => { - let designSystem = await __unstable__loadDesignSystem( - css` - @import 'tailwindcss'; - @plugin "./plugin.js"; - `, - { - async loadStylesheet(_, base) { - return { - base, - path: '', - content: '@tailwind utilities;', - } - }, - async loadModule() { - return { - base: '', - path: '', - module: plugin(({ matchComponents }) => { - matchComponents( - { - myicon: () => { - throw new Error('Mimic as-if a custom plugin failed for some reason') - }, - }, - { values: { icon: __filename } }, - ) - }), - } - }, - }, - ) - - expect( - designSystem.canonicalizeCandidates(['border-[1.5px]', 'flex'], { - collapse: true, - logicalToPhysical: true, - rem: 16, - }), - ).toEqual(expect.arrayContaining(['border-[1.5px]', 'flex'])) - }, - ) - - // https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1579 - test('does not suggest invalid alternative when canonicalizing calc expressions', async () => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - `) - - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, - } - - expect(designSystem.canonicalizeCandidates(['px-[calc(1rem+0px)]'], options)).toEqual(['px-4']) - }) - - // https://github.com/tailwindlabs/tailwindcss/issues/20295 - test('canonicalizations work when casing of arbitrary values is different', async () => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - @theme { - --color-brand-purple: #3f3cbb; - } - `) - - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, - } - - expect(designSystem.canonicalizeCandidates(['bg-[#3F3cbb]'], options)).toEqual([ - 'bg-brand-purple', - ]) - expect(designSystem.canonicalizeCandidates(['bg-[#3F3Cbb]'], options)).toEqual([ - 'bg-brand-purple', - ]) - expect(designSystem.canonicalizeCandidates(['bg-[#3F3CBB]'], options)).toEqual([ - 'bg-brand-purple', - ]) - }) - - // https://github.com/tailwindlabs/tailwindcss/issues/20365 - test('canonicalizes min/max media query and container query variants', async () => { - let designSystem = await __unstable__loadDesignSystem( - css` - @tailwind utilities; - @theme { - --breakpoint-lg: 64rem; - --container-lg: 32rem; - --spacing: 0.25rem; - } - `, - { base: __dirname }, - ) - - expect(designSystem.canonicalizeCandidates(['min-[64rem]:flex'])).toEqual(['lg:flex']) - expect(designSystem.canonicalizeCandidates(['max-[64rem]:flex'])).toEqual(['max-lg:flex']) - expect(designSystem.canonicalizeCandidates(['@min-[32rem]:flex'])).toEqual(['@lg:flex']) - expect(designSystem.canonicalizeCandidates(['@max-[32rem]:flex'])).toEqual(['@max-lg:flex']) - expect(designSystem.canonicalizeCandidates(['min-[1024px]:flex'], { rem: 16 })).toEqual([ - 'lg:flex', - ]) - }) - - // https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1610 - test('does not merge utilities whose theme variables resolve to CSS-wide keywords', async () => { - let designSystem = await __unstable__loadDesignSystem( - css` - @tailwind utilities; - @theme { - --foreground: unset; - --default: unset; - } - @theme inline { - --color-foreground: var(--foreground); - --color-default-soft-hover: color-mix(in oklab, var(--default) 60%, transparent); - } - `, - { base: __dirname }, - ) - - expect( - designSystem.canonicalizeCandidates(['text-foreground/60', 'text-default-soft-hover']), - ).toEqual(['text-foreground/60', 'text-default-soft-hover']) - }) -}) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index d1dfc09de..13c3bc257 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -11,9 +11,8 @@ import { type NamedUtilityValue, type Variant, } from './candidate' -import { canonicalizeCalcExpressionsAst } from './canonicalize-calc-expressions' import { keyPathToCssProperty } from './compat/apply-config-to-theme' -import { constantFoldDeclaration, constantFoldDeclarationAst } from './constant-fold-declaration' +import { constantFoldDeclaration } from './constant-fold-declaration' import type { DesignSystem as BaseDesignSystem } from './design-system' import { CompileAstFlags } from './design-system' import { expandDeclaration } from './expand-declaration' @@ -72,14 +71,11 @@ interface InternalCanonicalizeOptions { signatureOptions: SignatureOptions } -type RemValue = number | null - interface DesignSystem extends BaseDesignSystem { storage: { - [SIGNATURE_OPTIONS_KEY]: DefaultMap> - [COMPARE_CANDIDATES_KEY]: DefaultMap< - SignatureOptions, - (a: Candidate | string, b: Candidate | string) => boolean + [SIGNATURE_OPTIONS_KEY]: DefaultMap< + number | null, // Rem value + DefaultMap > [INTERNAL_OPTIONS_KEY]: DefaultMap< SignatureOptions, @@ -96,7 +92,7 @@ interface DesignSystem extends BaseDesignSystem { [CANONICALIZE_UTILITY_KEY]: DefaultMap> [CONVERTER_KEY]: (input: string, options?: Convert) => [string, CandidateModifier | null] [SPACING_KEY]: DefaultMap | null - [UTILITY_SIGNATURE_KEY]: DefaultMap> + [UTILITY_SIGNATURE_KEY]: DefaultMap> [STATIC_UTILITIES_KEY]: DefaultMap< SignatureOptions, DefaultMap>> @@ -106,15 +102,12 @@ interface DesignSystem extends BaseDesignSystem { DefaultMap>> > [PRE_COMPUTED_UTILITIES_KEY]: DefaultMap> - [VARIANT_SIGNATURE_KEY]: DefaultMap> - [PRE_COMPUTED_VARIANTS_KEY]: DefaultMap> + [VARIANT_SIGNATURE_KEY]: DefaultMap + [PRE_COMPUTED_VARIANTS_KEY]: DefaultMap } } -export function prepareDesignSystemStorage( - baseDesignSystem: BaseDesignSystem, - options?: CanonicalizeOptions, -): DesignSystem { +export function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignSystem { let designSystem = baseDesignSystem as DesignSystem designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache() @@ -123,14 +116,13 @@ export function prepareDesignSystemStorage( designSystem.storage[CANONICALIZE_VARIANT_KEY] ??= createCanonicalizeVariantCache() designSystem.storage[CANONICALIZE_UTILITY_KEY] ??= createCanonicalizeUtilityCache() designSystem.storage[CONVERTER_KEY] ??= createConverterCache(designSystem) - designSystem.storage[SPACING_KEY] ??= createSpacingCache(designSystem, options) + designSystem.storage[SPACING_KEY] ??= createSpacingCache(designSystem) designSystem.storage[UTILITY_SIGNATURE_KEY] ??= createUtilitySignatureCache(designSystem) designSystem.storage[STATIC_UTILITIES_KEY] ??= createStaticUtilitiesCache() designSystem.storage[UTILITY_PROPERTIES_KEY] ??= createUtilityPropertiesCache(designSystem) designSystem.storage[PRE_COMPUTED_UTILITIES_KEY] ??= createPreComputedUtilitiesCache(designSystem) designSystem.storage[VARIANT_SIGNATURE_KEY] ??= createVariantSignatureCache(designSystem) designSystem.storage[PRE_COMPUTED_VARIANTS_KEY] ??= createPreComputedVariantsCache(designSystem) - designSystem.storage[COMPARE_CANDIDATES_KEY] ??= createSignatureComparison(designSystem) return designSystem } @@ -144,25 +136,6 @@ function createSignatureOptionsCache(): DesignSystem['storage'][typeof SIGNATURE }) } -const COMPARE_CANDIDATES_KEY = Symbol() -function createSignatureComparison(designSystem: DesignSystem) { - return new DefaultMap((options: SignatureOptions) => { - let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options) - - return function hasSameSignature(a: Candidate | string, b: Candidate | string): boolean { - let aCandidateString = typeof a === 'string' ? a : designSystem.printCandidate(a) - let aSignature = signatures.get(aCandidateString) - if (typeof aSignature !== 'string') return false - - let bCandidateString = typeof b === 'string' ? b : designSystem.printCandidate(b) - let bSignature = signatures.get(bCandidateString) - if (typeof bSignature !== 'string') return false - - return aSignature === bSignature - } - }) -} - export function createSignatureOptions( baseDesignSystem: BaseDesignSystem, options?: CanonicalizeOptions, @@ -171,7 +144,7 @@ export function createSignatureOptions( if (options?.collapse) features |= SignatureFeatures.ExpandProperties if (options?.logicalToPhysical) features |= SignatureFeatures.LogicalToPhysical - let designSystem = prepareDesignSystemStorage(baseDesignSystem, options) + let designSystem = prepareDesignSystemStorage(baseDesignSystem) return designSystem.storage[SIGNATURE_OPTIONS_KEY].get(options?.rem ?? null).get(features) } @@ -222,6 +195,7 @@ export function canonicalizeCandidates( } function collapseCandidates(options: InternalCanonicalizeOptions, candidates: string[]): string[] { + if (candidates.length <= 1) return candidates let designSystem = options.designSystem // To keep things simple, we group candidates such that we only collapse @@ -281,126 +255,26 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st computeUtilitiesPropertiesLookup.get(candidate), ) - // Hard-coded optimization: if any candidate sets `line-height` and another - // candidate sets `font-size`, we pre-compute the `text-*` utilities with - // this line-height to try and collapse to those combined values. - if (candidatePropertiesValues.some((x) => x.has('line-height'))) { - let fontSizeNames = designSystem.theme.keysInNamespaces(['--text']) - if (fontSizeNames.length > 0) { - let interestingLineHeights = new Set() - let seenLineHeights = new Set() - for (let pairs of candidatePropertiesValues) { - if (!pairs.has('line-height')) continue - - for (let lineHeight of pairs.get('line-height')) { - if (seenLineHeights.has(lineHeight)) continue - seenLineHeights.add(lineHeight) - - let bareValue = designSystem.storage[SPACING_KEY]?.get(lineHeight) ?? null - if (bareValue !== null) { - if (isValidSpacingMultiplier(bareValue)) { - interestingLineHeights.add(bareValue) - - for (let name of fontSizeNames) { - computeUtilitiesPropertiesLookup.get(`text-${name}/${bareValue}`) - } - } else { - interestingLineHeights.add(lineHeight) - - for (let name of fontSizeNames) { - computeUtilitiesPropertiesLookup.get(`text-${name}/[${lineHeight}]`) - } - } - } - } - } - - let seenFontSizes = new Set() - for (let pairs of candidatePropertiesValues) { - if (!pairs.has('font-size')) continue - - for (let fontSize of pairs.get('font-size')) { - if (seenFontSizes.has(fontSize)) continue - seenFontSizes.add(fontSize) - - for (let lineHeight of interestingLineHeights) { - if (isValidSpacingMultiplier(lineHeight)) { - computeUtilitiesPropertiesLookup.get(`text-[${fontSize}]/${lineHeight}`) - } else { - computeUtilitiesPropertiesLookup.get(`text-[${fontSize}]/[${lineHeight}]`) - } - } - } - } - } - } - - let dynamicUtilities = new DefaultMap((candidate: string) => { - let result = new DefaultMap( - (_property: string) => new DefaultMap((_value: string) => new Set()), - ) - - let relevantProperties = new Set(computeUtilitiesPropertiesLookup.get(candidate).keys()) - if (relevantProperties.size === 0) return result - - for (let parsedCandidate of parseCandidate(designSystem, candidate)) { - if (parsedCandidate.kind !== 'functional' || parsedCandidate.value === null) { - continue - } - - for (let root of designSystem.utilities.keys('functional')) { - if (root === parsedCandidate.root) continue // Skip self - - let replacement = printUnprefixedCandidate(designSystem, { - ...cloneCandidate(parsedCandidate), - root, - }) - - let propertyValues = computeUtilitiesPropertiesLookup.get(replacement) - for (let [property, values] of propertyValues) { - if (!relevantProperties.has(property)) continue // Skip properties that are not relevant for the current candidate - - for (let value of values) { - result.get(property).get(value).add(replacement) - } - } - } - - return result - } - - return result - }) - // For each property, lookup other utilities that also set this property and // this exact value. If multiple properties are used, use the intersection of // each property. // // E.g.: `margin-top` → `mt-1`, `my-1`, `m-1` - let otherUtilities = candidatePropertiesValues.map((propertyValues, idx) => { + let otherUtilities = candidatePropertiesValues.map((propertyValues) => { let result: Set | null = null - for (let property of propertyValues.keys()) { - let otherUtilities = new Set() - for (let group of staticUtilities.get(property).values()) { - for (let candidate of group) { - otherUtilities.add(candidate) - } + for (let [property, values] of propertyValues) { + for (let value of values) { + let otherUtilities = staticUtilities.get(property).get(value) + + if (result === null) result = new Set(otherUtilities) + else result = intersection(result, otherUtilities) + + // The moment no other utilities match, we can stop searching because + // all intersections with an empty set will remain empty. + if (result!.size === 0) return result! } - - for (let value of propertyValues.get(property)) { - for (let candidate of dynamicUtilities.get(candidates[idx]).get(property).get(value)) { - otherUtilities.add(candidate) - } - } - - if (result === null) result = otherUtilities - else result = intersection(result, otherUtilities) - - // The moment no other utilities match, we can stop searching because - // all intersections with an empty set will remain empty. - if (result!.size === 0) return result! } - return result ?? new Set() + return result! }) // Link each candidate that could be linked via another utility @@ -412,10 +286,11 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st // E.g.: `mt-1` and `text-red-500` cannot be collapsed because there is no 3rd // utility with overlapping property/value combinations. let linked = new DefaultMap>((key) => new Set([key])) - for (let i = 0; i < otherUtilities.length; i++) { - let current = otherUtilities[i] - for (let j = i + 1; j < otherUtilities.length; j++) { - let other = otherUtilities[j] + let otherUtilitiesArray = Array.from(otherUtilities) + for (let i = 0; i < otherUtilitiesArray.length; i++) { + let current = otherUtilitiesArray[i] + for (let j = i + 1; j < otherUtilitiesArray.length; j++) { + let other = otherUtilitiesArray[j] for (let property of current) { if (other.has(property)) { @@ -466,17 +341,13 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st designSystem.storage[UTILITY_SIGNATURE_KEY].get(signatureOptions).get(replacement) if (signature !== collapsedSignature) continue // Not a safe replacement - // Use the replacement - result.add(replacement) - - // We can replace all items in the combo with the replacement. If the - // replacement is already part of the combo, keep that one around. + // We can replace all items in the combo with the replacement for (let item of combo) { - if (candidates[item] !== replacement) { - drop.add(candidates[item]) - } + drop.add(candidates[item]) } + // Use the replacement + result.add(replacement) break } } @@ -597,8 +468,6 @@ type UtilityCanonicalizationFunction = ( const UTILITY_CANONICALIZATIONS: UtilityCanonicalizationFunction[] = [ bgGradientToLinear, themeToVarUtility, - calcToSpacingFunction, - optimizeArbitraryValueExpressions, arbitraryUtilities, bareValueUtilities, deprecatedUtilities, @@ -712,69 +581,6 @@ function themeToVarVariant( return variant } -function calcToSpacingFunction( - candidate: Candidate, - options: InternalCanonicalizeOptions, -): Candidate { - if (candidate.kind === 'arbitrary') { - candidate.value = spacingCalcToSpacingFunction(candidate.value, options.designSystem) - } else if (candidate.kind === 'functional' && candidate.value?.kind === 'arbitrary') { - candidate.value.value = spacingCalcToSpacingFunction( - candidate.value.value, - options.designSystem, - ) - } - - return candidate -} - -function spacingCalcToSpacingFunction(input: string, designSystem: DesignSystem) { - let spacingVariable = designSystem.theme.prefix - ? `--${designSystem.theme.prefix}-spacing` - : '--spacing' - - let ast = ValueParser.parse(input) - - walk(ast, (node) => { - // calc(…) - if (node.kind !== 'function' || node.value !== 'calc') return - - // calc(var(--spacing) * 2) - // -------------- 1. function (var) - // - 2. separator ( ) - // - 3. word (*) - // - 4. separator ( ) - // - 5. any value, word (2) - if (node.nodes.length !== 5) return - - // * - if (node.nodes[2].kind !== 'word' || node.nodes[2].value !== '*') { - return - } - - // var(--spacing) - if ( - node.nodes[0].kind !== 'function' || - node.nodes[0].value !== 'var' || - node.nodes[0].nodes.length !== 1 || - node.nodes[0].nodes[0].kind !== 'word' || - node.nodes[0].nodes[0].value !== spacingVariable - ) { - return - } - - return WalkAction.Replace( - ValueParser.parse( - `--spacing(${ - ValueParser.toCss([node.nodes[4]]) // Value node - })`, - ), - ) - }) - - return ValueParser.toCss(ast) -} - const CONVERTER_KEY = Symbol() function createConverterCache( designSystem: DesignSystem, @@ -979,10 +785,10 @@ function substituteFunctionsInValue( fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path) if (replacement === null) return - { - let idx = ctx.index - 1 + if (ctx.parent) { + let idx = ctx.parent.nodes.indexOf(node) - 1 while (idx !== -1) { - let previous = ctx.siblings[idx] + let previous = ctx.parent.nodes[idx] // Skip the space separator if (previous.kind === 'separator' && previous.value.trim() === '') { idx -= 1 @@ -1073,56 +879,19 @@ function printUnprefixedCandidate(designSystem: DesignSystem, candidate: Candida // ---- const SPACING_KEY = Symbol() - -// We prefer bare values over arbitrary values such as `left-[6px]` → -// `left-1.5`. -// -// There are situations where this doesn't always make sense. E.g.: -// `left-[99999px]` → `left-24999.75`. -// -// We could try and analyze the incoming value and bail out if we detect certain -// patterns. But a first step would be to limit the bare values for large values -// and keep them low. The largest rem value we have is for the 2xl breakpoint, -// which is 96rem, which is 1536px. -const MAX_BARE_VALUE_IN_PX = 1536 -const MAX_BARE_VALUE_IN_REM = MAX_BARE_VALUE_IN_PX / 16 - -function isReasonableBareValue(value: number, designSystem: DesignSystem, rem: number | null) { - let spacingMultiplier = designSystem.resolveThemeValue('--spacing') - if (spacingMultiplier === undefined) return false - - let parsed = dimensions.get(constantFoldDeclaration(spacingMultiplier, rem)) - if (parsed === null) return false - - let [spacingValue, spacingUnit] = parsed - let bareValueInPixels = value * spacingValue - - if (spacingUnit === 'px') return bareValueInPixels <= MAX_BARE_VALUE_IN_PX - if (spacingUnit === 'rem') return bareValueInPixels <= MAX_BARE_VALUE_IN_REM - return false -} - function createSpacingCache( designSystem: DesignSystem, - options?: CanonicalizeOptions, ): DesignSystem['storage'][typeof SPACING_KEY] { let spacingMultiplier = designSystem.resolveThemeValue('--spacing') if (spacingMultiplier === undefined) return null - spacingMultiplier = constantFoldDeclaration(spacingMultiplier, options?.rem ?? null) - let parsed = dimensions.get(spacingMultiplier) if (!parsed) return null let [value, unit] = parsed return new DefaultMap((input) => { - // If we already know that the spacing multiplier is 0, all spacing - // multipliers will also be 0. No need to even try and parse/canonicalize - // the input value. - if (value === 0) return null - - let parsed = dimensions.get(constantFoldDeclaration(input, options?.rem ?? null)) + let parsed = dimensions.get(input) if (!parsed) return null let [myValue, myUnit] = parsed @@ -1146,7 +915,6 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO let designSystem = options.designSystem let utilities = designSystem.storage[PRE_COMPUTED_UTILITIES_KEY].get(options.signatureOptions) let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) - let hasSameSignature = designSystem.storage[COMPARE_CANDIDATES_KEY].get(options.signatureOptions) let targetCandidateString = designSystem.printCandidate(candidate) @@ -1156,7 +924,9 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO // Try a few options to find a suitable replacement utility for (let replacementCandidate of tryReplacements(targetSignature, candidate)) { - if (!hasSameSignature(candidate, replacementCandidate)) { + let replacementString = designSystem.printCandidate(replacementCandidate) + let replacementSignature = signatures.get(replacementString) + if (replacementSignature !== targetSignature) { continue } @@ -1177,29 +947,11 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO // Find a corresponding utility for the same signature let replacements = utilities.get(targetSignature) - // Multiple utilities can map to the same signature. - if (replacements.length > 1) { - // Prefer positive values over negative values - let maybeReplacement: string | undefined = undefined - for (let replacement of replacements) { - if (replacement[0] === '-') continue // Skip negative values - - // If multiple non-negative replacements exists then we are unsure - // what to do, so let's bail. - if (maybeReplacement) return - - // Consider this replacement - maybeReplacement = replacement - } - - if (maybeReplacement) { - for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) { - yield replacementCandidate - } - } - - return - } + // Multiple utilities can map to the same signature. Not sure how to migrate + // this one so let's just skip it for now. + // + // TODO: Do we just migrate to the first one? + if (replacements.length > 1) return // If we didn't find any replacement utilities, let's try to strip the // modifier and find a replacement then. If we do, we can try to re-add the @@ -1238,27 +990,6 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO candidate.kind === 'arbitrary' ? candidate.value : (candidate.value?.value ?? null) if (value === null) return - // Try to canonicalize any incoming arbitrary value. Canonicalization of - // `rem` and `px` values will be converted to `px`, so we have to - // canonicalize the spacing multiplier as well. - if ( - options.signatureOptions.rem !== null && - candidate.kind === 'functional' && - candidate.value?.kind === 'arbitrary' - ) { - let bareValue = designSystem.storage[SPACING_KEY]?.get(value) ?? null - if (bareValue !== null) { - if ( - isValidSpacingMultiplier(bareValue) && - isReasonableBareValue(bareValue, designSystem, options.signatureOptions.rem) - ) { - yield Object.assign({}, candidate, { - value: { kind: 'named', value: bareValue, fraction: null }, - }) - } - } - } - let spacingMultiplier = designSystem.storage[SPACING_KEY]?.get(value) ?? null let rootPrefix = '' if (spacingMultiplier !== null && spacingMultiplier < 0) { @@ -1292,11 +1023,7 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO // Try bare value based on the `--spacing` value. E.g.: // // - `w-[64rem]` → `w-256` - if ( - spacingMultiplier !== null && - isValidSpacingMultiplier(spacingMultiplier) && - isReasonableBareValue(spacingMultiplier, designSystem, options.signatureOptions.rem) - ) { + if (spacingMultiplier !== null) { for (let replacementCandidate of parseCandidate( designSystem, `${root}-${spacingMultiplier}`, @@ -1400,7 +1127,6 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO let designSystem = options.designSystem let utilities = designSystem.storage[PRE_COMPUTED_UTILITIES_KEY].get(options.signatureOptions) let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) - let hasSameSignature = designSystem.storage[COMPARE_CANDIDATES_KEY].get(options.signatureOptions) let targetCandidateString = designSystem.printCandidate(candidate) @@ -1410,7 +1136,9 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO // Try a few options to find a suitable replacement utility for (let replacementCandidate of tryReplacements(targetSignature, candidate)) { - if (!hasSameSignature(candidate, replacementCandidate)) { + let replacementString = designSystem.printCandidate(replacementCandidate) + let replacementSignature = signatures.get(replacementString) + if (replacementSignature !== targetSignature) { continue } @@ -1426,29 +1154,11 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO // Find a corresponding utility for the same signature let replacements = utilities.get(targetSignature) - // Multiple utilities can map to the same signature. - if (replacements.length > 1) { - // Prefer positive values over negative values - let maybeReplacement: string | undefined = undefined - for (let replacement of replacements) { - if (replacement[0] === '-') continue // Skip negative values - - // If multiple non-negative replacements exists then we are unsure - // what to do, so let's bail. - if (maybeReplacement) return - - // Consider this replacement - maybeReplacement = replacement - } - - if (maybeReplacement) { - for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) { - yield replacementCandidate - } - } - - return - } + // Multiple utilities can map to the same signature. Not sure how to migrate + // this one so let's just skip it for now. + // + // TODO: Do we just migrate to the first one? + if (replacements.length > 1) return // If we didn't find any replacement utilities, let's try to strip the // modifier and find a replacement then. If we do, we can try to re-add the @@ -1486,47 +1196,31 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO const DEPRECATION_MAP = new Map([ ['order-none', 'order-0'], ['break-words', 'wrap-break-word'], - ['overflow-ellipsis', 'text-ellipsis'], ]) -const DEPRECATION_TRANSFORMATION_MAP = new Map([ - [/^(-)?start-(.*?)$/, '$1inset-s-$2'], - [/^(-)?end-(.*?)$/, '$1inset-e-$2'], -]) - -function* tryDeprecatedUtilities(candidate: string) { - // Try static replacements - let replacement = DEPRECATION_MAP.get(candidate) - if (replacement) yield replacement - - // Try dynamic replacements - for (let [searchValue, replaceValue] of DEPRECATION_TRANSFORMATION_MAP) { - let replacement = candidate.replace(searchValue, replaceValue) - if (replacement === candidate) continue - - yield replacement - } -} - function deprecatedUtilities( candidate: Candidate, options: InternalCanonicalizeOptions, ): Candidate { let designSystem = options.designSystem - let hasSameSignature = designSystem.storage[COMPARE_CANDIDATES_KEY].get(options.signatureOptions) + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) let targetCandidateString = printUnprefixedCandidate(designSystem, candidate) - for (let replacementString of tryDeprecatedUtilities(targetCandidateString)) { - if (!hasSameSignature(candidate, replacementString)) { - continue - } + let replacementString = DEPRECATION_MAP.get(targetCandidateString) ?? null + if (replacementString === null) return candidate - let [replacement] = parseCandidate(designSystem, replacementString) - return replacement - } + let legacySignature = signatures.get(targetCandidateString) + if (typeof legacySignature !== 'string') return candidate - return candidate + let replacementSignature = signatures.get(replacementString) + if (typeof replacementSignature !== 'string') return candidate + + // Not the same signature, not safe to migrate + if (legacySignature !== replacementSignature) return candidate + + let [replacement] = parseCandidate(designSystem, replacementString) + return replacement } // ---- @@ -1536,8 +1230,8 @@ function arbitraryVariants( options: InternalCanonicalizeOptions, ): Variant | Variant[] { let designSystem = options.designSystem - let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY].get(options.signatureOptions.rem) - let variants = designSystem.storage[PRE_COMPUTED_VARIANTS_KEY].get(options.signatureOptions.rem) + let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] + let variants = designSystem.storage[PRE_COMPUTED_VARIANTS_KEY] let iterator = walkVariants(variant) for (let [variant] of iterator) { @@ -1548,11 +1242,7 @@ function arbitraryVariants( if (typeof targetSignature !== 'string') continue let foundVariants = variants.get(targetSignature) - if (foundVariants.length === 0) continue - - // The variant is already in a canonical form, e.g.: `min-lg` and `lg` - // produce the same CSS, but both are valid so we keep them as-is. - if (foundVariants.includes(targetString)) continue + if (foundVariants.length !== 1) continue let foundVariant = foundVariants[0] let parsedVariant = designSystem.parseVariant(foundVariant) @@ -1758,12 +1448,12 @@ function* tryValueReplacements( // ---- function isSingleSelector(ast: SelectorParser.SelectorAstNode[]): boolean { - if (ast.length === 1 && ast[0].kind === 'list') return false - return true + return !ast.some((node) => node.kind === 'separator' && node.value.trim() === ',') } -function isAttributeSelector(node: SelectorParser.SelectorNode): boolean { - return node.value[0] === '[' && node.value[node.value.length - 1] === ']' +function isAttributeSelector(node: SelectorParser.SelectorAstNode): boolean { + let value = node.value.trim() + return node.kind === 'selector' && value[0] === '[' && value[value.length - 1] === ']' } function modernizeArbitraryValuesVariant( @@ -1772,7 +1462,7 @@ function modernizeArbitraryValuesVariant( ): Variant | Variant[] { let result = [variant] let designSystem = options.designSystem - let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY].get(options.signatureOptions.rem) + let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] let iterator = walkVariants(variant) for (let [variant, parent] of iterator) { @@ -1794,17 +1484,11 @@ function modernizeArbitraryValuesVariant( // Expecting a non-relative arbitrary variant if (variant.relative) continue - let ast = SelectorParser.parse(variant.selector) + let ast = SelectorParser.parse(variant.selector.trim()) // Expecting a single selector node if (!isSingleSelector(ast)) continue - // Keep the existing arbitrary variant modernizations working on a flat - // selector sequence while the parser exposes complex selectors as a node. - if (ast.length === 1 && ast[0].kind === 'complex') { - ast = ast[0].nodes - } - // `[&>*]` can be replaced with `*` if ( // Only top-level, so `has-[&>*]` is not supported @@ -1815,7 +1499,7 @@ function modernizeArbitraryValuesVariant( ast[0].kind === 'selector' && ast[0].value === '&' && ast[1].kind === 'combinator' && - ast[1].value === '>' && + ast[1].value.trim() === '>' && ast[2].kind === 'selector' && ast[2].value === '*' ) { @@ -1833,7 +1517,7 @@ function modernizeArbitraryValuesVariant( ast[0].kind === 'selector' && ast[0].value === '&' && ast[1].kind === 'combinator' && - ast[1].value === ' ' && + ast[1].value.trim() === '' && // space, but trimmed because there could be multiple spaces ast[2].kind === 'selector' && ast[2].value === '*' ) { @@ -1841,30 +1525,6 @@ function modernizeArbitraryValuesVariant( continue } - // `[&:has(…)]` can be replaced with `has-[…]` - if ( - // Only top-level, so `group-[&:has(…)]` is not covered - - parent === null && - // [&:has(…)]:flex - // ^ ^^^^^^ - ast.length === 1 && - ast[0].kind === 'compound' && - ast[0].nodes.length === 2 && - ast[0].nodes[0].kind === 'selector' && - ast[0].nodes[0].value === '&' && - ast[0].nodes[1].kind === 'function' && - ast[0].nodes[1].value === ':has' && - ast[0].nodes[1].nodes.length === 1 && - ast[0].nodes[1].nodes[0].kind === 'selector' - ) { - replaceObject( - variant, - designSystem.parseVariant(`has-[${SelectorParser.toCss(ast[0].nodes[1].nodes, true)}]`), - ) - continue - } - // `in-*` variant. If the selector ends with ` &`, we can convert it to an // `in-*` variant. // @@ -1875,9 +1535,8 @@ function modernizeArbitraryValuesVariant( // [[data-visible]___&]:flex // ^^^^^^^^^^^^^^ ^ ^ ast.length === 3 && - ast[0].kind === 'selector' && ast[1].kind === 'combinator' && - ast[1].value === ' ' && + ast[1].value.trim() === '' && // Space, but trimmed because there could be multiple spaces ast[2].kind === 'selector' && ast[2].value === '&' ) { @@ -1889,7 +1548,7 @@ function modernizeArbitraryValuesVariant( // that we can convert `[[data-visible]_&]` to `in-[[data-visible]]`. // // Later this gets converted to `in-data-visible`. - replaceObject(variant, designSystem.parseVariant(`in-[${SelectorParser.toCss(ast, true)}]`)) + replaceObject(variant, designSystem.parseVariant(`in-[${SelectorParser.toCss(ast)}]`)) continue } @@ -1907,7 +1566,7 @@ function modernizeArbitraryValuesVariant( ) { let targetSignature = signatures.get(designSystem.printVariant(variant)) - let parsed = ValueParser.parse(SelectorParser.toCss(ast, true)) + let parsed = ValueParser.parse(SelectorParser.toCss(ast)) let containsNot = false walk(parsed, (node) => { if (node.kind === 'word' && node.value === 'not') { @@ -1947,11 +1606,11 @@ function modernizeArbitraryValuesVariant( // ^ ^ ^^^^^^^^^^^^^^ ast.length === 3 && ast[0].kind === 'selector' && - ast[0].value === '&' && + ast[0].value.trim() === '&' && ast[1].kind === 'combinator' && - ast[1].value === '>' && + ast[1].value.trim() === '>' && ast[2].kind === 'selector' && - (ast[2].value[0] === ':' || isAttributeSelector(ast[2])) + (isAttributeSelector(ast[2]) || ast[2].value[0] === ':') ) { ast = [ast[2]] prefixedVariant = designSystem.parseVariant('*') @@ -1965,34 +1624,20 @@ function modernizeArbitraryValuesVariant( // ^ ^^^^^^^^^^^^^^ ast.length === 3 && ast[0].kind === 'selector' && - ast[0].value === '&' && + ast[0].value.trim() === '&' && ast[1].kind === 'combinator' && - ast[1].value === ' ' && + ast[1].value.trim() === '' && // space, but trimmed because there could be multiple spaces ast[2].kind === 'selector' && - (ast[2].value[0] === ':' || isAttributeSelector(ast[2])) + (isAttributeSelector(ast[2]) || ast[2].value[0] === ':') ) { ast = [ast[2]] prefixedVariant = designSystem.parseVariant('**') } - let selectorNodes = ast - walk(selectorNodes, { - enter(node) { - // Filter out `&`. E.g.: `&[data-foo]` => `[data-foo]` - if (node.kind === 'selector' && node.value === '&') { - return WalkAction.Replace([]) - } - - if (node.kind === 'function') { - return WalkAction.Skip - } - }, - exit(node) { - if (node.kind === 'compound' && node.nodes.length === 1) { - return WalkAction.ReplaceSkip(node.nodes) - } - }, - }) + // Filter out `&`. E.g.: `&[data-foo]` => `[data-foo]` + let selectorNodes = ast.filter( + (node) => !(node.kind === 'selector' && node.value.trim() === '&'), + ) // Expecting a single selector (normal selector or attribute selector) if (selectorNodes.length !== 1) continue @@ -2009,7 +1654,7 @@ function modernizeArbitraryValuesVariant( } // Expecting a single attribute selector - if (target.nodes[0].kind === 'selector' && !isAttributeSelector(target.nodes[0])) continue + if (!isAttributeSelector(target.nodes[0])) continue // Unwrap the selector from inside `&:is(…)` target = target.nodes[0] @@ -2081,7 +1726,7 @@ function modernizeArbitraryValuesVariant( return `${variantName}-${targetNode.nodes[0].value}` } - return `${variantName}-[${SelectorParser.toCss(targetNode.nodes, true)}]` + return `${variantName}-[${SelectorParser.toCss(targetNode.nodes)}]` } } @@ -2122,7 +1767,7 @@ function modernizeArbitraryValuesVariant( } // Expecting an attribute selector - else if (target.kind === 'selector' && isAttributeSelector(target)) { + else if (isAttributeSelector(target)) { let attributeSelector = AttributeSelectorParser.parse(target.value) if (attributeSelector === null) continue // Invalid attribute selector @@ -2184,95 +1829,6 @@ function modernizeArbitraryValuesVariant( return result } -// --- - -function optimizeArbitraryValueExpressions( - candidate: Candidate, - options: InternalCanonicalizeOptions, -): Candidate { - if (candidate.kind !== 'functional' || candidate.value?.kind !== 'arbitrary') { - return candidate - } - - let designSystem = options.designSystem - let hasSameSignature = designSystem.storage[COMPARE_CANDIDATES_KEY].get(options.signatureOptions) - - let valueAst = ValueParser.parse(candidate.value.value) - - // Start by constant folding the value expression, when dealing with `calc(…)` - if (valueAst.length === 1 && valueAst[0].kind === 'function' && valueAst[0].value === 'calc') { - let [folded, foldedValueAst] = constantFoldDeclarationAst(valueAst, null, false) - if (folded) { - let replacement = cloneCandidate(candidate) - replacement.value!.value = ValueParser.toCss(foldedValueAst) - - if (hasSameSignature(candidate, replacement)) { - candidate = replacement - valueAst = foldedValueAst - } - } - } - - // Move `-` sign into the arbitrary value itself - if (candidate.root[0] === '-') { - // We're dealing with a `var(…)`, keep as-is - if (valueAst.length === 1 && valueAst[0].kind === 'function' && valueAst[0].value === 'var') { - return candidate - } - - // Move `* -1` inside, and try to constant fold to see if it's even worth - // updating the candidate or not. - let expressionAst = ValueParser.parse(`calc(${candidate.value!.value} * -1)`) - let [folded, foldedExpressionAst] = constantFoldDeclarationAst(expressionAst, null, false) - if (folded) { - let replacement = cloneCandidate(candidate) - - replacement.root = replacement.root.slice(1) // Drop the leading `-` - replacement.value!.value = ValueParser.toCss(foldedExpressionAst) - - if (hasSameSignature(candidate, replacement)) { - candidate = replacement - valueAst = foldedExpressionAst - } - } - } - - // Move `-` sign out of the arbitrary value - if (valueAst.length === 1 && valueAst[0].kind === 'function' && valueAst[0].value === 'calc') { - let calcArgs = valueAst[0].nodes - - // `calc(arg * -1)` or `calc(-1 * arg)` - if ( - calcArgs.length === 5 && - calcArgs[1].kind === 'separator' && - calcArgs[1].value === ' ' && - calcArgs[2].kind === 'word' && - calcArgs[2].value === '*' && - calcArgs[3].kind === 'separator' && - calcArgs[3].value === ' ' - ) { - let arg = - calcArgs[4].kind === 'word' && calcArgs[4].value === '-1' - ? calcArgs[0] - : calcArgs[0].kind === 'word' && calcArgs[0].value === '-1' - ? calcArgs[4] - : null - - if (arg) { - let replacement = cloneCandidate(candidate) - replacement.root = `-${candidate.root}` - replacement.value!.value = ValueParser.toCss([arg]) - - if (hasSameSignature(candidate, replacement)) { - candidate = replacement - } - } - } - } - - return candidate -} - // ---- // Optimize the modifier @@ -2390,7 +1946,7 @@ function createUtilitySignatureCache( designSystem: DesignSystem, ): DesignSystem['storage'][typeof UTILITY_SIGNATURE_KEY] { return new DefaultMap((options: SignatureOptions) => { - return new DefaultMap((utility) => { + return new DefaultMap((utility) => { try { // Ensure the prefix is added to the utility if it is not already present. utility = @@ -2428,12 +1984,6 @@ function createUtilitySignatureCache( }) } -// #RGB -// #RGBA -// #RRGGBB -// #RRGGBBAA -const HEX_REGEX = /#(?:[a-f0-9]{8}|[a-f0-9]{6}|[a-f0-9]{4}|[a-f0-9]{3})/gi - // Optimize the CSS AST to make it suitable for signature comparison. We want to // expand declarations, ignore comments, sort declarations etc... function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: SignatureOptions) { @@ -2450,7 +2000,7 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si // Ignore `--tw-{property}` if `{property}` exists with the same value if (node.property.startsWith('--tw-')) { if ( - ctx.siblings.some( + (ctx.parent?.nodes ?? []).some( (sibling) => sibling.kind === 'declaration' && node.value === sibling.value && @@ -2472,8 +2022,6 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si node.value = resolveVariablesInValue(node.value, designSystem) } - let valueAst = ValueParser.parse(node.value) - // Very basic `calc(…)` constant folding to handle the spacing scale // multiplier: // @@ -2482,17 +2030,7 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si // → `calc(0.25rem * 4)` ← this is the case we will see // after inlining the variable // → `1rem` - let [folded, foldedValueAst] = constantFoldDeclarationAst(valueAst, rem) - - // Normalize `calc(…)` expressions such that arguments that are - // associatively equivalent are rendered in the same way. - // - // `calc(var(--foo) * -1)` === `calc(-1 * var(--foo))` - let [normalized, canonicalizedValueAst] = canonicalizeCalcExpressionsAst(foldedValueAst) - - if (folded || normalized) { - node.value = ValueParser.toCss(canonicalizedValueAst) - } + node.value = constantFoldDeclaration(node.value, rem) // We will normalize the `node.value`, this is the same kind of logic // we use when printing arbitrary values. It will remove unnecessary @@ -2519,69 +2057,18 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si }, exit(node) { if (node.kind === 'rule' || node.kind === 'at-rule') { - // Remove declarations that are re-defined again later. - // - // This could maybe result in unwanted behavior (because similar - // properties typically exist for backwards compatibility), but for - // signature purposes we can assume that the last declaration wins. - if (node.nodes.length > 1) { - let seen = new Set() - for (let i = node.nodes.length - 1; i >= 0; i--) { - let child = node.nodes[i] - if (child.kind !== 'declaration') continue - if (child.value === undefined) continue - - if (seen.has(child.property)) { - node.nodes.splice(i, 1) - } - seen.add(child.property) - } - } - - // Sort declarations alphabetically by property name node.nodes.sort((a, b) => { if (a.kind !== 'declaration') return 0 if (b.kind !== 'declaration') return 0 return a.property.localeCompare(b.property) }) } - - // - else if (node.kind === 'declaration' && node.value) { - // Leave CSS variables alone - if (node.property[0] === '-' && node.property[1] === '-') return - - // Ensure hex colors are always lowercased - { - HEX_REGEX.lastIndex = 0 - node.value = node.value.replace(HEX_REGEX, (color) => color.toLowerCase()) - } - } }, }) return ast } -// Variables whose theme value is a CSS-wide keyword (e.g.: `unset`) are never -// inlined. These are typically registered as a placeholder to be re-assigned at -// runtime, so two variables that share such a value are not interchangeable. -// -// E.g.: -// -// ```css -// @theme { -// --foreground: unset; -// --background: unset; -// } -// ``` -// -// Inlining would make `text-(--foreground)` and `text-(--background)` produce -// the same signature `color: unset`, even though they are different at runtime. -// -// https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/Data_types#css-wide_keywords -const CSS_WIDE_KEYWORDS = ['initial', 'inherit', 'revert', 'revert-layer', 'revert-rule', 'unset'] - // Resolve theme values to their inlined value. // // E.g.: @@ -2597,8 +2084,8 @@ const CSS_WIDE_KEYWORDS = ['initial', 'inherit', 'revert', 'revert-layer', 'reve // } // ``` // -// Which conveniently will be equivalent to: `text-red-500` when we inline the -// value. +// Which conveniently will be equivalent to: `text-red-500` when we inline +// the value. // // Without inlining: // ```css @@ -2614,13 +2101,13 @@ const CSS_WIDE_KEYWORDS = ['initial', 'inherit', 'revert', 'revert-layer', 'reve // } // ``` // -// Recently we made sure that utilities like `text-red-500` also generate the -// fallback value for usage in `@reference` mode. +// Recently we made sure that utilities like `text-red-500` also generate +// the fallback value for usage in `@reference` mode. // -// The second assumption is that if you use `var(--key, fallback)` that happens -// to match a known variable _and_ its inlined value. Then we can replace it -// with the inlined variable. This allows us to handle custom `@theme` and -// `@theme inline` definitions. +// The second assumption is that if you use `var(--key, fallback)` that +// happens to match a known variable _and_ its inlined value. Then we can +// replace it with the inlined variable. This allows us to handle custom +// `@theme` and `@theme inline` definitions. function resolveVariablesInValue(value: string, designSystem: DesignSystem): string { let changed = false let valueAst = ValueParser.parse(value) @@ -2649,9 +2136,6 @@ function resolveVariablesInValue(value: string, designSystem: DesignSystem): str seen.add(variable) if (variableValue === undefined) return // Couldn't resolve the variable - // CSS-wide keywords are never inlined - if (CSS_WIDE_KEYWORDS.includes(variableValue.toLowerCase())) return - // Inject variable fallbacks when no fallback is present yet. // // A fallback could consist of multiple values. @@ -2716,9 +2200,13 @@ function createUtilityPropertiesCache( let parsed = designSystem.parseCandidate(className) if (parsed.length === 0) return localPropertyValueLookup - try { - let ast = designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node)) - walk(canonicalizeAst(designSystem, ast, options), (node) => { + walk( + canonicalizeAst( + designSystem, + designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node)), + options, + ), + (node) => { if (node.kind === 'declaration') { localPropertyValueLookup.get(node.property).add(node.value!) designSystem.storage[STATIC_UTILITIES_KEY].get(options) @@ -2726,12 +2214,8 @@ function createUtilityPropertiesCache( .get(node.value!) .add(className) } - }) - } catch { - // Ignore errors, this could happen when we call a plugin with a value - // it didn't expect. But since plugins are functions, we can't know - // ahead of time what it expects. - } + }, + ) return localPropertyValueLookup }) @@ -2810,110 +2294,92 @@ export const VARIANT_SIGNATURE_KEY = Symbol() function createVariantSignatureCache( designSystem: DesignSystem, ): DesignSystem['storage'][typeof VARIANT_SIGNATURE_KEY] { - return new DefaultMap((rem: number | null) => { - return new DefaultMap((variant) => { - try { - // Ensure the prefix is added to the utility if it is not already present. - variant = - designSystem.theme.prefix && !variant.startsWith(designSystem.theme.prefix) - ? `${designSystem.theme.prefix}:${variant}` - : variant + return new DefaultMap((variant) => { + try { + // Ensure the prefix is added to the utility if it is not already present. + variant = + designSystem.theme.prefix && !variant.startsWith(designSystem.theme.prefix) + ? `${designSystem.theme.prefix}:${variant}` + : variant - // Use `@apply` to normalize the selector to `.x` - let ast: AstNode[] = [styleRule('.x', [atRule('@apply', `${variant}:flex`)])] - substituteAtApply(ast, designSystem) + // Use `@apply` to normalize the selector to `.x` + let ast: AstNode[] = [styleRule('.x', [atRule('@apply', `${variant}:flex`)])] + substituteAtApply(ast, designSystem) - // Canonicalize selectors to their minimal form - walk(ast, (node) => { - // At-rules - if (node.kind === 'at-rule') { - // Normalize dimensions such that equivalent values in different - // units are considered the same. E.g.: with a root font size of - // `16px`, `@media (width >= 64rem)` and `@media (width >= 1024px)` - // are equivalent. - node.params = constantFoldDeclaration(node.params, rem) + // Canonicalize selectors to their minimal form + walk(ast, (node) => { + // At-rules + if (node.kind === 'at-rule' && node.params.includes(' ')) { + node.params = node.params.replaceAll(' ', '') + } - if (node.params.includes(' ')) { - node.params = node.params.replaceAll(' ', '') + // Style rules + else if (node.kind === 'rule') { + let selectorAst = SelectorParser.parse(node.selector) + let changed = false + walk(selectorAst, (node) => { + if (node.kind === 'separator' && node.value !== ' ') { + node.value = node.value.trim() + changed = true } - } - // Style rules - else if (node.kind === 'rule') { - let selectorAst = SelectorParser.parse(node.selector) - let changed = false - walk(selectorAst, (node) => { - // Assumption: when we have a list of selectors: `.foo, .bar` we - // want to mark this as changed because this will be re-printed as - // `.foo,.bar`. + // Remove unnecessary `:is(…)` selectors + else if (node.kind === 'function' && node.value === ':is') { + // A single selector inside of `:is(…)` can be replaced with the + // selector itself. // - // Similarly, when we receive a combinator like `.foo + .bar`, this - // will be printed as `.foo+.bar`. - // - // It could be that this was already optimal, but then this would be - // a no-op situation. - if (node.kind === 'list' || node.kind === 'combinator') { + // E.g.: `:is(.foo)` → `.foo` + if (node.nodes.length === 1) { changed = true + return WalkAction.Replace(node.nodes) } - // Remove unnecessary `:is(…)` selectors - else if (node.kind === 'function' && node.value === ':is') { - // A single selector inside of `:is(…)` can be replaced with the - // selector itself. - // - // E.g.: `:is(.foo)` → `.foo` - if (node.nodes.length === 1) { - changed = true - return WalkAction.Replace(node.nodes) - } - - // A selector with the universal selector `*` followed by a pseudo - // class, can be replaced with the pseudo class itself. - else if ( - node.nodes.length === 2 && - node.nodes[0].kind === 'selector' && - node.nodes[0].value === '*' && - node.nodes[1].kind === 'selector' && - node.nodes[1].value[0] === ':' - ) { - changed = true - return WalkAction.Replace(node.nodes[1]) - } - } - - // Ensure `*` exists before pseudo selectors inside of `:not(…)`, - // `:where(…)`, … - // - // E.g.: - // - // `:not(:first-child)` → `:not(*:first-child)` - // + // A selector with the universal selector `*` followed by a pseudo + // class, can be replaced with the pseudo class itself. else if ( - node.kind === 'function' && - node.value[0] === ':' && - node.nodes[0]?.kind === 'selector' && - node.nodes[0]?.value[0] === ':' + node.nodes.length === 2 && + node.nodes[0].kind === 'selector' && + node.nodes[0].value === '*' && + node.nodes[1].kind === 'selector' && + node.nodes[1].value[0] === ':' ) { changed = true - node.nodes.unshift({ kind: 'selector', value: '*' }) + return WalkAction.Replace(node.nodes[1]) } - }) - - if (changed) { - node.selector = SelectorParser.toCss(selectorAst, true) } - } - }) - // Compute the final signature, by generating the CSS for the variant - let signature = toCss(ast) - return signature - } catch { - // A unique symbol is returned to ensure that 2 signatures resulting in - // `null` are not considered equal. - return Symbol() - } - }) + // Ensure `*` exists before pseudo selectors inside of `:not(…)`, + // `:where(…)`, … + // + // E.g.: + // + // `:not(:first-child)` → `:not(*:first-child)` + // + else if ( + node.kind === 'function' && + node.value[0] === ':' && + node.nodes[0]?.kind === 'selector' && + node.nodes[0]?.value[0] === ':' + ) { + changed = true + node.nodes.unshift({ kind: 'selector', value: '*' }) + } + }) + + if (changed) { + node.selector = SelectorParser.toCss(selectorAst) + } + } + }) + + // Compute the final signature, by generating the CSS for the variant + let signature = toCss(ast) + return signature + } catch { + // A unique symbol is returned to ensure that 2 signatures resulting in + // `null` are not considered equal. + return Symbol() + } }) } @@ -2921,31 +2387,19 @@ export const PRE_COMPUTED_VARIANTS_KEY = Symbol() function createPreComputedVariantsCache( designSystem: DesignSystem, ): DesignSystem['storage'][typeof PRE_COMPUTED_VARIANTS_KEY] { - return new DefaultMap((rem: number | null) => { - let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY].get(rem) - let lookup = new DefaultMap(() => []) + let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] + let lookup = new DefaultMap(() => []) - for (let [root, variant] of designSystem.variants.entries()) { - // Actual static variants - if (variant.kind === 'static') { - let signature = signatures.get(root) - if (typeof signature !== 'string') continue - lookup.get(signature).push(root) - } - - // Functional variants with known values, e.g.: `max-lg`, `@min-md`, … - else if (variant.kind === 'functional') { - for (let value of designSystem.variants.getCompletions(root)) { - let name = root === '@' ? `@${value}` : `${root}-${value}` - let signature = signatures.get(name) - if (typeof signature !== 'string') continue - lookup.get(signature).push(name) - } - } + // Actual static variants + for (let [root, variant] of designSystem.variants.entries()) { + if (variant.kind === 'static') { + let signature = signatures.get(root) + if (typeof signature !== 'string') continue + lookup.get(signature).push(root) } + } - return lookup - }) + return lookup } function temporarilyDisableThemeInline(designSystem: DesignSystem, cb: () => T): T { diff --git a/packages/tailwindcss/src/cartesian.ts b/packages/tailwindcss/src/cartesian.ts deleted file mode 100644 index f0f7bbba4..000000000 --- a/packages/tailwindcss/src/cartesian.ts +++ /dev/null @@ -1,45 +0,0 @@ -type CartesianInput = readonly unknown[][] - -type CartesianResult = T extends [ - infer Head extends unknown[], - ...infer Tail extends CartesianInput, -] - ? [Head[number], ...CartesianResult] - : [] - -export function* cartesian(...sets: T): Generator> { - let n = sets.length - if (n === 0) return - - // If any input set is empty, the Cartesian product is empty. - if (sets.some((set) => set.length === 0)) { - return - } - - // Index lookup - let idx = Array(n).fill(0) - - while (true) { - // Compute current combination - let result = [] as CartesianResult - for (let i = 0; i < n; i++) { - result[i] = sets[i][idx[i]] - } - yield result - - // Update index vector - let k = n - 1 - while (k >= 0) { - idx[k]++ - if (idx[k] < sets[k].length) { - break - } - idx[k] = 0 - k-- - } - - if (k < 0) { - return - } - } -} diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 036b9d88e..6a9f580b6 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -309,19 +309,6 @@ function upgradeToFullPluginSupport({ let resolvedValue = sharedPluginApi.theme(path, undefined) - // If we're dealing with an object that has the `DEFAULT` key, unwrap it to - // the default value first. This happens for namespace root lookups where - // the CSS theme defines a bare value, e.g. `--shadow` resolved via - // `theme('shadow')`. - if ( - typeof resolvedValue === 'object' && - resolvedValue !== null && - !Array.isArray(resolvedValue) && - 'DEFAULT' in resolvedValue - ) { - resolvedValue = resolvedValue.DEFAULT - } - // When a tuple is returned, return the first element if (Array.isArray(resolvedValue) && resolvedValue.length === 2) { return resolvedValue[0] @@ -332,6 +319,16 @@ function upgradeToFullPluginSupport({ return resolvedValue.join(', ') } + // If we're dealing with an object that has the `DEFAULT` key, return the + // default value + else if ( + typeof resolvedValue === 'object' && + resolvedValue !== null && + 'DEFAULT' in resolvedValue + ) { + return resolvedValue.DEFAULT + } + // Otherwise only allow string values here, objects (and namespace maps) // are treated as non-resolved values for the CSS `theme()` function. else if (typeof resolvedValue === 'string') { diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts index 12f195e94..3fe171dfc 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -1,7 +1,11 @@ import { describe, expect, test } from 'vitest' import { buildDesignSystem } from '../design-system' import { Theme, ThemeOptions } from '../theme' -import { applyConfigToTheme, keyPathToCssProperty } from './apply-config-to-theme' +import { + applyConfigToTheme, + keyPathToCssProperty, + keyPathsToCssProperty, +} from './apply-config-to-theme' import { resolveConfig } from './config/resolve-config' test('config values can be merged into the theme', () => { @@ -31,16 +35,6 @@ test('config values can be merged into the theme', () => { normal: '0 1px 3px black', }, - borderWidth: { - DEFAULT: '1.5px', - }, - outlineWidth: { - DEFAULT: '2.5px', - }, - ringWidth: { - DEFAULT: '3.5px', - }, - borderRadius: { sm: '0.33rem', }, @@ -67,10 +61,6 @@ test('config values can be merged into the theme', () => { '2xl': ['2rem'], }, - ringColor: { - DEFAULT: '#fff', - }, - letterSpacing: { superWide: '0.25em', }, @@ -91,12 +81,8 @@ test('config values can be merged into the theme', () => { }, transitionTimingFunction: { - DEFAULT: 'ease-in-out', fast: 'cubic-bezier(0, 0.55, 0.45, 1)', }, - transitionDuration: { - DEFAULT: '1234ms', - }, }, }, base: '/root', @@ -136,30 +122,13 @@ test('config values can be merged into the theme', () => { ]) expect(theme.resolve('2xl', ['--text'])).toEqual('2rem') expect(theme.resolveWith('2xl', ['--text'], ['--line-height'])).toEqual(['2rem', {}]) - expect(theme.resolve('superWide', ['--tracking'])).toEqual('0.25em') - expect(theme.resolve('superLoose', ['--leading'])).toEqual('3') + expect(theme.resolve('super-wide', ['--tracking'])).toEqual('0.25em') + expect(theme.resolve('super-loose', ['--leading'])).toEqual('3') expect(theme.resolve('1/2', ['--width'])).toEqual('60%') expect(theme.resolve('0.5', ['--width'])).toEqual('60%') expect(theme.resolve('100%', ['--width'])).toEqual('100%') expect(theme.resolve('9xs', ['--container'])).toEqual('6rem') expect(theme.resolve('fast', ['--ease'])).toEqual('cubic-bezier(0, 0.55, 0.45, 1)') - - expect(theme.get(['--border'])).toEqual(null) - expect(theme.get(['--default-border-width'])).toEqual('1.5px') - - expect(theme.get(['--outline'])).toEqual(null) - expect(theme.get(['--default-outline-width'])).toEqual('2.5px') - - expect(theme.get(['--ring-color'])).toEqual(null) - expect(theme.get(['--default-ring-color'])).toEqual('#fff') - - expect(theme.get(['--ring-width'])).toEqual(null) - expect(theme.get(['--default-ring-width'])).toEqual('3.5px') - - expect(theme.get(['--default-transition-duration'])).toEqual('1234ms') - - expect(theme.get(['--ease'])).toEqual(null) - expect(theme.get(['--default-transition-timing-function'])).toEqual('ease-in-out') }) test('will reset default theme values with overwriting theme values', () => { @@ -248,6 +217,48 @@ describe('keyPathToCssProperty', () => { }) }) +describe('keyPathsToCssProperty', () => { + test.each([ + // No "1" entries - should return single result + [['width', '40', '2/5'], ['width-40-2/5']], + [['spacing', '0.5'], ['spacing-0_5']], + + // Single "1" entry before the end - should return two variants + [ + ['fontSize', 'xs', '1', 'lineHeight'], + ['text-xs--line-height', 'text-xs-1-line-height'], + ], + + // Multiple "1" entries before the end - should only split the last "1" + [ + ['test', '1', 'middle', '1', 'end'], + ['test-1-middle--end', 'test-1-middle-1-end'], + ], + + // A "1" at the end means everything should be kept as-is + [['spacing', '1'], ['spacing-1']], + + // Even when there are other 1s in the path + [['test', '1', 'middle', '1'], ['test-1-middle-1']], + + [['colors', 'a', '1', 'DEFAULT'], ['color-a-1']], + [ + ['colors', 'a', '1', 'foo'], + ['color-a--foo', 'color-a-1-foo'], + ], + ])('converts %s to %s', (keyPath, expected) => { + expect(keyPathsToCssProperty(keyPath)).toEqual(expected) + }) + + test('returns empty array for container path', () => { + expect(keyPathsToCssProperty(['container', 'sm'])).toEqual([]) + }) + + test('returns empty array for invalid keys', () => { + expect(keyPathsToCssProperty(['test', 'invalid@key'])).toEqual([]) + }) +}) + test('converts opacity modifiers from decimal to percentage values', () => { let theme = new Theme() let design = buildDesignSystem(theme) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index 8757520ee..60f42c3bd 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -25,10 +25,9 @@ export function applyConfigToTheme( replacedThemeKeys: Set, ) { for (let replacedThemeKey of replacedThemeKeys) { - let name = keyPathToCssProperty([replacedThemeKey]) - if (!name) continue - - designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT) + for (let name of keyPathsToCssProperty([replacedThemeKey])) { + designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT) + } } for (let [path, value] of themeableValues(theme)) { @@ -50,14 +49,13 @@ export function applyConfigToTheme( } } - let name = keyPathToCssProperty(path) - if (!name) continue - - designSystem.theme.add( - `--${name}`, - '' + value, - ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT, - ) + for (let name of keyPathsToCssProperty(path)) { + designSystem.theme.add( + `--${name}`, + '' + value, + ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT, + ) + } } // If someone has updated `fontFamily.sans` or `fontFamily.mono` in a JS @@ -147,96 +145,70 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk return toAdd } -const SPECIAL_DEFAULT_KEYS: Record = { - borderWidth: 'border-width', - outlineWidth: 'outline-width', - ringColor: 'ring-color', - ringWidth: 'ring-width', - transitionDuration: 'transition-duration', - transitionTimingFunction: 'transition-timing-function', -} - -const OLD_TO_NEW_NAMESPACE: Record = { - animation: 'animate', - aspectRatio: 'aspect', - borderRadius: 'radius', - boxShadow: 'shadow', - colors: 'color', - containers: 'container', - fontFamily: 'font', - fontSize: 'text', - letterSpacing: 'tracking', - lineHeight: 'leading', - maxWidth: 'container', - screens: 'breakpoint', - transitionTimingFunction: 'ease', -} - -const IS_VALID_KEY = /^[a-zA-Z0-9-_%/.]+$/ +const IS_VALID_KEY = /^[a-zA-Z0-9-_%/\.]+$/ export function keyPathToCssProperty(path: string[]) { - // In some special cases the `DEFAULT` key did not map to a "default" utility - // e.g. `ringColor.DEFAULT` wasn't *just* used for `ring`. It was used for - // all ring utilities as the color when one wasn't specified. - // - // We place these specialty values under the `--default-*` namespace to signal - // that they are defaults used by (potentially) multiple utilities. - let specialDefault = SPECIAL_DEFAULT_KEYS[path[0]] - if (specialDefault && path[1] === 'DEFAULT') return `default-${specialDefault}` + return keyPathsToCssProperty(path)[0] ?? null +} +export function keyPathsToCssProperty(path: string[]): string[] { // The legacy container component config should not be included in the Theme - if (path[0] === 'container') return null + if (path[0] === 'container') return [] + + path = path.slice() + + if (path[0] === 'animation') path[0] = 'animate' + if (path[0] === 'aspectRatio') path[0] = 'aspect' + if (path[0] === 'borderRadius') path[0] = 'radius' + if (path[0] === 'boxShadow') path[0] = 'shadow' + if (path[0] === 'colors') path[0] = 'color' + if (path[0] === 'containers') path[0] = 'container' + if (path[0] === 'fontFamily') path[0] = 'font' + if (path[0] === 'fontSize') path[0] = 'text' + if (path[0] === 'letterSpacing') path[0] = 'tracking' + if (path[0] === 'lineHeight') path[0] = 'leading' + if (path[0] === 'maxWidth') path[0] = 'container' + if (path[0] === 'screens') path[0] = 'breakpoint' + if (path[0] === 'transitionTimingFunction') path[0] = 'ease' for (let part of path) { - if (!IS_VALID_KEY.test(part)) return null + if (!IS_VALID_KEY.test(part)) return [] } - // Map old v3 namespaces to new theme namespaces - let ns = OLD_TO_NEW_NAMESPACE[path[0]] - if (ns) { - path = path.slice() - path[0] = ns + // Find the position of the last `1` as long as it's not at the end + let lastOnePosition = path.lastIndexOf('1') + if (lastOnePosition === path.length - 1) lastOnePosition = -1 + + // Generate two combinations based on tuple access: + let paths: string[][] = [] + + // Option 1: Replace the last "1" with empty string if it exists + // + // We place this first as we "prefer" treating this as a tuple access. The exception to this is if + // the keypath ends in `DEFAULT` otherwise we'd see a key that ends in a dash like `--color-a-` + if (lastOnePosition !== -1 && path.at(-1) !== 'DEFAULT') { + let modified = path.slice() + modified[lastOnePosition] = '' + paths.push(modified) } - return ( - path - // [1] should move into the nested object tuple. To create the CSS variable - // name for this, we replace it with an empty string that will result in two - // subsequent dashes when joined. - // - // E.g.: - // - `fontSize.xs.1.lineHeight` -> `font-size-xs--line-height` - // - `spacing.1` -> `--spacing-1` - .map((path, idx, all) => (path === '1' && idx !== all.length - 1 ? '' : path)) + // Option 2: The path as is + paths.push(path) - // Resolve the key path to a CSS variable segment - .map((part, idx) => { - part = part.replaceAll('.', '_') + return paths.map((path) => { + // Remove the `DEFAULT` key at the end of a path + // We're reading from CSS anyway so it'll be a string + if (path.at(-1) === 'DEFAULT') path = path.slice(0, -1) - let shouldConvert = - // The first "namespace" part should be converted to kebab-case - // This converts things like backgroundColor to `background-color` - idx === 0 || - // Any tuple nested key should be converted to kebab-case - // These are identified with a leading `-` - // e.g. `fontSize.xs.1.lineHeight` -> `font-size-xs--line-height` - part.startsWith('-') || - // `lineHeight` is a bit of a special case in which it does not - // always begin with a leading `-` even when as a nested tuple key - part === 'lineHeight' - - if (shouldConvert) { - part = part.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`) - } - - return part - }) - - // Remove the `DEFAULT` key at the end of a path - // We're reading from CSS anyway so it'll be a string - .filter((part, index) => part !== 'DEFAULT' || index !== path.length - 1) + // Resolve the key path to a CSS variable segment + return path + .map((part) => + part + .replaceAll('.', '_') + .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`), + ) .join('-') - ) + }) } function isValidThemePrimitive(value: unknown) { diff --git a/packages/tailwindcss/src/compat/colors.ts b/packages/tailwindcss/src/compat/colors.ts index 64e3bc9c7..2f443b4b6 100644 --- a/packages/tailwindcss/src/compat/colors.ts +++ b/packages/tailwindcss/src/compat/colors.ts @@ -31,7 +31,7 @@ export default { '950': 'oklch(13% 0.028 261.692)', }, zinc: { - '50': 'oklch(98.5% 0 none)', + '50': 'oklch(98.5% 0 0)', '100': 'oklch(96.7% 0.001 286.375)', '200': 'oklch(92% 0.004 286.32)', '300': 'oklch(87.1% 0.006 286.286)', @@ -44,17 +44,17 @@ export default { '950': 'oklch(14.1% 0.005 285.823)', }, neutral: { - '50': 'oklch(98.5% 0 none)', - '100': 'oklch(97% 0 none)', - '200': 'oklch(92.2% 0 none)', - '300': 'oklch(87% 0 none)', - '400': 'oklch(70.8% 0 none)', - '500': 'oklch(55.6% 0 none)', - '600': 'oklch(43.9% 0 none)', - '700': 'oklch(37.1% 0 none)', - '800': 'oklch(26.9% 0 none)', - '900': 'oklch(20.5% 0 none)', - '950': 'oklch(14.5% 0 none)', + '50': 'oklch(98.5% 0 0)', + '100': 'oklch(97% 0 0)', + '200': 'oklch(92.2% 0 0)', + '300': 'oklch(87% 0 0)', + '400': 'oklch(70.8% 0 0)', + '500': 'oklch(55.6% 0 0)', + '600': 'oklch(43.9% 0 0)', + '700': 'oklch(37.1% 0 0)', + '800': 'oklch(26.9% 0 0)', + '900': 'oklch(20.5% 0 0)', + '950': 'oklch(14.5% 0 0)', }, stone: { '50': 'oklch(98.5% 0.001 106.423)', @@ -69,58 +69,6 @@ export default { '900': 'oklch(21.6% 0.006 56.043)', '950': 'oklch(14.7% 0.004 49.25)', }, - mauve: { - '50': 'oklch(98.5% 0 none)', - '100': 'oklch(96% 0.003 325.6)', - '200': 'oklch(92.2% 0.005 325.62)', - '300': 'oklch(86.5% 0.012 325.68)', - '400': 'oklch(71.1% 0.019 323.02)', - '500': 'oklch(54.2% 0.034 322.5)', - '600': 'oklch(43.5% 0.029 321.78)', - '700': 'oklch(36.4% 0.029 323.89)', - '800': 'oklch(26.3% 0.024 320.12)', - '900': 'oklch(21.2% 0.019 322.12)', - '950': 'oklch(14.5% 0.008 326)', - }, - olive: { - '50': 'oklch(98.8% 0.003 106.5)', - '100': 'oklch(96.6% 0.005 106.5)', - '200': 'oklch(93% 0.007 106.5)', - '300': 'oklch(88% 0.011 106.6)', - '400': 'oklch(73.7% 0.021 106.9)', - '500': 'oklch(58% 0.031 107.3)', - '600': 'oklch(46.6% 0.025 107.3)', - '700': 'oklch(39.4% 0.023 107.4)', - '800': 'oklch(28.6% 0.016 107.4)', - '900': 'oklch(22.8% 0.013 107.4)', - '950': 'oklch(15.3% 0.006 107.1)', - }, - mist: { - '50': 'oklch(98.7% 0.002 197.1)', - '100': 'oklch(96.3% 0.002 197.1)', - '200': 'oklch(92.5% 0.005 214.3)', - '300': 'oklch(87.2% 0.007 219.6)', - '400': 'oklch(72.3% 0.014 214.4)', - '500': 'oklch(56% 0.021 213.5)', - '600': 'oklch(45% 0.017 213.2)', - '700': 'oklch(37.8% 0.015 216)', - '800': 'oklch(27.5% 0.011 216.9)', - '900': 'oklch(21.8% 0.008 223.9)', - '950': 'oklch(14.8% 0.004 228.8)', - }, - taupe: { - '50': 'oklch(98.6% 0.002 67.8)', - '100': 'oklch(96% 0.002 17.2)', - '200': 'oklch(92.2% 0.005 34.3)', - '300': 'oklch(86.8% 0.007 39.5)', - '400': 'oklch(71.4% 0.014 41.2)', - '500': 'oklch(54.7% 0.021 43.1)', - '600': 'oklch(43.8% 0.017 39.3)', - '700': 'oklch(36.7% 0.016 35.7)', - '800': 'oklch(26.8% 0.011 36.5)', - '900': 'oklch(21.4% 0.009 43.1)', - '950': 'oklch(14.7% 0.004 49.3)', - }, red: { '50': 'oklch(97.1% 0.013 17.38)', '100': 'oklch(93.6% 0.032 17.717)', diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 1f69ae446..94cfd1ad6 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test, vi } from 'vitest' import { compile, type Config } from '..' import { default as plugin } from '../plugin' -import { compileCss, run } from '../test-utils/run' import flattenColorPalette from './flatten-color-palette' const css = String.raw @@ -20,21 +19,18 @@ test('Config files can add content', async () => { }) test('Config files can change dark mode (media)', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root', path: '' }), - }, - ), - ).toMatchInlineSnapshot(` - " - @media (prefers-color-scheme: dark) { - .dark\\:underline { + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root', path: '' }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + @media (prefers-color-scheme: dark) { text-decoration-line: underline; } } @@ -43,80 +39,75 @@ test('Config files can change dark mode (media)', async () => { }) test('Config files can change dark mode (selector)', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root', path: '' }), - }, - ), - ).toMatchInlineSnapshot(` - " - .dark\\:underline:where(.dark, .dark *) { - text-decoration-line: underline; + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root', path: '' }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + &:where(.dark, .dark *) { + text-decoration-line: underline; + } } " `) }) test('Config files can change dark mode (variant)', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { darkMode: ['variant', '&:where(:not(.light))'] }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .dark\\:underline:where(:not(.light)) { - text-decoration-line: underline; + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { darkMode: ['variant', '&:where(:not(.light))'] }, + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + &:where(:not(.light)) { + text-decoration-line: underline; + } } " `) }) test('Config files can add plugins', async () => { - expect( - await run( - ['no-scrollbar'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - plugins: [ - plugin(function ({ addUtilities }) { - addUtilities({ - '.no-scrollbar': { - 'scrollbar-width': 'none', - }, - }) - }), - ], - }, - base: '/root', - path: '', - }), + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + plugins: [ + plugin(function ({ addUtilities }) { + addUtilities({ + '.no-scrollbar': { + 'scrollbar-width': 'none', + }, + }) + }), + ], }, - ), - ).toMatchInlineSnapshot(` - " - .no-scrollbar { + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['no-scrollbar'])).toMatchInlineSnapshot(` + ".no-scrollbar { scrollbar-width: none; } " @@ -124,145 +115,143 @@ test('Config files can add plugins', async () => { }) test('Plugins loaded from config files can contribute to the config', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - plugins: [ - plugin(() => {}, { - darkMode: ['variant', '&:where(:not(.light))'], - }), - ], - }, - base: '/root', - path: '', - }), + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + plugins: [ + plugin(() => {}, { + darkMode: ['variant', '&:where(:not(.light))'], + }), + ], }, - ), - ).toMatchInlineSnapshot(` - " - .dark\\:underline:where(:not(.light)) { - text-decoration-line: underline; + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + &:where(:not(.light)) { + text-decoration-line: underline; + } } " `) }) test('Config file presets can contribute to the config', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { presets: [{ darkMode: ['variant', '&:where(:not(.light))'] }] }, - base: '/root', - path: '', - }), + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + presets: [ + { + darkMode: ['variant', '&:where(:not(.light))'], + }, + ], }, - ), - ).toMatchInlineSnapshot(` - " - .dark\\:underline:where(:not(.light)) { - text-decoration-line: underline; + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + &:where(:not(.light)) { + text-decoration-line: underline; + } } " `) }) test('Config files can affect the theme', async () => { - expect( - await run( - ['bg-primary', 'scrollbar-primary'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - colors: { - primary: '#c0ffee', - }, - }, - }, + let input = css` + @tailwind utilities; + @config "./config.js"; + ` - plugins: [ - plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.scrollbar-primary': { - scrollbarColor: theme('colors.primary'), - }, - }) - }), - ], + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + primary: '#c0ffee', + }, }, - base: '/root', - path: '', - }), + }, + + plugins: [ + plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.scrollbar-primary': { + scrollbarColor: theme('colors.primary'), + }, + }) + }), + ], }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['bg-primary', 'scrollbar-primary'])).toMatchInlineSnapshot(` + ".bg-primary { + background-color: #c0ffee; + } .scrollbar-primary { scrollbar-color: #c0ffee; } - - .bg-primary { - background-color: #c0ffee; - } " `) }) // https://github.com/tailwindlabs/tailwindcss/issues/19091 test('Accessing a default color if a sub-color exists via CSS should work as expected', async () => { - expect( - await compileCss( - css` - @tailwind utilities; - @config "./config.js"; + let input = css` + @tailwind utilities; + @config "./config.js"; - .example { - color: theme('colors.foo-bar'); - border-color: theme('colors.foo'); - } - `, - { - loadModule: async () => ({ - module: { - theme: { - // Internally this object gets converted to something like: - // ``` - // { - // foo: { DEFAULT: 'var(--foo-foo)', bar: 'var(--foo-foo-bar)' }, - // } - // ``` - colors: { - foo: 'var(--foo-foo)', - 'foo-bar': 'var(--foo-foo-bar)', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " .example { + color: theme('colors.foo-bar'); + border-color: theme('colors.foo'); + } + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + // Internally this object gets converted to something like: + // ``` + // { + // foo: { DEFAULT: 'var(--foo-foo)', bar: 'var(--foo-foo-bar)' }, + // } + // ``` + colors: { + foo: 'var(--foo-foo)', + 'foo-bar': 'var(--foo-foo-bar)', + }, + }, + }, + base: '/root', + path: '', + }), + }) + + expect(compiler.build([])).toMatchInlineSnapshot(` + ".example { color: var(--foo-foo-bar); border-color: var(--foo-foo); } @@ -271,34 +260,106 @@ test('Accessing a default color if a sub-color exists via CSS should work as exp }) test('Variants in CSS overwrite variants from plugins', async () => { - expect( - await run( - ['dark:underline', 'light:underline'], - css` - @tailwind utilities; - @config "./config.js"; - @custom-variant dark (&:is(.my-dark)); - @custom-variant light (&:is(.my-light)); - `, - { - loadModule: async () => ({ - module: { - darkMode: ['variant', '&:is(.dark)'], - plugins: [ - plugin(function ({ addVariant }) { - addVariant('light', '&:is(.light)') - }), - ], - }, - base: '/root', - path: '', - }), + let input = css` + @tailwind utilities; + @config "./config.js"; + @custom-variant dark (&:is(.my-dark)); + @custom-variant light (&:is(.my-light)); + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + darkMode: ['variant', '&:is(.dark)'], + plugins: [ + plugin(function ({ addVariant }) { + addVariant('light', '&:is(.light)') + }), + ], }, - ), - ).toMatchInlineSnapshot(` + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['dark:underline', 'light:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + &:is(.my-dark) { + text-decoration-line: underline; + } + } + .light\\:underline { + &:is(.my-light) { + text-decoration-line: underline; + } + } " - .dark\\:underline.my-dark, .light\\:underline.my-light { - text-decoration-line: underline; + `) +}) + +test('A `1` key is treated like a nested theme key *and* a normal theme key', async () => { + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontSize: { + xs: ['0.5rem', { lineHeight: '0.25rem' }], + }, + colors: { + a: { + 1: { DEFAULT: '#ffffff', hovered: '#000000' }, + 2: { DEFAULT: '#000000', hovered: '#c0ffee' }, + }, + b: { + 1: '#ffffff', + 2: '#000000', + }, + }, + }, + }, + base: '/root', + path: '', + }), + }) + + expect( + compiler.build([ + 'text-xs', + + 'text-a-1', + 'text-a-2', + 'text-a-1-hovered', + 'text-a-2-hovered', + 'text-b-1', + 'text-b-2', + ]), + ).toMatchInlineSnapshot(` + ".text-xs { + font-size: 0.5rem; + line-height: var(--tw-leading, 0.25rem); + } + .text-a-1 { + color: #ffffff; + } + .text-a-1-hovered { + color: #000000; + } + .text-a-2 { + color: #000000; + } + .text-a-2-hovered { + color: #c0ffee; + } + .text-b-1 { + color: #ffffff; + } + .text-b-2 { + color: #000000; } " `) @@ -308,167 +369,156 @@ describe('theme callbacks', () => { test('tuple values from the config overwrite `@theme default` tuple-ish values from the CSS theme', async ({ expect, }) => { - expect( - await run( - ['leading-base', 'leading-md', 'leading-xl', 'prose'], - css` - @theme default { - --text-base: 0rem; - --text-base--line-height: 1rem; - --text-md: 0rem; - --text-md--line-height: 1rem; - --text-xl: 0rem; - --text-xl--line-height: 1rem; - } - @theme { - --text-base: 100rem; - --text-md--line-height: 101rem; - } - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - fontSize: { - base: ['200rem', { lineHeight: '201rem' }], - md: ['200rem', { lineHeight: '201rem' }], - xl: ['200rem', { lineHeight: '201rem' }], - }, + let input = css` + @theme default { + --text-base: 0rem; + --text-base--line-height: 1rem; + --text-md: 0rem; + --text-md--line-height: 1rem; + --text-xl: 0rem; + --text-xl--line-height: 1rem; + } + @theme { + --text-base: 100rem; + --text-md--line-height: 101rem; + } + @tailwind utilities; + @config "./config.js"; + ` - // Direct access - lineHeight: ({ theme }) => ({ - base: theme('fontSize.base[1].lineHeight'), - md: theme('fontSize.md[1].lineHeight'), - xl: theme('fontSize.xl[1].lineHeight'), - }), - - // Tuple access - typography: ({ theme }) => ({ - '[class~=lead-base]': { - fontSize: theme('fontSize.base')[0], - ...theme('fontSize.base')[1], - }, - '[class~=lead-md]': { - fontSize: theme('fontSize.md')[0], - ...theme('fontSize.md')[1], - }, - '[class~=lead-xl]': { - fontSize: theme('fontSize.xl')[0], - ...theme('fontSize.xl')[1], - }, - }), - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + fontSize: { + base: ['200rem', { lineHeight: '201rem' }], + md: ['200rem', { lineHeight: '201rem' }], + xl: ['200rem', { lineHeight: '201rem' }], }, - plugins: [ - plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.prose': { - ...theme('typography'), - }, - }) - }), - ], - } satisfies Config, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @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-leading: initial; + // Direct access + lineHeight: ({ theme }) => ({ + base: theme('fontSize.base[1].lineHeight'), + md: theme('fontSize.md[1].lineHeight'), + xl: theme('fontSize.xl[1].lineHeight'), + }), + + // Tuple access + typography: ({ theme }) => ({ + '[class~=lead-base]': { + fontSize: theme('fontSize.base')[0], + ...theme('fontSize.base')[1], + }, + '[class~=lead-md]': { + fontSize: theme('fontSize.md')[0], + ...theme('fontSize.md')[1], + }, + '[class~=lead-xl]': { + fontSize: theme('fontSize.xl')[0], + ...theme('fontSize.xl')[1], + }, + }), + }, + }, + + plugins: [ + plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.prose': { + ...theme('typography'), + }, + }) + }), + ], + } satisfies Config, + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['leading-base', 'leading-md', 'leading-xl', 'prose'])) + .toMatchInlineSnapshot(` + "@layer properties; + .prose { + [class~=lead-base] { + font-size: 100rem; + line-height: 201rem; + } + [class~=lead-md] { + font-size: 200rem; + line-height: 101rem; + } + [class~=lead-xl] { + font-size: 200rem; + line-height: 201rem; } } - } - - .prose [class~="lead-base"] { - font-size: 100rem; - line-height: 201rem; - } - - .prose [class~="lead-md"] { - font-size: 200rem; - line-height: 101rem; - } - - .prose [class~="lead-xl"] { - font-size: 200rem; - line-height: 201rem; - } - - .leading-base { - --tw-leading: 201rem; - line-height: 201rem; - } - - .leading-md { - --tw-leading: 101rem; - line-height: 101rem; - } - - .leading-xl { - --tw-leading: 201rem; - line-height: 201rem; - } - - @property --tw-leading { - syntax: "*"; - inherits: false - } - " - `) + .leading-base { + --tw-leading: 201rem; + line-height: 201rem; + } + .leading-md { + --tw-leading: 101rem; + line-height: 101rem; + } + .leading-xl { + --tw-leading: 201rem; + line-height: 201rem; + } + @property --tw-leading { + syntax: "*"; + inherits: false; + } + @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-leading: initial; + } + } + } + " + `) }) }) describe('theme overrides order', () => { test('user theme > js config > default theme', async () => { - expect( - await run( - ['bg-red', 'bg-blue'], - css` - @theme default { - --color-red: red; - } - @theme { - --color-blue: blue; - } - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - colors: { - red: 'very-red', - blue: 'very-blue', - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + let input = css` + @theme default { + --color-red: red; + } + @theme { --color-blue: blue; } + @tailwind utilities; + @config "./config.js"; + ` + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + red: 'very-red', + blue: 'very-blue', + }, + }, + }, + }, + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['bg-red', 'bg-blue'])).toMatchInlineSnapshot(` + ":root, :host { + --color-blue: blue; + } .bg-blue { background-color: var(--color-blue); } - .bg-red { background-color: very-red; } @@ -477,134 +527,133 @@ describe('theme overrides order', () => { }) test('user theme > js config > default theme (with nested object)', async () => { - expect( - await run( - [ - 'bg-slate-100', - 'bg-slate-200', - 'bg-slate-300', - 'bg-slate-400', - 'bg-slate-500', - 'bg-slate-600', - 'hover-bg-slate-100', - 'hover-bg-slate-200', - 'hover-bg-slate-300', - 'hover-bg-slate-400', - 'hover-bg-slate-500', - 'hover-bg-slate-600', - ], - css` - @theme default { - --color-slate-100: #000100; - --color-slate-200: #000200; - --color-slate-300: #000300; - } - @theme { - --color-slate-400: #100400; - --color-slate-500: #100500; - } - @tailwind utilities; - @config "./config.js"; - @plugin "./plugin.js"; - `, - { - loadModule: async (id) => { - if (id.includes('config.js')) { - return { - module: { - theme: { - extend: { - colors: { - slate: { - 200: '#200200', - 400: '#200400', - 600: '#200600', - }, - }, + let input = css` + @theme default { + --color-slate-100: #000100; + --color-slate-200: #000200; + --color-slate-300: #000300; + } + @theme { + --color-slate-400: #100400; + --color-slate-500: #100500; + } + @tailwind utilities; + @config "./config.js"; + @plugin "./plugin.js"; + ` + + let compiler = await compile(input, { + loadModule: async (id) => { + if (id.includes('config.js')) { + return { + module: { + theme: { + extend: { + colors: { + slate: { + 200: '#200200', + 400: '#200400', + 600: '#200600', }, }, - } satisfies Config, - base: '/root', - path: '', - } - } else { - return { - module: plugin(({ matchUtilities, theme }) => { - matchUtilities( - { - 'hover-bg': (value) => { - return { - '&:hover': { - backgroundColor: value, - }, - } + }, + }, + } satisfies Config, + base: '/root', + path: '', + } + } else { + return { + module: plugin(({ matchUtilities, theme }) => { + matchUtilities( + { + 'hover-bg': (value) => { + return { + '&:hover': { + backgroundColor: value, }, - }, - { values: flattenColorPalette(theme('colors')) }, - ) - }), - base: '/root', - path: '', - } - } - }, - }, - ), + } + }, + }, + { values: flattenColorPalette(theme('colors')) }, + ) + }), + base: '/root', + path: '', + } + } + }, + }) + + expect( + compiler.build([ + 'bg-slate-100', + 'bg-slate-200', + 'bg-slate-300', + 'bg-slate-400', + 'bg-slate-500', + 'bg-slate-600', + 'hover-bg-slate-100', + 'hover-bg-slate-200', + 'hover-bg-slate-300', + 'hover-bg-slate-400', + 'hover-bg-slate-500', + 'hover-bg-slate-600', + ]), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-slate-100: #000100; --color-slate-300: #000300; --color-slate-400: #100400; --color-slate-500: #100500; } - .bg-slate-100 { background-color: var(--color-slate-100); } - .bg-slate-200 { background-color: #200200; } - .bg-slate-300 { background-color: var(--color-slate-300); } - .bg-slate-400 { background-color: var(--color-slate-400); } - .bg-slate-500 { background-color: var(--color-slate-500); } - .bg-slate-600 { background-color: #200600; } - - .hover-bg-slate-100:hover { - background-color: #000100; + .hover-bg-slate-100 { + &:hover { + background-color: #000100; + } } - - .hover-bg-slate-200:hover { - background-color: #200200; + .hover-bg-slate-200 { + &:hover { + background-color: #200200; + } } - - .hover-bg-slate-300:hover { - background-color: #000300; + .hover-bg-slate-300 { + &:hover { + background-color: #000300; + } } - - .hover-bg-slate-400:hover { - background-color: #100400; + .hover-bg-slate-400 { + &:hover { + background-color: #100400; + } } - - .hover-bg-slate-500:hover { - background-color: #100500; + .hover-bg-slate-500 { + &:hover { + background-color: #100500; + } } - - .hover-bg-slate-600:hover { - background-color: #200600; + .hover-bg-slate-600 { + &:hover { + background-color: #200600; + } } " `) @@ -613,35 +662,32 @@ describe('theme overrides order', () => { describe('default font family compatibility', () => { test('overriding `fontFamily.sans` sets `--default-font-family`', async () => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: 'Potato Sans', - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: 'Potato Sans', }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .font-sans { + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ".font-sans { font-family: Potato Sans; } " @@ -651,37 +697,34 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans[1].fontFeatureSettings` sets `--default-font-feature-settings`', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: ['Potato Sans', { fontFeatureSettings: '"cv06"' }], - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Potato Sans', { fontFeatureSettings: '"cv06"' }], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .font-sans { - font-feature-settings: "cv06"; + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ".font-sans { font-family: Potato Sans; + font-feature-settings: "cv06"; } " `) @@ -690,37 +733,34 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans[1].fontVariationSettings` sets `--default-font-variation-settings`', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: ['Potato Sans', { fontVariationSettings: '"XHGT" 0.7' }], - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Potato Sans', { fontVariationSettings: '"XHGT" 0.7' }], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .font-sans { - font-variation-settings: "XHGT" .7; + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ".font-sans { font-family: Potato Sans; + font-variation-settings: "XHGT" 0.7; } " `) @@ -729,41 +769,38 @@ describe('default font family compatibility', () => { test('overriding `fontFeatureSettings` and `fontVariationSettings` for `fontFamily.sans` sets `--default-font-feature-settings` and `--default-font-variation-settings`', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: [ - 'Potato Sans', - { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, - ], - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: [ + 'Potato Sans', + { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, + ], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .font-sans { - font-feature-settings: "cv06"; - font-variation-settings: "XHGT" .7; + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ".font-sans { font-family: Potato Sans; + font-feature-settings: "cv06"; + font-variation-settings: "XHGT" 0.7; } " `) @@ -772,41 +809,37 @@ describe('default font family compatibility', () => { test('overriding `--font-family-sans` in `@theme` without `default` preserves the original `--default-font-*` values', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @theme { - --font-sans: Sandwich Sans; - } - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: 'Potato Sans', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @theme { --font-sans: Sandwich Sans; } + @tailwind utilities; + ` + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: 'Potato Sans', + }, + }, + }, + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root, :host { + --font-sans: Sandwich Sans; + } .font-sans { font-family: var(--font-sans); } @@ -817,35 +850,32 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans` in a config file with an array sets `--default-font-family`', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'], - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .font-sans { + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ".font-sans { font-family: Inter, system-ui, sans-serif; } " @@ -855,67 +885,60 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans` in a config file with an unexpected type is ignored', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: { foo: 'bar', banana: 'sandwich' }, - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: { foo: 'bar', banana: 'sandwich' }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toEqual('') + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(`""`) }) test('overriding `fontFamily.mono` sets `--default-mono-font-family`', async () => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-family-mono); - --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); - --default-mono-font-variation-settings: var( - --font-family-mono--font-variation-settings - ); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: 'Potato Mono', - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: 'Potato Mono', }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .font-mono { + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ".font-mono { font-family: Potato Mono; } " @@ -925,39 +948,34 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.mono[1].fontFeatureSettings` sets `--default-mono-font-feature-settings`', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-family-mono); - --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); - --default-mono-font-variation-settings: var( - --font-family-mono--font-variation-settings - ); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: ['Potato Mono', { fontFeatureSettings: '"cv06"' }], - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: ['Potato Mono', { fontFeatureSettings: '"cv06"' }], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .font-mono { - font-feature-settings: "cv06"; + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ".font-mono { font-family: Potato Mono; + font-feature-settings: "cv06"; } " `) @@ -966,39 +984,34 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.mono[1].fontVariationSettings` sets `--default-mono-font-variation-settings`', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-family-mono); - --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); - --default-mono-font-variation-settings: var( - --font-family-mono--font-variation-settings - ); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: ['Potato Mono', { fontVariationSettings: '"XHGT" 0.7' }], - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: ['Potato Mono', { fontVariationSettings: '"XHGT" 0.7' }], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .font-mono { - font-variation-settings: "XHGT" .7; + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ".font-mono { font-family: Potato Mono; + font-variation-settings: "XHGT" 0.7; } " `) @@ -1007,41 +1020,38 @@ describe('default font family compatibility', () => { test('overriding `fontFeatureSettings` and `fontVariationSettings` for `fontFamily.mono` sets `--default-mono-font-feature-settings` and `--default-mono-font-variation-settings`', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-mono); - --default-mono-font-feature-settings: var(--font-mono--font-feature-settings); - --default-mono-font-variation-settings: var(--font-mono--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: [ - 'Potato Mono', - { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, - ], - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-mono); + --default-mono-font-feature-settings: var(--font-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: [ + 'Potato Mono', + { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, + ], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .font-mono { - font-feature-settings: "cv06"; - font-variation-settings: "XHGT" .7; + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ".font-mono { font-family: Potato Mono; + font-feature-settings: "cv06"; + font-variation-settings: "XHGT" 0.7; } " `) @@ -1050,41 +1060,37 @@ describe('default font family compatibility', () => { test('overriding `--font-family-mono` in `@theme` without `default` preserves the original `--default-mono-font-*` values', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-mono); - --default-mono-font-feature-settings: var(--font-mono--font-feature-settings); - --default-mono-font-variation-settings: var(--font-mono--font-variation-settings); - } - @config "./config.js"; - @theme { - --font-mono: Sandwich Mono; - } - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: 'Potato Mono', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + let input = css` + @theme default { + --default-mono-font-family: var(--font-mono); + --default-mono-font-feature-settings: var(--font-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-mono--font-variation-settings); + } + @config "./config.js"; + @theme { --font-mono: Sandwich Mono; } + @tailwind utilities; + ` + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: 'Potato Mono', + }, + }, + }, + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root, :host { + --font-mono: Sandwich Mono; + } .font-mono { font-family: var(--font-mono); } @@ -1095,117 +1101,117 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.mono` in a config file with an unexpected type is ignored', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-family-mono); - --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); - --default-mono-font-variation-settings: var( - --font-family-mono--font-variation-settings - ); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: { foo: 'bar', banana: 'sandwich' }, - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: { foo: 'bar', banana: 'sandwich' }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toEqual('') + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(`""`) }) }) test('creates variants for `data`, `supports`, and `aria` theme options at the same level as the core utility ', async () => { - expect( - await run( - [ - 'aria-polite:underline', - 'supports-child-combinator:underline', - 'supports-foo:underline', - 'data-checked:underline', + let input = css` + @tailwind utilities; + @config "./config.js"; + ` - // Ensure core variants still work - 'aria-hidden:flex', - 'supports-grid:flex', - 'data-foo:flex', - - // The `print` variant should still be sorted last, even after registering - // the other custom variants. - 'print:flex', - ], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - aria: { - polite: 'live="polite"', - }, - supports: { - 'child-combinator': 'selector(h2 > p)', - foo: 'bar', - }, - data: { - checked: 'ui~="checked"', - }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + aria: { + polite: 'live="polite"', + }, + supports: { + 'child-combinator': 'selector(h2 > p)', + foo: 'bar', + }, + data: { + checked: 'ui~="checked"', }, }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + path: '', + }), + }) + + expect( + compiler.build([ + 'aria-polite:underline', + 'supports-child-combinator:underline', + 'supports-foo:underline', + 'data-checked:underline', + + // Ensure core variants still work + 'aria-hidden:flex', + 'supports-grid:flex', + 'data-foo:flex', + + // The `print` variant should still be sorted last, even after registering + // the other custom variants. + 'print:flex', + ]), ).toMatchInlineSnapshot(` - " - .aria-hidden\\:flex[aria-hidden="true"] { - display: flex; - } - - .aria-polite\\:underline[aria-live="polite"], .data-checked\\:underline[data-ui~="checked"] { - text-decoration-line: underline; - } - - .data-foo\\:flex[data-foo] { - display: flex; - } - - @supports selector(h2 > p) { - .supports-child-combinator\\:underline { - text-decoration-line: underline; - } - } - - @supports (bar: var(--tw)) { - .supports-foo\\:underline { - text-decoration-line: underline; - } - } - - @supports (grid: var(--tw)) { - .supports-grid\\:flex { + ".aria-hidden\\:flex { + &[aria-hidden="true"] { display: flex; } } - - @media print { - .print\\:flex { + .aria-polite\\:underline { + &[aria-live="polite"] { + text-decoration-line: underline; + } + } + .data-checked\\:underline { + &[data-ui~="checked"] { + text-decoration-line: underline; + } + } + .data-foo\\:flex { + &[data-foo] { + display: flex; + } + } + .supports-child-combinator\\:underline { + @supports selector(h2 > p) { + text-decoration-line: underline; + } + } + .supports-foo\\:underline { + @supports (bar: var(--tw)) { + text-decoration-line: underline; + } + } + .supports-grid\\:flex { + @supports (grid: var(--tw)) { + display: flex; + } + } + .print\\:flex { + @media print { display: flex; } } @@ -1214,66 +1220,63 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s }) test('merges css breakpoints with js config screens', async () => { - expect( - await run( - ['sm:flex', 'md:flex', 'lg:flex', 'min-sm:max-md:underline'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @theme { - --breakpoint-md: 50rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: '44rem', - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @theme { + --breakpoint-md: 50rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '44rem', }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - @media (min-width: 44rem) { - .sm\\:flex { - display: flex; - } + base: '/root', + path: '', + }), + }) - @media not all and (min-width: 50rem) { - .min-sm\\:max-md\\:underline { - text-decoration-line: underline; + expect(compiler.build(['sm:flex', 'md:flex', 'lg:flex', 'min-sm:max-md:underline'])) + .toMatchInlineSnapshot(` + ".sm\\:flex { + @media (width >= 44rem) { + display: flex; + } + } + .min-sm\\:max-md\\:underline { + @media (width >= 44rem) { + @media (width < 50rem) { + text-decoration-line: underline; + } } } - } - - @media (min-width: 50rem) { .md\\:flex { - display: flex; + @media (width >= 50rem) { + display: flex; + } } - } - - @media (min-width: 64rem) { .lg\\:flex { - display: flex; + @media (width >= 64rem) { + display: flex; + } } - } - " - `) + " + `) }) test('utilities must be prefixed', async () => { @@ -1286,60 +1289,66 @@ test('utilities must be prefixed', async () => { } ` - let options: Partial[1]> = { - loadModule: async (_id, base) => ({ + let compiler = await compile(input, { + loadModule: async (id, base) => ({ path: '', base, module: { prefix: 'tw' }, }), - } + }) // Prefixed utilities are generated - expect(await run(['tw:underline', 'tw:hover:line-through', 'tw:custom'], input, options)) + expect(compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom'])) .toMatchInlineSnapshot(` - " - .tw\\:custom { + ".tw\\:custom { color: red; } - .tw\\:underline { text-decoration-line: underline; } - - @media (hover: hover) { - .tw\\:hover\\:line-through:hover { - text-decoration-line: line-through; + .tw\\:hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } } } " `) // Non-prefixed utilities are ignored - expect(await run(['underline', 'hover:line-through', 'custom'], input, options)).toEqual('') + compiler = await compile(input, { + loadModule: async (id, base) => ({ + path: '', + base, + module: { prefix: 'tw' }, + }), + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('') }) test('utilities used in @apply must be prefixed', async () => { - // Prefixed utilities are generated - expect( - await compileCss( - css` - @config "./config.js"; + let compiler = await compile( + css` + @config "./config.js"; - .my-underline { - @apply tw:underline; - } - `, - { - loadModule: async (_id, base) => ({ - path: '', - base, - module: { prefix: 'tw' }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .my-underline { + .my-underline { + @apply tw:underline; + } + `, + { + loadModule: async (id, base) => ({ + path: '', + base, + module: { prefix: 'tw' }, + }), + }, + ) + + // Prefixed utilities are generated + expect(compiler.build([])).toMatchInlineSnapshot(` + ".my-underline { text-decoration-line: underline; } " @@ -1357,7 +1366,7 @@ test('utilities used in @apply must be prefixed', async () => { } `, { - loadModule: async (_id, base) => ({ + loadModule: async (id, base) => ({ path: '', base, module: { prefix: 'tw' }, @@ -1370,37 +1379,35 @@ test('utilities used in @apply must be prefixed', async () => { }) test('Prefixes configured in CSS take precedence over those defined in JS configs', async () => { - expect( - await run( - ['wat:custom'], - css` - @theme prefix(wat) { - --color-red: #f00; - --color-green: #0f0; - --breakpoint-sm: 640px; + let compiler = await compile( + css` + @theme prefix(wat) { + --color-red: #f00; + --color-green: #0f0; + --breakpoint-sm: 640px; + } + + @config "./plugin.js"; + + @tailwind utilities; + + @utility custom { + color: red; + } + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: { prefix: 'tw' }, } - - @config "./plugin.js"; - - @tailwind utilities; - - @utility custom { - color: red; - } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: { prefix: 'tw' }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - .wat\\:custom { + }, + ) + + expect(compiler.build(['wat:custom'])).toMatchInlineSnapshot(` + ".wat\\:custom { color: red; } " @@ -1414,7 +1421,7 @@ test('a prefix must be letters only', async () => { @config "./plugin.js"; `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -1429,38 +1436,37 @@ test('a prefix must be letters only', async () => { }) test('important: `#app`', async () => { - expect( - await run( - ['underline', 'hover:line-through', 'custom'], - css` - @tailwind utilities; - @config "./config.js"; + let input = css` + @tailwind utilities; + @config "./config.js"; - @utility custom { - color: red; - } - `, - { - loadModule: async (_id, base) => ({ - path: '', - base, - module: { important: '#app' }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - #app .custom { + @utility custom { color: red; } + ` - #app .underline { - text-decoration-line: underline; - } + let compiler = await compile(input, { + loadModule: async (_, base) => ({ + path: '', + base, + module: { important: '#app' }, + }), + }) - @media (hover: hover) { - #app .hover\\:line-through:hover { - text-decoration-line: line-through; + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` + "#app { + .custom { + color: red; + } + .underline { + text-decoration-line: underline; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } } } " @@ -1468,38 +1474,35 @@ test('important: `#app`', async () => { }) test('important: true', async () => { - expect( - await run( - ['underline', 'hover:line-through', 'custom'], - css` - @tailwind utilities; - @config "./config.js"; + let input = css` + @tailwind utilities; + @config "./config.js"; - @utility custom { - color: red; - } - `, - { - loadModule: async (_id, base) => ({ - path: '', - base, - module: { important: true }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .custom { + @utility custom { + color: red; + } + ` + + let compiler = await compile(input, { + loadModule: async (_, base) => ({ + path: '', + base, + module: { important: true }, + }), + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` + ".custom { color: red !important; } - .underline { text-decoration-line: underline !important; } - - @media (hover: hover) { - .hover\\:line-through:hover { - text-decoration-line: line-through !important; + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through !important; + } } } " @@ -1507,45 +1510,43 @@ test('important: true', async () => { }) test('blocklisted candidates are not generated', async () => { - let input = css` - @theme reference { - --color-white: #fff; - --breakpoint-md: 48rem; - } - @tailwind utilities; - @config "./config.js"; - ` - - let options: Partial[1]> = { - async loadModule(_id, base) { - return { - path: '', - base, - module: { - blocklist: ['bg-white'], - }, + let compiler = await compile( + css` + @theme reference { + --color-white: #fff; + --breakpoint-md: 48rem; } + @tailwind utilities; + @config "./config.js"; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: { + blocklist: ['bg-white'], + }, + } + }, }, - } + ) // bg-white will not get generated - expect(await run(['bg-white'], input, options)).toEqual('') + expect(compiler.build(['bg-white'])).toEqual('') // underline will as will md:bg-white - expect(await run(['underline', 'bg-white', 'md:bg-white'], input, options)) - .toMatchInlineSnapshot(` - " - .underline { - text-decoration-line: underline; + expect(compiler.build(['underline', 'bg-white', 'md:bg-white'])).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline; + } + .md\\:bg-white { + @media (width >= 48rem) { + background-color: var(--color-white, #fff); } - - @media (min-width: 48rem) { - .md\\:bg-white { - background-color: var(--color-white, #fff); - } - } - " - `) + } + " + `) }) test('blocklisted candidates cannot be used with `@apply`', async () => { @@ -1563,7 +1564,7 @@ test('blocklisted candidates cannot be used with `@apply`', async () => { } `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -1580,7 +1581,7 @@ test('blocklisted candidates cannot be used with `@apply`', async () => { }) test('old theme values are merged with their renamed counterparts in the CSS theme', async () => { - using didCallPluginFn = vi.fn() + let didCallPluginFn = vi.fn() await compile( css` @@ -1616,7 +1617,7 @@ test('old theme values are merged with their renamed counterparts in the CSS the @plugin "./plugin.js"; `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -1704,55 +1705,50 @@ test('old theme values are merged with their renamed counterparts in the CSS the }) test('handles setting theme keys to null', async () => { - expect( - await run( - ['bg-red-50', 'bg-red-100', 'bg-red-200'], - css` - @theme default { - --color-red-50: oklch(0.971 0.013 17.38); - --color-red-100: oklch(0.936 0.032 17.717); - } - @config "./my-config.js"; - @tailwind utilities; - @theme { - --color-red-100: oklch(0.936 0.032 17.717); - --color-red-200: oklch(0.885 0.062 18.334); - } - `, - { - loadModule: async () => { - return { - module: { - theme: { - extend: { - colors: { - red: null, - }, + let compiler = await compile( + css` + @theme default { + --color-red-50: oklch(0.971 0.013 17.38); + --color-red-100: oklch(0.936 0.032 17.717); + } + @config "./my-config.js"; + @tailwind utilities; + @theme { + --color-red-100: oklch(0.936 0.032 17.717); + --color-red-200: oklch(0.885 0.062 18.334); + } + `, + { + loadModule: async () => { + return { + module: { + theme: { + extend: { + colors: { + red: null, }, }, }, - base: '/root', - path: '', - } - }, + }, + base: '/root', + path: '', + } }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --color-red-50: oklch(97.1% .013 17.38); - --color-red-100: oklch(93.6% .032 17.717); - --color-red-200: oklch(88.5% .062 18.334); - } + }, + ) + expect(compiler.build(['bg-red-50', 'bg-red-100', 'bg-red-200'])).toMatchInlineSnapshot(` + ":root, :host { + --color-red-50: oklch(0.971 0.013 17.38); + --color-red-100: oklch(0.936 0.032 17.717); + --color-red-200: oklch(0.885 0.062 18.334); + } .bg-red-50 { background-color: var(--color-red-50); } - .bg-red-100 { background-color: var(--color-red-100); } - .bg-red-200 { background-color: var(--color-red-200); } @@ -1761,89 +1757,33 @@ test('handles setting theme keys to null', async () => { }) test('The theme() function does not try indexing into strings', async () => { - expect( - await compileCss(css` - @theme default { - --color-what-50: #f00; - --color-what-950: #f00; - } + let compiler = await compile(css` + @theme default { + --color-what-50: #f00; + --color-what-950: #f00; + } - @theme { - /* + @theme { + /* * The value of this theme variable is > 50 chars because colors.what.50 was previously * indexing the string: "light-dark(theme(colors.what.950), theme(colors.what.50))" * because the resolved config contained an object with a "what" key that was that string */ - --color-what: light-dark(theme(colors.what.950), theme(colors.what.50)); - } - - @source inline("text-what"); - - @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - :root, :host { - --color-what: red; + --color-what: light-dark(theme(colors.what.950), theme(colors.what.50)); } + @source inline("text-what"); + + @tailwind utilities; + `) + + expect(compiler.build([])).toMatchInlineSnapshot(` + ":root, :host { + --color-what: light-dark(#f00, #f00); + } .text-what { color: var(--color-what); } " `) }) - -test('camel case keys are preserved', async () => { - expect( - await run( - [ - // From CSS - 'bg-blue-green', // should be output - 'bg-blueGreen', // should not - - // From JS config - 'bg-light-green', // should not be output - 'bg-lightGreen', // should be - ], - css` - @tailwind utilities; - @theme { - --color-blue-green: slate; - } - @config "./plugin.js"; - `, - { - loadModule: async () => { - return { - base: '/', - path: '', - module: { - theme: { - extend: { - backgroundColor: { - lightGreen: '#c0ffee', - }, - }, - }, - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .bg-blue-green { - background-color: var(--color-blue-green); - } - - .bg-lightGreen { - background-color: #c0ffee; - } - - :root, :host { - --color-blue-green: slate; - } - " - `) -}) diff --git a/packages/tailwindcss/src/compat/config/create-compat-config.test.ts b/packages/tailwindcss/src/compat/config/create-compat-config.test.ts deleted file mode 100644 index aa1b21cb7..000000000 --- a/packages/tailwindcss/src/compat/config/create-compat-config.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { buildDesignSystem } from '../../design-system' -import { Theme } from '../../theme' -import { createCompatConfig } from './create-compat-config' -import { resolveConfig } from './resolve-config' - -function buildCompatConfig(cssValues: Record) { - let theme = new Theme() - for (let [key, value] of Object.entries(cssValues)) { - theme.add(key, value) - } - let design = buildDesignSystem(theme) - - let { resolvedConfig } = resolveConfig(design, [ - { config: createCompatConfig(design.theme), base: '/root', reference: true, src: undefined }, - ]) - - return resolvedConfig -} - -describe('theme namespace lookups', () => { - test('a namespace that only defines a bare value resolves to `{ DEFAULT: … }`, not a string', () => { - // When the CSS theme only defines `--text` (a single bare value), - // `theme('text', {})` used to return the string `'1rem'`. - // - // Spreading that string, like the compat config does, produced char-indexed - // garbage (`{ '0': '1', '1': 'r', … }`) instead of `{ DEFAULT: '1rem' }`. - let config = buildCompatConfig({ '--text': '1rem' }) - - expect(config.theme?.fontSize?.DEFAULT).toBe('1rem') - expect(config.theme?.fontSize).not.toHaveProperty('0') - }) - - test('a bare value is kept alongside suffixed values in the same namespace', () => { - let config = buildCompatConfig({ - '--text': '1rem', - '--text-sm': '0.875rem', - }) - - expect(config.theme?.fontSize?.DEFAULT).toBe('1rem') - expect(config.theme?.fontSize?.sm).toBe('0.875rem') - }) - - test('a naive spread of a theme namespace in a user config is safe', () => { - let theme = new Theme() - theme.add('--text', '1rem') - let design = buildDesignSystem(theme) - - let { resolvedConfig } = resolveConfig(design, [ - { - config: { - theme: { - extend: { - fontSize: ({ theme }: any) => ({ ...theme('text', {}) }), - }, - }, - }, - base: '/root', - reference: true, - src: undefined, - }, - ]) - - expect(resolvedConfig.theme?.fontSize?.DEFAULT).toBe('1rem') - expect(resolvedConfig.theme?.fontSize).not.toHaveProperty('0') - }) - - test('namespace root lookups resolve to an object, key lookups resolve to the value', () => { - let theme = new Theme() - theme.add('--text', '1rem') - theme.add('--animate-spin', 'spin 1s linear infinite') - let design = buildDesignSystem(theme) - - let root: unknown - let rootWithDefault: unknown - let leaf: unknown - - resolveConfig(design, [ - { - config: { - theme: { - extend: { - fontSize: ({ theme }: any) => { - root = theme('text') - rootWithDefault = theme('text', {}) - leaf = theme('animate.spin') - return {} - }, - }, - }, - }, - base: '/root', - reference: true, - src: undefined, - }, - ]) - - // A namespace root lookup always resolves to an object, like in v3, even - // when the namespace only contains a bare value - expect(root).toMatchObject({ DEFAULT: '1rem' }) - expect(rootWithDefault).toMatchObject({ DEFAULT: '1rem' }) - - // Lookups of a specific key resolve to the value itself - expect(leaf).toBe('spin 1s linear infinite') - }) - - test('lookups of a specific key still resolve to the value itself', () => { - let theme = new Theme() - theme.add('--animate-spin', 'spin 1s linear infinite') - let design = buildDesignSystem(theme) - - let { resolvedConfig } = resolveConfig(design, [ - { - config: { - theme: { - extend: { - animation: ({ theme }: any) => ({ spin: theme('animate.spin') }), - }, - }, - }, - base: '/root', - reference: true, - src: undefined, - }, - ]) - - expect(resolvedConfig.theme?.animation?.spin).toBe('spin 1s linear infinite') - }) -}) - -describe('createCompatConfig namespace mappings', () => { - test('fontSize maps from CSS text namespace', () => { - let config = buildCompatConfig({ '--text-base': '1rem' }) - expect(config.theme?.fontSize?.base).toBe('1rem') - }) - - test('boxShadow maps from CSS shadow namespace', () => { - let config = buildCompatConfig({ '--shadow-md': '0 4px 6px #000' }) - expect(config.theme?.boxShadow?.md).toBe('0 4px 6px #000') - }) - - test('animation maps from CSS animate namespace', () => { - let config = buildCompatConfig({ '--animate-spin': 'spin 1s linear infinite' }) - expect(config.theme?.animation?.spin).toBe('spin 1s linear infinite') - }) - - test('aspectRatio maps from CSS aspect namespace', () => { - let config = buildCompatConfig({ '--aspect-square': '1 / 1' }) - expect(config.theme?.aspectRatio?.square).toBe('1 / 1') - }) - - test('borderRadius maps from CSS radius namespace', () => { - let config = buildCompatConfig({ '--radius-lg': '0.5rem' }) - expect(config.theme?.borderRadius?.lg).toBe('0.5rem') - }) - - test('screens maps from CSS breakpoint namespace', () => { - let config = buildCompatConfig({ '--breakpoint-sm': '640px' }) - expect(config.theme?.screens?.sm).toBe('640px') - }) - - test('letterSpacing maps from CSS tracking namespace', () => { - let config = buildCompatConfig({ '--tracking-wide': '0.025em' }) - expect(config.theme?.letterSpacing?.wide).toBe('0.025em') - }) - - test('lineHeight maps from CSS leading namespace', () => { - let config = buildCompatConfig({ '--leading-relaxed': '1.75' }) - expect(config.theme?.lineHeight?.relaxed).toBe('1.75') - }) - - test('maxWidth maps from the container namespace in the default theme', () => { - let config = buildCompatConfig({}) - // The default theme has container.sm = '24rem' - expect(config.theme?.maxWidth?.sm).toBe('24rem') - }) - - test('transitionDuration gets DEFAULT from --default-transition-duration', () => { - let config = buildCompatConfig({ '--default-transition-duration': '150ms' }) - expect(config.theme?.transitionDuration?.DEFAULT).toBe('150ms') - }) - - test('transitionTimingFunction gets DEFAULT from --default-transition-timing-function', () => { - let config = buildCompatConfig({ - '--default-transition-timing-function': 'cubic-bezier(0.4, 0, 0.2, 1)', - }) - expect(config.theme?.transitionTimingFunction?.DEFAULT).toBe('cubic-bezier(0.4, 0, 0.2, 1)') - }) -}) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 858f4bbbe..3fdc1f454 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -33,7 +33,6 @@ interface ResolutionContext { let minimal: ResolvedConfig = { blocklist: [], future: {}, - experimental: {}, prefix: '', important: false, darkMode: null, diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts index 76851a397..f70afa2cb 100644 --- a/packages/tailwindcss/src/compat/config/types.ts +++ b/packages/tailwindcss/src/compat/config/types.ts @@ -105,12 +105,3 @@ export interface UserConfig { export interface ResolvedConfig { future: Record } - -// `experimental` key support -export interface UserConfig { - experimental?: 'all' | Record -} - -export interface ResolvedConfig { - experimental: Record -} diff --git a/packages/tailwindcss/src/compat/container-config.test.ts b/packages/tailwindcss/src/compat/container-config.test.ts index 39a12557c..22841efbf 100644 --- a/packages/tailwindcss/src/compat/container-config.test.ts +++ b/packages/tailwindcss/src/compat/container-config.test.ts @@ -1,74 +1,55 @@ import { expect, test } from 'vitest' -import { run } from '../test-utils/run' +import { compile } from '..' const css = String.raw test('creates a custom utility to extend the built-in container', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - center: true, - padding: '2rem', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; } + @config "./config.js"; + @tailwind utilities; + ` - @media (min-width: 40rem) { - .container { + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + container: { + center: true, + padding: '2rem', + }, + }, + }, + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['container'])).toMatchInlineSnapshot(` + ".container { + width: 100%; + @media (width >= 40rem) { max-width: 40rem; } - } - - @media (min-width: 48rem) { - .container { + @media (width >= 48rem) { max-width: 48rem; } - } - - @media (min-width: 64rem) { - .container { + @media (width >= 64rem) { max-width: 64rem; } - } - - @media (min-width: 80rem) { - .container { + @media (width >= 80rem) { max-width: 80rem; } - } - - @media (min-width: 96rem) { - .container { + @media (width >= 96rem) { max-width: 96rem; } } - .container { margin-inline: auto; padding-inline: 2rem; @@ -78,87 +59,62 @@ test('creates a custom utility to extend the built-in container', async () => { }) test('allows padding to be defined at custom breakpoints', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - padding: { - // The order here is messed up on purpose - '2xl': '3rem', - DEFAULT: '1rem', - lg: '2rem', - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + container: { + padding: { + // The order here is messed up on purpose + '2xl': '3rem', + DEFAULT: '1rem', + lg: '2rem', }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } + base: '/root', + path: '', + }), + }) - @media (min-width: 40rem) { - .container { + expect(compiler.build(['container'])).toMatchInlineSnapshot(` + ".container { + width: 100%; + @media (width >= 40rem) { max-width: 40rem; } - } - - @media (min-width: 48rem) { - .container { + @media (width >= 48rem) { max-width: 48rem; } - } - - @media (min-width: 64rem) { - .container { + @media (width >= 64rem) { max-width: 64rem; } - } - - @media (min-width: 80rem) { - .container { + @media (width >= 80rem) { max-width: 80rem; } - } - - @media (min-width: 96rem) { - .container { + @media (width >= 96rem) { max-width: 96rem; } } - .container { padding-inline: 1rem; - } - - @media (min-width: 64rem) { - .container { + @media (width >= 64rem) { padding-inline: 2rem; } - } - - @media (min-width: 96rem) { - .container { + @media (width >= 96rem) { padding-inline: 3rem; } } @@ -167,87 +123,62 @@ test('allows padding to be defined at custom breakpoints', async () => { }) test('allows breakpoints to be overwritten', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - screens: { - xl: '1280px', - '2xl': '1536px', - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + container: { + screens: { + xl: '1280px', + '2xl': '1536px', }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } + base: '/root', + path: '', + }), + }) - @media (min-width: 40rem) { - .container { + expect(compiler.build(['container'])).toMatchInlineSnapshot(` + ".container { + width: 100%; + @media (width >= 40rem) { max-width: 40rem; } - } - - @media (min-width: 48rem) { - .container { + @media (width >= 48rem) { max-width: 48rem; } - } - - @media (min-width: 64rem) { - .container { + @media (width >= 64rem) { max-width: 64rem; } - } - - @media (min-width: 80rem) { - .container { + @media (width >= 80rem) { max-width: 80rem; } - } - - @media (min-width: 96rem) { - .container { + @media (width >= 96rem) { max-width: 96rem; } } - - @media (min-width: 40rem) { - .container { + .container { + @media (width >= 40rem) { max-width: none; } - } - - @media (min-width: 1280px) { - .container { + @media (width >= 1280px) { max-width: 1280px; } - } - - @media (min-width: 1536px) { - .container { + @media (width >= 1536px) { max-width: 1536px; } } @@ -256,84 +187,62 @@ test('allows breakpoints to be overwritten', async () => { }) test('padding applies to custom `container` screens', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - padding: { - sm: '2rem', - md: '3rem', - }, - screens: { - md: '48rem', - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + container: { + padding: { + sm: '2rem', + md: '3rem', + }, + screens: { + md: '48rem', }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } + base: '/root', + path: '', + }), + }) - @media (min-width: 40rem) { - .container { + expect(compiler.build(['container'])).toMatchInlineSnapshot(` + ".container { + width: 100%; + @media (width >= 40rem) { max-width: 40rem; } - } - - @media (min-width: 48rem) { - .container { + @media (width >= 48rem) { max-width: 48rem; } - } - - @media (min-width: 64rem) { - .container { + @media (width >= 64rem) { max-width: 64rem; } - } - - @media (min-width: 80rem) { - .container { + @media (width >= 80rem) { max-width: 80rem; } - } - - @media (min-width: 96rem) { - .container { + @media (width >= 96rem) { max-width: 96rem; } } - - @media (min-width: 40rem) { - .container { + .container { + @media (width >= 40rem) { max-width: none; } - } - - @media (min-width: 48rem) { - .container { + @media (width >= 48rem) { max-width: 48rem; padding-inline: 3rem; } @@ -343,81 +252,59 @@ test('padding applies to custom `container` screens', async () => { }) test("an empty `screen` config will undo all custom media screens and won't apply any breakpoint-specific padding", async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - padding: { - DEFAULT: '1rem', - sm: '2rem', - md: '3rem', - }, - screens: {}, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; } + @config "./config.js"; + @tailwind utilities; + ` - @media (min-width: 40rem) { - .container { + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + container: { + padding: { + DEFAULT: '1rem', + sm: '2rem', + md: '3rem', + }, + screens: {}, + }, + }, + }, + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['container'])).toMatchInlineSnapshot(` + ".container { + width: 100%; + @media (width >= 40rem) { max-width: 40rem; } - } - - @media (min-width: 48rem) { - .container { + @media (width >= 48rem) { max-width: 48rem; } - } - - @media (min-width: 64rem) { - .container { + @media (width >= 64rem) { max-width: 64rem; } - } - - @media (min-width: 80rem) { - .container { + @media (width >= 80rem) { max-width: 80rem; } - } - - @media (min-width: 96rem) { - .container { + @media (width >= 96rem) { max-width: 96rem; } } - .container { padding-inline: 1rem; - } - - @media (min-width: 40rem) { - .container { + @media (width >= 40rem) { max-width: none; } } @@ -426,50 +313,46 @@ test("an empty `screen` config will undo all custom media screens and won't appl }) test('legacy container component does not interfere with new --container variables', async () => { - expect( - await run( - ['max-w-sm'], - css` - @theme default { - --container-3xs: 16rem; - --container-2xs: 18rem; - --container-xs: 20rem; - --container-sm: 24rem; - --container-md: 28rem; - --container-lg: 32rem; - --container-xl: 36rem; - --container-2xl: 42rem; - --container-3xl: 48rem; - --container-4xl: 56rem; - --container-5xl: 64rem; - --container-6xl: 72rem; - --container-7xl: 80rem; - --container-prose: 65ch; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - center: true, - padding: '2rem', - }, - }, + let input = css` + @theme default { + --container-3xs: 16rem; + --container-2xs: 18rem; + --container-xs: 20rem; + --container-sm: 24rem; + --container-md: 28rem; + --container-lg: 32rem; + --container-xl: 36rem; + --container-2xl: 42rem; + --container-3xl: 48rem; + --container-4xl: 56rem; + --container-5xl: 64rem; + --container-6xl: 72rem; + --container-7xl: 80rem; + --container-prose: 65ch; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + container: { + center: true, + padding: '2rem', }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + base: '/root', + path: '', + }), + }) + + expect(compiler.build(['max-w-sm'])).toMatchInlineSnapshot(` + ":root, :host { --container-sm: 24rem; } - .max-w-sm { max-width: var(--container-sm); } @@ -478,168 +361,108 @@ test('legacy container component does not interfere with new --container variabl }) test('combines custom padding and screen overwrites', async () => { - expect( - await run( - ['container', '!container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - center: true, - padding: { - DEFAULT: '2rem', - '2xl': '4rem', - }, - screens: { - md: '48rem', // Matches a default --breakpoint - xl: '1280px', - '2xl': '1536px', - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + container: { + center: true, + padding: { + DEFAULT: '2rem', + '2xl': '4rem', + }, + screens: { + md: '48rem', // Matches a default --breakpoint + xl: '1280px', + '2xl': '1536px', }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .\\!container { - width: 100% !important; - } + base: '/root', + path: '', + }), + }) - @media (min-width: 40rem) { - .\\!container { + expect(compiler.build(['container', '!container'])).toMatchInlineSnapshot(` + ".\\!container { + width: 100% !important; + @media (width >= 40rem) { max-width: 40rem !important; } - } - - @media (min-width: 48rem) { - .\\!container { + @media (width >= 48rem) { max-width: 48rem !important; } - } - - @media (min-width: 64rem) { - .\\!container { + @media (width >= 64rem) { max-width: 64rem !important; } - } - - @media (min-width: 80rem) { - .\\!container { + @media (width >= 80rem) { max-width: 80rem !important; } - } - - @media (min-width: 96rem) { - .\\!container { + @media (width >= 96rem) { max-width: 96rem !important; } } - .container { width: 100%; - } - - @media (min-width: 40rem) { - .container { + @media (width >= 40rem) { max-width: 40rem; } - } - - @media (min-width: 48rem) { - .container { + @media (width >= 48rem) { max-width: 48rem; } - } - - @media (min-width: 64rem) { - .container { + @media (width >= 64rem) { max-width: 64rem; } - } - - @media (min-width: 80rem) { - .container { + @media (width >= 80rem) { max-width: 80rem; } - } - - @media (min-width: 96rem) { - .container { + @media (width >= 96rem) { max-width: 96rem; } } - .\\!container { margin-inline: auto !important; padding-inline: 2rem !important; - } - - @media (min-width: 40rem) { - .\\!container { + @media (width >= 40rem) { max-width: none !important; } - } - - @media (min-width: 48rem) { - .\\!container { + @media (width >= 48rem) { max-width: 48rem !important; } - } - - @media (min-width: 1280px) { - .\\!container { + @media (width >= 1280px) { max-width: 1280px !important; } - } - - @media (min-width: 1536px) { - .\\!container { + @media (width >= 1536px) { max-width: 1536px !important; padding-inline: 4rem !important; } } - .container { margin-inline: auto; padding-inline: 2rem; - } - - @media (min-width: 40rem) { - .container { + @media (width >= 40rem) { max-width: none; } - } - - @media (min-width: 48rem) { - .container { + @media (width >= 48rem) { max-width: 48rem; } - } - - @media (min-width: 1280px) { - .container { + @media (width >= 1280px) { max-width: 1280px; } - } - - @media (min-width: 1536px) { - .container { + @media (width >= 1536px) { max-width: 1536px; padding-inline: 4rem; } @@ -649,106 +472,75 @@ test('combines custom padding and screen overwrites', async () => { }) test('filters out complex breakpoints', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - center: true, - padding: { - DEFAULT: '2rem', - '2xl': '4rem', - }, - screens: { - sm: '20px', - md: { min: '100px' }, - lg: { max: '200px' }, - xl: { min: '300px', max: '400px' }, - '2xl': { raw: 'print' }, - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + container: { + center: true, + padding: { + DEFAULT: '2rem', + '2xl': '4rem', + }, + screens: { + sm: '20px', + md: { min: '100px' }, + lg: { max: '200px' }, + xl: { min: '300px', max: '400px' }, + '2xl': { raw: 'print' }, }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } + base: '/root', + path: '', + }), + }) - @media (min-width: 40rem) { - .container { + expect(compiler.build(['container'])).toMatchInlineSnapshot(` + ".container { + width: 100%; + @media (width >= 40rem) { max-width: 40rem; } - } - - @media (min-width: 48rem) { - .container { + @media (width >= 48rem) { max-width: 48rem; } - } - - @media (min-width: 64rem) { - .container { + @media (width >= 64rem) { max-width: 64rem; } - } - - @media (min-width: 80rem) { - .container { + @media (width >= 80rem) { max-width: 80rem; } - } - - @media (min-width: 96rem) { - .container { + @media (width >= 96rem) { max-width: 96rem; } } - .container { margin-inline: auto; padding-inline: 2rem; - } - - @media (min-width: 40rem) { - .container { + @media (width >= 40rem) { max-width: none; } - } - - @media (min-width: 20px) { - .container { + @media (width >= 20px) { max-width: 20px; } - } - - @media (min-width: 100px) { - .container { + @media (width >= 100px) { max-width: 100px; } - } - - @media (min-width: 300px) { - .container { + @media (width >= 300px) { max-width: 300px; } } diff --git a/packages/tailwindcss/src/compat/default-theme.ts b/packages/tailwindcss/src/compat/default-theme.ts index 90e699c1c..f540e63a2 100644 --- a/packages/tailwindcss/src/compat/default-theme.ts +++ b/packages/tailwindcss/src/compat/default-theme.ts @@ -353,13 +353,8 @@ export default { }, fontFamily: { sans: [ - '-apple-system', - 'BlinkMacSystemFont', - '"Segoe UI"', - 'Roboto', - '"Helvetica Neue"', - '"Noto Sans"', - 'Arial', + 'ui-sans-serif', + 'system-ui', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', diff --git a/packages/tailwindcss/src/compat/legacy-utilities.test.ts b/packages/tailwindcss/src/compat/legacy-utilities.test.ts index c90eae845..9a828d673 100644 --- a/packages/tailwindcss/src/compat/legacy-utilities.test.ts +++ b/packages/tailwindcss/src/compat/legacy-utilities.test.ts @@ -1,23 +1,27 @@ import { expect, test } from 'vitest' -import { run } from '../test-utils/run' +import { compileCss, run } from '../test-utils/run' const css = String.raw test('bg-gradient-*', async () => { expect( - await run([ - 'bg-gradient-to-t', - 'bg-gradient-to-tr', - 'bg-gradient-to-r', - 'bg-gradient-to-br', - 'bg-gradient-to-b', - 'bg-gradient-to-bl', - 'bg-gradient-to-l', - 'bg-gradient-to-tl', - ]), + await compileCss( + css` + @tailwind utilities; + `, + [ + 'bg-gradient-to-t', + 'bg-gradient-to-tr', + 'bg-gradient-to-r', + 'bg-gradient-to-br', + 'bg-gradient-to-b', + 'bg-gradient-to-bl', + 'bg-gradient-to-l', + 'bg-gradient-to-tl', + ], + ), ).toMatchInlineSnapshot(` - " - .bg-gradient-to-b { + ".bg-gradient-to-b { --tw-gradient-position: to bottom in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } @@ -55,21 +59,13 @@ test('bg-gradient-*', async () => { .bg-gradient-to-tr { --tw-gradient-position: to top right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); - } - " + }" `) }) test('max-w-screen', async () => { expect( - await run( - [ - 'max-w-screen-sm', - 'max-w-screen-md', - 'max-w-screen-lg', - 'max-w-screen-xl', - 'max-w-screen-2xl', - ], + await compileCss( css` @theme { --breakpoint-sm: 40rem; @@ -80,10 +76,16 @@ test('max-w-screen', async () => { } @tailwind utilities; `, + [ + 'max-w-screen-sm', + 'max-w-screen-md', + 'max-w-screen-lg', + 'max-w-screen-xl', + 'max-w-screen-2xl', + ], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --breakpoint-sm: 40rem; --breakpoint-md: 48rem; --breakpoint-lg: 64rem; @@ -109,8 +111,7 @@ test('max-w-screen', async () => { .max-w-screen-xl { max-width: var(--breakpoint-xl); - } - " + }" `) expect( await run([ @@ -133,8 +134,7 @@ test('max-w-screen', async () => { test('box-decoration', async () => { expect(await run(['decoration-slice', 'decoration-clone'])).toMatchInlineSnapshot(` - " - .decoration-clone { + ".decoration-clone { -webkit-box-decoration-break: clone; box-decoration-break: clone; } @@ -142,25 +142,21 @@ test('box-decoration', async () => { .decoration-slice { -webkit-box-decoration-break: slice; box-decoration-break: slice; - } - " + }" `) }) test('overflow-ellipsis', async () => { expect(await run(['overflow-ellipsis'])).toMatchInlineSnapshot(` - " - .overflow-ellipsis { + ".overflow-ellipsis { text-overflow: ellipsis; - } - " + }" `) }) test('flex-grow', async () => { expect(await run(['flex-grow', 'flex-grow-0', 'flex-grow-[123]'])).toMatchInlineSnapshot(` - " - .flex-grow { + ".flex-grow { flex-grow: 1; } @@ -170,8 +166,7 @@ test('flex-grow', async () => { .flex-grow-\\[123\\] { flex-grow: 123; - } - " + }" `) expect( await run([ @@ -190,8 +185,7 @@ test('flex-grow', async () => { test('flex-shrink', async () => { expect(await run(['flex-shrink', 'flex-shrink-0', 'flex-shrink-[123]'])).toMatchInlineSnapshot(` - " - .flex-shrink { + ".flex-shrink { flex-shrink: 1; } @@ -201,8 +195,7 @@ test('flex-shrink', async () => { .flex-shrink-\\[123\\] { flex-shrink: 123; - } - " + }" `) expect( await run([ @@ -218,181 +211,3 @@ test('flex-shrink', async () => { ]), ).toEqual('') }) - -test('start', async () => { - expect( - await run( - [ - 'start-shadowned', - 'start-auto', - '-start-full', - 'start-full', - 'start-3/4', - 'start-4', - '-start-4', - 'start-[4px]', - ], - css` - @theme { - --spacing: 0.25rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing: .25rem; - --inset-shadowned: 1940px; - } - - .-start-4 { - inset-inline-start: calc(var(--spacing) * -4); - } - - .-start-full { - inset-inline-start: -100%; - } - - .start-3\\/4 { - inset-inline-start: 75%; - } - - .start-4 { - inset-inline-start: calc(var(--spacing) * 4); - } - - .start-\\[4px\\] { - inset-inline-start: 4px; - } - - .start-auto { - inset-inline-start: auto; - } - - .start-full { - inset-inline-start: 100%; - } - - .start-shadowned { - inset-inline-start: var(--inset-shadowned); - } - " - `) - expect( - await run( - [ - 'start-shadow-sm', - 'start', - 'start--1', - 'start--1/2', - 'start--1/-2', - 'start-1/-2', - 'start-auto/foo', - '-start-full/foo', - 'start-full/foo', - 'start-3/4/foo', - 'start-4/foo', - '-start-4/foo', - 'start-[4px]/foo', - ], - css` - @theme reference { - --spacing: 0.25rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, - ), - ).toEqual('') -}) - -test('end', async () => { - expect( - await run( - [ - 'end-shadowned', - 'end-auto', - '-end-full', - 'end-full', - 'end-3/4', - 'end-4', - '-end-4', - 'end-[4px]', - ], - css` - @theme { - --spacing: 0.25rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing: .25rem; - --inset-shadowned: 1940px; - } - - .-end-4 { - inset-inline-end: calc(var(--spacing) * -4); - } - - .-end-full { - inset-inline-end: -100%; - } - - .end-3\\/4 { - inset-inline-end: 75%; - } - - .end-4 { - inset-inline-end: calc(var(--spacing) * 4); - } - - .end-\\[4px\\] { - inset-inline-end: 4px; - } - - .end-auto { - inset-inline-end: auto; - } - - .end-full { - inset-inline-end: 100%; - } - - .end-shadowned { - inset-inline-end: var(--inset-shadowned); - } - " - `) - expect( - await run( - [ - 'end-shadow-sm', - 'end', - 'end--1', - 'end--1/2', - 'end--1/-2', - 'end-1/-2', - 'end-auto/foo', - '-end-full/foo', - 'end-full/foo', - 'end-3/4/foo', - 'end-4/foo', - '-end-4/foo', - 'end-[4px]/foo', - ], - css` - @theme reference { - --spacing: 0.25rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, - ), - ).toEqual('') -}) diff --git a/packages/tailwindcss/src/compat/legacy-utilities.ts b/packages/tailwindcss/src/compat/legacy-utilities.ts index 8e7829035..ffefca940 100644 --- a/packages/tailwindcss/src/compat/legacy-utilities.ts +++ b/packages/tailwindcss/src/compat/legacy-utilities.ts @@ -1,7 +1,6 @@ import { decl } from '../ast' import type { DesignSystem } from '../design-system' -import { isPositiveInteger, isValidSpacingMultiplier } from '../utils/infer-data-type' -import { segment } from '../utils/segment' +import { isPositiveInteger } from '../utils/infer-data-type' export function registerLegacyUtilities(designSystem: DesignSystem) { for (let [value, direction] of [ @@ -112,63 +111,4 @@ export function registerLegacyUtilities(designSystem: DesignSystem) { designSystem.utilities.suggest('break-words', () => []) designSystem.utilities.static('break-words', () => [decl('overflow-wrap', 'break-word')]) - - // Legacy `start` and `end` inset utilities, replaced by `inset-s` and `inset-e` - for (let [name, property] of [ - ['start', 'inset-inline-start'], - ['end', 'inset-inline-end'], - ] as const) { - designSystem.utilities.static(`${name}-auto`, () => [decl(property, 'auto')]) - designSystem.utilities.static(`${name}-full`, () => [decl(property, '100%')]) - designSystem.utilities.static(`-${name}-full`, () => [decl(property, '-100%')]) - designSystem.utilities.static(`${name}-px`, () => [decl(property, '1px')]) - designSystem.utilities.static(`-${name}-px`, () => [decl(property, '-1px')]) - - function handleInset({ negative }: { negative: boolean }) { - return (candidate: Extract) => { - if (candidate.value === null) return - - if (candidate.value.kind === 'arbitrary') { - if (candidate.modifier) return - let value = candidate.value.value - return [decl(property, negative ? `calc(${value} * -1)` : value)] - } - - let value = designSystem.theme.resolve(candidate.value.fraction ?? candidate.value.value, [ - '--inset', - '--spacing', - ]) - - // Handle fractions like `start-1/2` - if (value === null && candidate.value.fraction) { - let [lhs, rhs] = segment(candidate.value.fraction, '/') - if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) return - value = `calc(${candidate.value.fraction} * 100%)` - } - - // Handle bare spacing multiplier values like `start-4` - if (value === null && negative) { - let multiplier = designSystem.theme.resolve(null, ['--spacing']) - if (multiplier && isValidSpacingMultiplier(candidate.value.value)) { - value = `calc(${multiplier} * -${candidate.value.value})` - if (value !== null) return [decl(property, value)] - } - } - - if (value === null) { - let multiplier = designSystem.theme.resolve(null, ['--spacing']) - if (multiplier && isValidSpacingMultiplier(candidate.value.value)) { - value = `calc(${multiplier} * ${candidate.value.value})` - } - } - - if (value === null) return - - return [decl(property, negative ? `calc(${value} * -1)` : value)] - } - } - - designSystem.utilities.functional(`-${name}`, handleInset({ negative: true })) - designSystem.utilities.functional(name, handleInset({ negative: false })) - } } diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 5fcacb5cd..4e35afa5a 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test, vi } from 'vitest' import { compile } from '..' import plugin from '../plugin' -import { compileCss, run } from '../test-utils/run' +import { optimizeCss } from '../test-utils/run' import defaultTheme from './default-theme' import type { CssInJs, PluginAPI } from './plugin-api' @@ -9,61 +9,58 @@ const css = String.raw describe('theme', async () => { test('plugin theme can contain objects', async () => { - expect( - await compileCss( - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ addBase, theme }) { - addBase({ - '@keyframes enter': theme('keyframes.enter'), - '@keyframes exit': theme('keyframes.exit'), - }) - }, - { - theme: { - extend: { - keyframes: { - enter: { - from: { - opacity: 'var(--tw-enter-opacity, 1)', - transform: - 'translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))', - }, - }, - exit: { - to: { - opacity: 'var(--tw-exit-opacity, 1)', - transform: - 'translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))', - }, - }, + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ addBase, theme }) { + addBase({ + '@keyframes enter': theme('keyframes.enter'), + '@keyframes exit': theme('keyframes.exit'), + }) + }, + { + theme: { + extend: { + keyframes: { + enter: { + from: { + opacity: 'var(--tw-enter-opacity, 1)', + transform: + 'translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))', + }, + }, + exit: { + to: { + opacity: 'var(--tw-exit-opacity, 1)', + transform: + 'translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))', }, }, }, }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + }, + }, + ), + } + }, + }) + + expect(compiler.build([])).toMatchInlineSnapshot(` + "@layer base { @keyframes enter { from { opacity: var(--tw-enter-opacity, 1); transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0)); } } - @keyframes exit { to { opacity: var(--tw-exit-opacity, 1); @@ -76,29 +73,31 @@ describe('theme', async () => { }) test('keyframes added via addUtilities are appended to the AST', async () => { - expect( - await compileCss( - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ addUtilities }) { - addUtilities({ - '@keyframes enter': { from: { opacity: 'var(--tw-enter-opacity, 1)' } }, - }) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @keyframes enter { + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin(function ({ addUtilities, theme }) { + addUtilities({ + '@keyframes enter': { + from: { + opacity: 'var(--tw-enter-opacity, 1)', + }, + }, + }) + }), + } + }, + }) + + expect(compiler.build([])).toMatchInlineSnapshot(` + "@keyframes enter { from { opacity: var(--tw-enter-opacity, 1); } @@ -108,40 +107,48 @@ describe('theme', async () => { }) test('plugin theme can extend colors', async () => { - expect( - await run( - ['scrollbar-red-500', 'scrollbar-russet-700'], - css` - @theme reference { - --color-red-500: #ef4444; - } - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { values: theme('colors') }, - ) + let input = css` + @theme reference { + --color-red-500: #ef4444; + } + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), }, - { theme: { extend: { colors: { 'russet-700': '#7a4724' } } } }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .scrollbar-red-500 { + { + values: theme('colors'), + }, + ) + }, + { + theme: { + extend: { + colors: { + 'russet-700': '#7a4724', + }, + }, + }, + }, + ), + } + }, + }) + + expect(compiler.build(['scrollbar-red-500', 'scrollbar-russet-700'])).toMatchInlineSnapshot(` + ".scrollbar-red-500 { scrollbar-color: #ef4444; } - .scrollbar-russet-700 { scrollbar-color: #7a4724; } @@ -152,43 +159,46 @@ describe('theme', async () => { test('plugin theme values can reference legacy theme keys that have been replaced with bare value support', async ({ expect, }) => { - expect( - await run( - ['animate-duration-316'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'animate-duration': (value) => ({ 'animation-duration': value }) }, - { values: theme('animationDuration') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-duration': value }), }, { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => { - return { ...theme('transitionDuration') } - }, - }, + values: theme('animationDuration'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => { + return { + ...theme('transitionDuration'), + } }, }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animate-duration-316 { - animation-duration: .316s; + }, + }, + ), + } + }, + }) + + expect(compiler.build(['animate-duration-316'])).toMatchInlineSnapshot(` + ".animate-duration-316 { + animation-duration: 316ms; } " `) @@ -197,92 +207,98 @@ describe('theme', async () => { test('plugin theme values that support bare values are merged with other values for that theme key', async ({ expect, }) => { - expect( - await run( - ['animate-duration-316', 'animate-duration-slow'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'animate-duration': (value) => ({ 'animation-duration': value }) }, - { values: theme('animationDuration') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-duration': value }), }, { - theme: { - extend: { - transitionDuration: { slow: '800ms' }, - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), - }, - }, + values: theme('animationDuration'), }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animate-duration-316 { - animation-duration: .316s; - } + ) + }, + { + theme: { + extend: { + transitionDuration: { + slow: '800ms', + }, - .animate-duration-slow { - animation-duration: .8s; - } - " - `) + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + }, + }, + }, + ), + } + }, + }) + + expect(compiler.build(['animate-duration-316', 'animate-duration-slow'])) + .toMatchInlineSnapshot(` + ".animate-duration-316 { + animation-duration: 316ms; + } + .animate-duration-slow { + animation-duration: 800ms; + } + " + `) }) test('plugin theme can have opacity modifiers', async () => { - expect( - await run( - ['percentage', 'fraction', 'variable'], - css` - @tailwind utilities; - @theme { - --color-red-500: #ef4444; - } - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.percentage': { color: theme('colors.red.500 / 50%') }, - '.fraction': { color: theme('colors.red.500 / 0.5') }, - '.variable': { color: theme('colors.red.500 / var(--opacity)') }, - }) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .fraction, .percentage { - color: oklab(63.6834% .187 .088 / .5); + let input = css` + @tailwind utilities; + @theme { + --color-red-500: #ef4444; } + @plugin "my-plugin"; + ` + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.percentage': { + color: theme('colors.red.500 / 50%'), + }, + '.fraction': { + color: theme('colors.red.500 / 0.5'), + }, + '.variable': { + color: theme('colors.red.500 / var(--opacity)'), + }, + }) + }), + } + }, + }) + + expect(compiler.build(['percentage', 'fraction', 'variable'])).toMatchInlineSnapshot(` + ".fraction { + color: color-mix(in oklab, #ef4444 50%, transparent); + } + .percentage { + color: color-mix(in oklab, #ef4444 50%, transparent); + } .variable { color: #ef4444; - } - - @supports (color: color-mix(in lab, red, red)) { - .variable { + @supports (color: color-mix(in lab, red, red)) { color: color-mix(in oklab, #ef4444 var(--opacity), transparent); } } @@ -291,81 +307,89 @@ describe('theme', async () => { }) test('plugin theme colors can use ', async () => { + let input = css` + @tailwind utilities; + @theme { + /* This should not work */ + --color-custom-css: rgba(255 0 0 / ); + } + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ addUtilities, theme }) { + addUtilities({ + '.css-percentage': { + color: theme('colors.custom-css / 50%'), + }, + '.css-fraction': { + color: theme('colors.custom-css / 0.5'), + }, + '.css-variable': { + color: theme('colors.custom-css / var(--opacity)'), + }, + '.js-percentage': { + color: theme('colors.custom-js / 50%'), + }, + '.js-fraction': { + color: theme('colors.custom-js / 0.5'), + }, + '.js-variable': { + color: theme('colors.custom-js / var(--opacity)'), + }, + }) + }, + { + theme: { + colors: { + /* This should work */ + 'custom-js': 'rgb(255 0 0 / )', + }, + }, + }, + ), + } + }, + }) + expect( - await run( - [ - 'bg-custom', - 'css-percentage', - 'css-fraction', - 'css-variable', - 'js-percentage', - 'js-fraction', - 'js-variable', - ], - css` - @tailwind utilities; - @theme { - /* This should not work */ - --color-custom-css: rgba(255 0 0 / ); - } - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ addUtilities, theme }) { - addUtilities({ - '.css-percentage': { color: theme('colors.custom-css / 50%') }, - '.css-fraction': { color: theme('colors.custom-css / 0.5') }, - '.css-variable': { color: theme('colors.custom-css / var(--opacity)') }, - '.js-percentage': { color: theme('colors.custom-js / 50%') }, - '.js-fraction': { color: theme('colors.custom-js / 0.5') }, - '.js-variable': { color: theme('colors.custom-js / var(--opacity)') }, - }) - }, - { - theme: { - colors: { - /* This should work */ - 'custom-js': 'rgb(255 0 0 / )', - }, - }, - }, - ), - } - }, - }, - ), + compiler.build([ + 'bg-custom', + 'css-percentage', + 'css-fraction', + 'css-variable', + 'js-percentage', + 'js-fraction', + 'js-variable', + ]), ).toMatchInlineSnapshot(` - " - .css-fraction, .css-percentage { + ".css-fraction { + color: color-mix(in oklab, rgba(255 0 0 / ) 50%, transparent); + } + .css-percentage { color: color-mix(in oklab, rgba(255 0 0 / ) 50%, transparent); } - .css-variable { color: rgba(255 0 0 / ); - } - - @supports (color: color-mix(in lab, red, red)) { - .css-variable { + @supports (color: color-mix(in lab, red, red)) { color: color-mix(in oklab, rgba(255 0 0 / ) var(--opacity), transparent); } } - - .js-fraction, .js-percentage { - color: oklab(62.7955% .224 .125 / .5); + .js-fraction { + color: color-mix(in oklab, rgb(255 0 0 / 1) 50%, transparent); + } + .js-percentage { + color: color-mix(in oklab, rgb(255 0 0 / 1) 50%, transparent); } - .js-variable { - color: red; - } - - @supports (color: color-mix(in lab, red, red)) { - .js-variable { - color: color-mix(in oklab, red var(--opacity), transparent); + color: rgb(255 0 0 / 1); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, rgb(255 0 0 / 1) var(--opacity), transparent); } } " @@ -373,226 +397,260 @@ describe('theme', async () => { }) test('theme value functions are resolved correctly regardless of order', async () => { - expect( - await run( - ['animate-delay-316', 'animate-delay-slow'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'animate-delay': (value) => ({ 'animation-delay': value }) }, - { values: theme('animationDelay') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-delay': (value) => ({ 'animation-delay': value }), }, { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), - animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('animationDuration'), - }), - transitionDuration: { slow: '800ms' }, - }, + values: theme('animationDelay'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + + animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('animationDuration'), + }), + + transitionDuration: { + slow: '800ms', }, }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animate-delay-316 { - animation-delay: .316s; - } + }, + }, + ), + } + }, + }) + expect(compiler.build(['animate-delay-316', 'animate-delay-slow'])).toMatchInlineSnapshot(` + ".animate-delay-316 { + animation-delay: 316ms; + } .animate-delay-slow { - animation-delay: .8s; + animation-delay: 800ms; } " `) }) test('plugins can override the default key', async () => { - expect( - await run( - ['animate-duration'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'animate-duration': (value) => ({ 'animation-delay': value }) }, - { values: theme('transitionDuration') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-delay': value }), }, - { theme: { extend: { transitionDuration: { DEFAULT: '1500ms' } } } }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animate-duration { - animation-delay: 1.5s; + { + values: theme('transitionDuration'), + }, + ) + }, + { + theme: { + extend: { + transitionDuration: { + DEFAULT: '1500ms', + }, + }, + }, + }, + ), + } + }, + }) + + expect(compiler.build(['animate-duration'])).toMatchInlineSnapshot(` + ".animate-duration { + animation-delay: 1500ms; } " `) }) test('plugins can read CSS theme keys using the old theme key notation', async () => { - expect( - await run( - ['animation-spin', 'animation', 'animation2', 'animation2-twist'], - css` - @tailwind utilities; - @plugin "my-plugin"; - @theme reference { - --animate: pulse 1s linear infinite; - --animate-spin: spin 1s linear infinite; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ matchUtilities, theme }) { - matchUtilities( - { animation: (value) => ({ animation: value }) }, - { values: theme('animation') }, - ) - - matchUtilities( - { animation2: (value) => ({ animation: value }) }, - { - values: { DEFAULT: theme('animation.DEFAULT'), twist: theme('animation.spin') }, - }, - ) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animation { - animation: 1s linear infinite pulse; + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + @theme reference { + --animate: pulse 1s linear infinite; + --animate-spin: spin 1s linear infinite; } + ` - .animation-spin { - animation: 1s linear infinite spin; - } + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin(function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ animation: value }), + }, + { + values: theme('animation'), + }, + ) - .animation2 { - animation: 1s linear infinite pulse; - } + matchUtilities( + { + animation2: (value) => ({ animation: value }), + }, + { + values: { + DEFAULT: theme('animation.DEFAULT'), + twist: theme('animation.spin'), + }, + }, + ) + }), + } + }, + }) - .animation2-twist { - animation: 1s linear infinite spin; - } - " - `) + expect(compiler.build(['animation-spin', 'animation', 'animation2', 'animation2-twist'])) + .toMatchInlineSnapshot(` + ".animation { + animation: pulse 1s linear infinite; + } + .animation-spin { + animation: spin 1s linear infinite; + } + .animation2 { + animation: pulse 1s linear infinite; + } + .animation2-twist { + animation: spin 1s linear infinite; + } + " + `) }) test('CSS theme values are merged with JS theme values', async () => { - expect( - await run( - ['animation', 'animation-spin', 'animation-bounce'], - css` - @tailwind utilities; - @plugin "my-plugin"; - @theme reference { - --animate: pulse 1s linear infinite; - --animate-spin: spin 1s linear infinite; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { animation: (value) => ({ '--animation': value }) }, - { values: theme('animation') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + @theme reference { + --animate: pulse 1s linear infinite; + --animate-spin: spin 1s linear infinite; + } + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ '--animation': value }), }, - { theme: { extend: { animation: { bounce: 'bounce 1s linear infinite' } } } }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animation { - --animation: pulse 1s linear infinite; - } + { + values: theme('animation'), + }, + ) + }, + { + theme: { + extend: { + animation: { + bounce: 'bounce 1s linear infinite', + }, + }, + }, + }, + ), + } + }, + }) - .animation-bounce { - --animation: bounce 1s linear infinite; - } - - .animation-spin { - --animation: spin 1s linear infinite; - } - " - `) + expect(compiler.build(['animation', 'animation-spin', 'animation-bounce'])) + .toMatchInlineSnapshot(` + ".animation { + --animation: pulse 1s linear infinite; + } + .animation-bounce { + --animation: bounce 1s linear infinite; + } + .animation-spin { + --animation: spin 1s linear infinite; + } + " + `) }) test('CSS theme defaults take precedence over JS theme defaults', async () => { - expect( - await run( - ['animation'], - css` - @tailwind utilities; - @plugin "my-plugin"; - @theme reference { - --animate: pulse 1s linear infinite; - --animate-spin: spin 1s linear infinite; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { animation: (value) => ({ '--animation': value }) }, - { values: theme('animation') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + @theme reference { + --animate: pulse 1s linear infinite; + --animate-spin: spin 1s linear infinite; + } + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ '--animation': value }), }, - { theme: { extend: { animation: { DEFAULT: 'twist 1s linear infinite' } } } }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animation { + { + values: theme('animation'), + }, + ) + }, + { + theme: { + extend: { + animation: { + DEFAULT: 'twist 1s linear infinite', + }, + }, + }, + }, + ), + } + }, + }) + + expect(compiler.build(['animation'])).toMatchInlineSnapshot(` + ".animation { --animation: pulse 1s linear infinite; } " @@ -609,10 +667,10 @@ describe('theme', async () => { } ` - using fn = vi.fn() + let fn = vi.fn() await compile(input, { - loadModule: async (_id, base) => { + loadModule: async (id, base) => { return { path: '', base, @@ -621,7 +679,13 @@ describe('theme', async () => { fn(theme('animation.simple')) }, { - theme: { extend: { animation: { simple: 'simple 1s linear' } } }, + theme: { + extend: { + animation: { + simple: 'simple 1s linear', + }, + }, + }, }, ), } @@ -635,259 +699,241 @@ describe('theme', async () => { }) }) - test('theme() resolves the DEFAULT value when a bare CSS theme key shares a prefix with a sibling key', async () => { - expect( - await run( - ['example-foo', 'example-foo-bar'], - css` - @tailwind utilities; - @theme { - --color-foo: red; - --color-foo-bar: blue; - } - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.example-foo': { color: theme('colors.foo') }, - '.example-foo-bar': { color: theme('colors.foo-bar') }, - }) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .example-foo { - color: red; - } - - .example-foo-bar { - color: #00f; - } - " - `) - }) - test('all necessary theme keys support bare values', async () => { - expect( - await run( - [ - 'my-aspect-2/5', - 'my-backdrop-brightness-1', - 'my-backdrop-contrast-1', - 'my-backdrop-grayscale-1', - 'my-backdrop-hue-rotate-1', - 'my-backdrop-invert-1', - 'my-backdrop-opacity-1', - 'my-backdrop-saturate-1', - 'my-backdrop-sepia-1', - 'my-border-width-1', - 'my-brightness-1', - 'my-columns-1', - 'my-contrast-1', - 'my-divide-width-1', - 'my-flex-grow-1', - 'my-flex-shrink-1', - 'my-gradient-color-stop-positions-1', - 'my-grayscale-1', - 'my-grid-row-end-1', - 'my-grid-row-start-1', - 'my-grid-template-columns-1', - 'my-grid-template-rows-1', - 'my-hue-rotate-1', - 'my-invert-1', - 'my-line-clamp-1', - 'my-opacity-1', - 'my-order-1', - 'my-outline-offset-1', - 'my-outline-width-1', - 'my-ring-offset-width-1', - 'my-ring-width-1', - 'my-rotate-1', - 'my-saturate-1', - 'my-scale-1', - 'my-sepia-1', - 'my-skew-1', - 'my-stroke-width-1', - 'my-text-decoration-thickness-1', - 'my-text-underline-offset-1', - 'my-transition-delay-1', - 'my-transition-duration-1', - 'my-z-index-1', - ], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ matchUtilities, theme }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - { values: theme(themeKey) }, - ) - } + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` - utility('my-aspect', 'aspectRatio') - utility('my-backdrop-brightness', 'backdropBrightness') - utility('my-backdrop-contrast', 'backdropContrast') - utility('my-backdrop-grayscale', 'backdropGrayscale') - utility('my-backdrop-hue-rotate', 'backdropHueRotate') - utility('my-backdrop-invert', 'backdropInvert') - utility('my-backdrop-opacity', 'backdropOpacity') - utility('my-backdrop-saturate', 'backdropSaturate') - utility('my-backdrop-sepia', 'backdropSepia') - utility('my-border-width', 'borderWidth') - utility('my-brightness', 'brightness') - utility('my-columns', 'columns') - utility('my-contrast', 'contrast') - utility('my-divide-width', 'divideWidth') - utility('my-flex-grow', 'flexGrow') - utility('my-flex-shrink', 'flexShrink') - utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') - utility('my-grayscale', 'grayscale') - utility('my-grid-row-end', 'gridRowEnd') - utility('my-grid-row-start', 'gridRowStart') - utility('my-grid-template-columns', 'gridTemplateColumns') - utility('my-grid-template-rows', 'gridTemplateRows') - utility('my-hue-rotate', 'hueRotate') - utility('my-invert', 'invert') - utility('my-line-clamp', 'lineClamp') - utility('my-opacity', 'opacity') - utility('my-order', 'order') - utility('my-outline-offset', 'outlineOffset') - utility('my-outline-width', 'outlineWidth') - utility('my-ring-offset-width', 'ringOffsetWidth') - utility('my-ring-width', 'ringWidth') - utility('my-rotate', 'rotate') - utility('my-saturate', 'saturate') - utility('my-scale', 'scale') - utility('my-sepia', 'sepia') - utility('my-skew', 'skew') - utility('my-stroke-width', 'strokeWidth') - utility('my-text-decoration-thickness', 'textDecorationThickness') - utility('my-text-underline-offset', 'textUnderlineOffset') - utility('my-transition-delay', 'transitionDelay') - utility('my-transition-duration', 'transitionDuration') - utility('my-z-index', 'zIndex') - }), + let { build } = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin(function ({ matchUtilities, theme }) { + function utility(name: string, themeKey: string) { + matchUtilities( + { [name]: (value) => ({ '--value': value }) }, + { values: theme(themeKey) }, + ) } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .my-aspect-2\\/5 { + + utility('my-aspect', 'aspectRatio') + utility('my-backdrop-brightness', 'backdropBrightness') + utility('my-backdrop-contrast', 'backdropContrast') + utility('my-backdrop-grayscale', 'backdropGrayscale') + utility('my-backdrop-hue-rotate', 'backdropHueRotate') + utility('my-backdrop-invert', 'backdropInvert') + utility('my-backdrop-opacity', 'backdropOpacity') + utility('my-backdrop-saturate', 'backdropSaturate') + utility('my-backdrop-sepia', 'backdropSepia') + utility('my-border-width', 'borderWidth') + utility('my-brightness', 'brightness') + utility('my-columns', 'columns') + utility('my-contrast', 'contrast') + utility('my-divide-width', 'divideWidth') + utility('my-flex-grow', 'flexGrow') + utility('my-flex-shrink', 'flexShrink') + utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') + utility('my-grayscale', 'grayscale') + utility('my-grid-row-end', 'gridRowEnd') + utility('my-grid-row-start', 'gridRowStart') + utility('my-grid-template-columns', 'gridTemplateColumns') + utility('my-grid-template-rows', 'gridTemplateRows') + utility('my-hue-rotate', 'hueRotate') + utility('my-invert', 'invert') + utility('my-line-clamp', 'lineClamp') + utility('my-opacity', 'opacity') + utility('my-order', 'order') + utility('my-outline-offset', 'outlineOffset') + utility('my-outline-width', 'outlineWidth') + utility('my-ring-offset-width', 'ringOffsetWidth') + utility('my-ring-width', 'ringWidth') + utility('my-rotate', 'rotate') + utility('my-saturate', 'saturate') + utility('my-scale', 'scale') + utility('my-sepia', 'sepia') + utility('my-skew', 'skew') + utility('my-stroke-width', 'strokeWidth') + utility('my-text-decoration-thickness', 'textDecorationThickness') + utility('my-text-underline-offset', 'textUnderlineOffset') + utility('my-transition-delay', 'transitionDelay') + utility('my-transition-duration', 'transitionDuration') + utility('my-z-index', 'zIndex') + }), + } + }, + }) + + let output = build([ + 'my-aspect-2/5', + 'my-backdrop-brightness-1', + 'my-backdrop-contrast-1', + 'my-backdrop-grayscale-1', + 'my-backdrop-hue-rotate-1', + 'my-backdrop-invert-1', + 'my-backdrop-opacity-1', + 'my-backdrop-saturate-1', + 'my-backdrop-sepia-1', + 'my-border-width-1', + 'my-brightness-1', + 'my-columns-1', + 'my-contrast-1', + 'my-divide-width-1', + 'my-flex-grow-1', + 'my-flex-shrink-1', + 'my-gradient-color-stop-positions-1', + 'my-grayscale-1', + 'my-grid-row-end-1', + 'my-grid-row-start-1', + 'my-grid-template-columns-1', + 'my-grid-template-rows-1', + 'my-hue-rotate-1', + 'my-invert-1', + 'my-line-clamp-1', + 'my-opacity-1', + 'my-order-1', + 'my-outline-offset-1', + 'my-outline-width-1', + 'my-ring-offset-width-1', + 'my-ring-width-1', + 'my-rotate-1', + 'my-saturate-1', + 'my-scale-1', + 'my-sepia-1', + 'my-skew-1', + 'my-stroke-width-1', + 'my-text-decoration-thickness-1', + 'my-text-underline-offset-1', + 'my-transition-delay-1', + 'my-transition-duration-1', + 'my-z-index-1', + ]) + + expect(output).toMatchInlineSnapshot(` + ".my-aspect-2\\/5 { --value: 2/5; } - - .my-backdrop-brightness-1, .my-backdrop-contrast-1, .my-backdrop-grayscale-1 { + .my-backdrop-brightness-1 { + --value: 1%; + } + .my-backdrop-contrast-1 { + --value: 1%; + } + .my-backdrop-grayscale-1 { --value: 1%; } - .my-backdrop-hue-rotate-1 { --value: 1deg; } - - .my-backdrop-invert-1, .my-backdrop-opacity-1, .my-backdrop-saturate-1, .my-backdrop-sepia-1 { + .my-backdrop-invert-1 { + --value: 1%; + } + .my-backdrop-opacity-1 { + --value: 1%; + } + .my-backdrop-saturate-1 { + --value: 1%; + } + .my-backdrop-sepia-1 { --value: 1%; } - .my-border-width-1 { --value: 1px; } - .my-brightness-1 { --value: 1%; } - .my-columns-1 { --value: 1; } - .my-contrast-1 { --value: 1%; } - .my-divide-width-1 { --value: 1px; } - - .my-flex-grow-1, .my-flex-shrink-1 { + .my-flex-grow-1 { --value: 1; } - - .my-gradient-color-stop-positions-1, .my-grayscale-1 { + .my-flex-shrink-1 { + --value: 1; + } + .my-gradient-color-stop-positions-1 { --value: 1%; } - - .my-grid-row-end-1, .my-grid-row-start-1 { + .my-grayscale-1 { + --value: 1%; + } + .my-grid-row-end-1 { --value: 1; } - - .my-grid-template-columns-1, .my-grid-template-rows-1 { + .my-grid-row-start-1 { + --value: 1; + } + .my-grid-template-columns-1 { + --value: repeat(1, minmax(0, 1fr)); + } + .my-grid-template-rows-1 { --value: repeat(1, minmax(0, 1fr)); } - .my-hue-rotate-1 { --value: 1deg; } - .my-invert-1 { --value: 1%; } - .my-line-clamp-1 { --value: 1; } - .my-opacity-1 { --value: 1%; } - .my-order-1 { --value: 1; } - - .my-outline-offset-1, .my-outline-width-1, .my-ring-offset-width-1, .my-ring-width-1 { + .my-outline-offset-1 { + --value: 1px; + } + .my-outline-width-1 { + --value: 1px; + } + .my-ring-offset-width-1 { + --value: 1px; + } + .my-ring-width-1 { --value: 1px; } - .my-rotate-1 { --value: 1deg; } - - .my-saturate-1, .my-scale-1, .my-sepia-1 { + .my-saturate-1 { + --value: 1%; + } + .my-scale-1 { + --value: 1%; + } + .my-sepia-1 { --value: 1%; } - .my-skew-1 { --value: 1deg; } - .my-stroke-width-1 { --value: 1; } - - .my-text-decoration-thickness-1, .my-text-underline-offset-1 { + .my-text-decoration-thickness-1 { --value: 1px; } - - .my-transition-delay-1, .my-transition-duration-1 { + .my-text-underline-offset-1 { + --value: 1px; + } + .my-transition-delay-1 { + --value: 1ms; + } + .my-transition-duration-1 { --value: 1ms; } - .my-z-index-1 { --value: 1; } @@ -905,10 +951,10 @@ describe('theme', async () => { } ` - using fn = vi.fn() + let fn = vi.fn() await compile(input, { - loadModule: async (_id, base) => { + loadModule: async (id, base) => { return { path: '', base, @@ -948,10 +994,10 @@ describe('theme', async () => { } ` - using fn = vi.fn() + let fn = vi.fn() await compile(input, { - loadModule: async (_id, base) => { + loadModule: async (id, base) => { return { path: '', base, @@ -980,10 +1026,10 @@ describe('theme', async () => { } ` - using fn = vi.fn() + let fn = vi.fn() await compile(input, { - loadModule: async (_id, base) => { + loadModule: async (id, base) => { return { path: '', base, @@ -1009,10 +1055,10 @@ describe('theme', async () => { @plugin "my-plugin"; ` - using fn = vi.fn() + let fn = vi.fn() await compile(input, { - loadModule: async (_id, base) => { + loadModule: async (id, base) => { return { path: '', base, @@ -1033,242 +1079,273 @@ describe('theme', async () => { }) test('Candidates can match multiple utility definitions', async () => { - expect( - await run( - ['foo-bar'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(({ addUtilities, matchUtilities }) => { - addUtilities({ '.foo-bar': { color: 'red' } }) - matchUtilities( - { foo: (value) => ({ '--my-prop': value }) }, - { values: { bar: 'bar-valuer', baz: 'bar-valuer' } }, - ) - addUtilities({ '.foo-bar': { backgroundColor: 'red' } }) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let { build } = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin(({ addUtilities, matchUtilities }) => { + addUtilities({ + '.foo-bar': { + color: 'red', + }, + }) + + matchUtilities( + { + foo: (value) => ({ + '--my-prop': value, + }), + }, + { + values: { + bar: 'bar-valuer', + baz: 'bar-valuer', + }, + }, + ) + + addUtilities({ + '.foo-bar': { + backgroundColor: 'red', + }, + }) + }), + } + }, + }) + + expect(build(['foo-bar'])).toMatchInlineSnapshot(` + ".foo-bar { + background-color: red; + } .foo-bar { color: red; + } + .foo-bar { --my-prop: bar-valuer; - background-color: red; } " `) }) test('spreading `tailwindcss/defaultTheme` exports keeps bare values', async () => { - expect( - await run( - [ - 'my-aspect-2/5', - // 'my-backdrop-brightness-1', - // 'my-backdrop-contrast-1', - // 'my-backdrop-grayscale-1', - // 'my-backdrop-hue-rotate-1', - // 'my-backdrop-invert-1', - // 'my-backdrop-opacity-1', - // 'my-backdrop-saturate-1', - // 'my-backdrop-sepia-1', - // 'my-divide-width-1', - 'my-border-width-1', - 'my-brightness-1', - 'my-columns-1', - 'my-contrast-1', - 'my-flex-grow-1', - 'my-flex-shrink-1', - 'my-gradient-color-stop-positions-1', - 'my-grayscale-1', - 'my-grid-row-end-1', - 'my-grid-row-start-1', - 'my-grid-template-columns-1', - 'my-grid-template-rows-1', - 'my-hue-rotate-1', - 'my-invert-1', - 'my-line-clamp-1', - 'my-opacity-1', - 'my-order-1', - 'my-outline-offset-1', - 'my-outline-width-1', - 'my-ring-offset-width-1', - 'my-ring-width-1', - 'my-rotate-1', - 'my-saturate-1', - 'my-scale-1', - 'my-sepia-1', - 'my-skew-1', - 'my-stroke-width-1', - 'my-text-decoration-thickness-1', - 'my-text-underline-offset-1', - 'my-transition-delay-1', - 'my-transition-duration-1', - 'my-z-index-1', - ], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ matchUtilities }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - // @ts-ignore - { values: defaultTheme[themeKey] }, - ) - } + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` - utility('my-aspect', 'aspectRatio') - // The following keys deliberately doesn't work as these are exported - // as functions from the compat config. - // - // utility('my-backdrop-brightness', 'backdropBrightness') - // utility('my-backdrop-contrast', 'backdropContrast') - // utility('my-backdrop-grayscale', 'backdropGrayscale') - // utility('my-backdrop-hue-rotate', 'backdropHueRotate') - // utility('my-backdrop-invert', 'backdropInvert') - // utility('my-backdrop-opacity', 'backdropOpacity') - // utility('my-backdrop-saturate', 'backdropSaturate') - // utility('my-backdrop-sepia', 'backdropSepia') - // utility('my-divide-width', 'divideWidth') - utility('my-border-width', 'borderWidth') - utility('my-brightness', 'brightness') - utility('my-columns', 'columns') - utility('my-contrast', 'contrast') - utility('my-flex-grow', 'flexGrow') - utility('my-flex-shrink', 'flexShrink') - utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') - utility('my-grayscale', 'grayscale') - utility('my-grid-row-end', 'gridRowEnd') - utility('my-grid-row-start', 'gridRowStart') - utility('my-grid-template-columns', 'gridTemplateColumns') - utility('my-grid-template-rows', 'gridTemplateRows') - utility('my-hue-rotate', 'hueRotate') - utility('my-invert', 'invert') - utility('my-line-clamp', 'lineClamp') - utility('my-opacity', 'opacity') - utility('my-order', 'order') - utility('my-outline-offset', 'outlineOffset') - utility('my-outline-width', 'outlineWidth') - utility('my-ring-offset-width', 'ringOffsetWidth') - utility('my-ring-width', 'ringWidth') - utility('my-rotate', 'rotate') - utility('my-saturate', 'saturate') - utility('my-scale', 'scale') - utility('my-sepia', 'sepia') - utility('my-skew', 'skew') - utility('my-stroke-width', 'strokeWidth') - utility('my-text-decoration-thickness', 'textDecorationThickness') - utility('my-text-underline-offset', 'textUnderlineOffset') - utility('my-transition-delay', 'transitionDelay') - utility('my-transition-duration', 'transitionDuration') - utility('my-z-index', 'zIndex') - }), + let { build } = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin(function ({ matchUtilities }) { + function utility(name: string, themeKey: string) { + matchUtilities( + { [name]: (value) => ({ '--value': value }) }, + // @ts-ignore + { values: defaultTheme[themeKey] }, + ) } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .my-aspect-2\\/5 { + + utility('my-aspect', 'aspectRatio') + // The following keys deliberately doesn't work as these are exported + // as functions from the compat config. + // + // utility('my-backdrop-brightness', 'backdropBrightness') + // utility('my-backdrop-contrast', 'backdropContrast') + // utility('my-backdrop-grayscale', 'backdropGrayscale') + // utility('my-backdrop-hue-rotate', 'backdropHueRotate') + // utility('my-backdrop-invert', 'backdropInvert') + // utility('my-backdrop-opacity', 'backdropOpacity') + // utility('my-backdrop-saturate', 'backdropSaturate') + // utility('my-backdrop-sepia', 'backdropSepia') + // utility('my-divide-width', 'divideWidth') + utility('my-border-width', 'borderWidth') + utility('my-brightness', 'brightness') + utility('my-columns', 'columns') + utility('my-contrast', 'contrast') + utility('my-flex-grow', 'flexGrow') + utility('my-flex-shrink', 'flexShrink') + utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') + utility('my-grayscale', 'grayscale') + utility('my-grid-row-end', 'gridRowEnd') + utility('my-grid-row-start', 'gridRowStart') + utility('my-grid-template-columns', 'gridTemplateColumns') + utility('my-grid-template-rows', 'gridTemplateRows') + utility('my-hue-rotate', 'hueRotate') + utility('my-invert', 'invert') + utility('my-line-clamp', 'lineClamp') + utility('my-opacity', 'opacity') + utility('my-order', 'order') + utility('my-outline-offset', 'outlineOffset') + utility('my-outline-width', 'outlineWidth') + utility('my-ring-offset-width', 'ringOffsetWidth') + utility('my-ring-width', 'ringWidth') + utility('my-rotate', 'rotate') + utility('my-saturate', 'saturate') + utility('my-scale', 'scale') + utility('my-sepia', 'sepia') + utility('my-skew', 'skew') + utility('my-stroke-width', 'strokeWidth') + utility('my-text-decoration-thickness', 'textDecorationThickness') + utility('my-text-underline-offset', 'textUnderlineOffset') + utility('my-transition-delay', 'transitionDelay') + utility('my-transition-duration', 'transitionDuration') + utility('my-z-index', 'zIndex') + }), + } + }, + }) + + let output = build([ + 'my-aspect-2/5', + // 'my-backdrop-brightness-1', + // 'my-backdrop-contrast-1', + // 'my-backdrop-grayscale-1', + // 'my-backdrop-hue-rotate-1', + // 'my-backdrop-invert-1', + // 'my-backdrop-opacity-1', + // 'my-backdrop-saturate-1', + // 'my-backdrop-sepia-1', + // 'my-divide-width-1', + 'my-border-width-1', + 'my-brightness-1', + 'my-columns-1', + 'my-contrast-1', + 'my-flex-grow-1', + 'my-flex-shrink-1', + 'my-gradient-color-stop-positions-1', + 'my-grayscale-1', + 'my-grid-row-end-1', + 'my-grid-row-start-1', + 'my-grid-template-columns-1', + 'my-grid-template-rows-1', + 'my-hue-rotate-1', + 'my-invert-1', + 'my-line-clamp-1', + 'my-opacity-1', + 'my-order-1', + 'my-outline-offset-1', + 'my-outline-width-1', + 'my-ring-offset-width-1', + 'my-ring-width-1', + 'my-rotate-1', + 'my-saturate-1', + 'my-scale-1', + 'my-sepia-1', + 'my-skew-1', + 'my-stroke-width-1', + 'my-text-decoration-thickness-1', + 'my-text-underline-offset-1', + 'my-transition-delay-1', + 'my-transition-duration-1', + 'my-z-index-1', + ]) + + expect(output).toMatchInlineSnapshot(` + ".my-aspect-2\\/5 { --value: 2/5; } - .my-border-width-1 { --value: 1px; } - .my-brightness-1 { --value: 1%; } - .my-columns-1 { --value: 1; } - .my-contrast-1 { --value: 1%; } - - .my-flex-grow-1, .my-flex-shrink-1 { + .my-flex-grow-1 { --value: 1; } - - .my-gradient-color-stop-positions-1, .my-grayscale-1 { + .my-flex-shrink-1 { + --value: 1; + } + .my-gradient-color-stop-positions-1 { --value: 1%; } - - .my-grid-row-end-1, .my-grid-row-start-1 { + .my-grayscale-1 { + --value: 1%; + } + .my-grid-row-end-1 { --value: 1; } - - .my-grid-template-columns-1, .my-grid-template-rows-1 { + .my-grid-row-start-1 { + --value: 1; + } + .my-grid-template-columns-1 { + --value: repeat(1, minmax(0, 1fr)); + } + .my-grid-template-rows-1 { --value: repeat(1, minmax(0, 1fr)); } - .my-hue-rotate-1 { --value: 1deg; } - .my-invert-1 { --value: 1%; } - .my-line-clamp-1 { --value: 1; } - .my-opacity-1 { --value: 1%; } - .my-order-1 { --value: 1; } - - .my-outline-offset-1, .my-outline-width-1, .my-ring-offset-width-1, .my-ring-width-1 { + .my-outline-offset-1 { + --value: 1px; + } + .my-outline-width-1 { + --value: 1px; + } + .my-ring-offset-width-1 { + --value: 1px; + } + .my-ring-width-1 { --value: 1px; } - .my-rotate-1 { --value: 1deg; } - - .my-saturate-1, .my-scale-1, .my-sepia-1 { + .my-saturate-1 { + --value: 1%; + } + .my-scale-1 { + --value: 1%; + } + .my-sepia-1 { --value: 1%; } - .my-skew-1 { --value: 1deg; } - .my-stroke-width-1 { --value: 1; } - - .my-text-decoration-thickness-1, .my-text-underline-offset-1 { + .my-text-decoration-thickness-1 { --value: 1px; } - - .my-transition-delay-1, .my-transition-duration-1 { + .my-text-underline-offset-1 { + --value: 1px; + } + .my-transition-delay-1 { + --value: 1ms; + } + .my-transition-duration-1 { --value: 1ms; } - .my-z-index-1 { --value: 1; } @@ -1277,211 +1354,196 @@ describe('theme', async () => { }) test('can use escaped JS variables in theme values', async () => { - expect( - await run( - ['my-width-1', 'my-width-1/2', 'my-width-1.5'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'my-width': (value) => ({ width: value }) }, - { values: theme('width') }, - ) - }, - { - theme: { - extend: { - width: { - '1': '0.25rem', - // Purposely setting to something different from the v3 default - '1/2': '60%', - '1.5': '0.375rem', - }, - }, + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { 'my-width': (value) => ({ width: value }) }, + { values: theme('width') }, + ) + }, + { + theme: { + extend: { + width: { + '1': '0.25rem', + // Purposely setting to something different from the v3 default + '1/2': '60%', + '1.5': '0.375rem', }, }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .my-width-1 { - width: .25rem; - } + }, + }, + ), + } + }, + }) + expect(compiler.build(['my-width-1', 'my-width-1/2', 'my-width-1.5'])).toMatchInlineSnapshot( + ` + ".my-width-1 { + width: 0.25rem; + } .my-width-1\\.5 { - width: .375rem; + width: 0.375rem; } - .my-width-1\\/2 { width: 60%; } " - `) + `, + ) }) test('can use escaped CSS variables in theme values', async () => { - expect( - await run( - ['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5'], - css` - @tailwind utilities; - @plugin "my-plugin"; + let input = css` + @tailwind utilities; + @plugin "my-plugin"; - @theme { - --width-1: 0.25rem; - /* Purposely setting to something different from the v3 default */ - --width-1\/2: 60%; - --width-1\.5: 0.375rem; - --width-2_5: 0.625rem; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ matchUtilities, theme }) { - matchUtilities( - { 'my-width': (value) => ({ width: value }) }, - { values: theme('width') }, - ) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .my-width-1 { - width: .25rem; + @theme { + --width-1: 0.25rem; + /* Purposely setting to something different from the v3 default */ + --width-1\/2: 60%; + --width-1\.5: 0.375rem; + --width-2_5: 0.625rem; } + ` - .my-width-1\\.5 { - width: .375rem; - } + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin(function ({ matchUtilities, theme }) { + matchUtilities( + { 'my-width': (value) => ({ width: value }) }, + { values: theme('width') }, + ) + }), + } + }, + }) - .my-width-1\\/2 { - width: 60%; - } - - .my-width-2\\.5 { - width: .625rem; - } - " - `) + expect(compiler.build(['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5'])) + .toMatchInlineSnapshot(` + ".my-width-1 { + width: 0.25rem; + } + .my-width-1\\.5 { + width: 0.375rem; + } + .my-width-1\\/2 { + width: 60%; + } + .my-width-2\\.5 { + width: 0.625rem; + } + " + `) }) test('can use escaped CSS variables in referenced theme namespace', async () => { - expect( - await run( - ['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5'], - css` - @tailwind utilities; - @plugin "my-plugin"; + let input = css` + @tailwind utilities; + @plugin "my-plugin"; - @theme { - --width-1: 0.25rem; - /* Purposely setting to something different from the v3 default */ - --width-1\/2: 60%; - --width-1\.5: 0.375rem; - --width-2_5: 0.625rem; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'my-width': (value) => ({ width: value }) }, - { values: theme('myWidth') }, - ) - }, - { - theme: { myWidth: ({ theme }) => theme('width') }, - }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .my-width-1 { - width: .25rem; + @theme { + --width-1: 0.25rem; + /* Purposely setting to something different from the v3 default */ + --width-1\/2: 60%; + --width-1\.5: 0.375rem; + --width-2_5: 0.625rem; } + ` - .my-width-1\\.5 { - width: .375rem; - } + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + path: '', + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { 'my-width': (value) => ({ width: value }) }, + { values: theme('myWidth') }, + ) + }, + { + theme: { myWidth: ({ theme }) => theme('width') }, + }, + ), + } + }, + }) - .my-width-1\\/2 { - width: 60%; - } - - .my-width-2\\.5 { - width: .625rem; - } - " - `) + expect(compiler.build(['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5'])) + .toMatchInlineSnapshot(` + ".my-width-1 { + width: 0.25rem; + } + .my-width-1\\.5 { + width: 0.375rem; + } + .my-width-1\\/2 { + width: 60%; + } + .my-width-2\\.5 { + width: 0.625rem; + } + " + `) }) }) describe('addBase', () => { test('does not create rules when imported via `@import "…" reference`', async () => { - expect( - await compileCss( - css` - @tailwind utilities; - @plugin "outside"; - @import './inside.css' reference; - `, - { - loadModule: async (_id, base) => { - if (_id === 'inside') { - return { - path: '', - base, - module: plugin(function ({ addBase }) { - addBase({ inside: { color: 'red' } }) - }), - } - } - return { - path: '', - base, - module: plugin(function ({ addBase }) { - addBase({ outside: { color: 'red' } }) - }), - } - }, - async loadStylesheet() { - return { - path: '', - base: '', - content: css` - @plugin "inside"; - `, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + let input = css` + @tailwind utilities; + @plugin "outside"; + @import './inside.css' reference; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + if (id === 'inside') { + return { + path: '', + base, + module: plugin(function ({ addBase }) { + addBase({ inside: { color: 'red' } }) + }), + } + } + return { + path: '', + base, + module: plugin(function ({ addBase }) { + addBase({ outside: { color: 'red' } }) + }), + } + }, + async loadStylesheet() { + return { + path: '', + base: '', + content: css` + @plugin "inside"; + `, + } + }, + }) + + expect(compiler.build([])).toMatchInlineSnapshot(` + "@layer base { outside { color: red; } @@ -1491,24 +1553,28 @@ describe('addBase', () => { }) test('does not modify CSS variables', async () => { - expect( - await compileCss( - css` - @plugin "my-plugin"; - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: plugin(function ({ addBase }) { - addBase({ ':root': { '--PascalCase': '1', '--camelCase': '1', '--UPPERCASE': '1' } }) - }), - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + let input = css` + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + path: '', + base: '/root', + module: plugin(function ({ addBase }) { + addBase({ + ':root': { + '--PascalCase': '1', + '--camelCase': '1', + '--UPPERCASE': '1', + }, + }) + }), + }), + }) + + expect(compiler.build([])).toMatchInlineSnapshot(` + "@layer base { :root { --PascalCase: 1; --camelCase: 1; @@ -1518,321 +1584,154 @@ describe('addBase', () => { " `) }) - - test('@variant is replaced inside rules using addBase', async () => { - expect( - await compileCss( - css` - @plugin "my-plugin"; - @theme { - --breakpoint-supertiny: 128px; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: plugin(function ({ addBase }) { - addBase({ - ':root': { - '@variant supertiny': { - '--PascalCase': '1', - '--camelCase': '1', - '--UPPERCASE': '1', - }, - '@variant data-enabled:focus, disabled': { - '--x': '1', - }, - }, - }) - }), - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { - @media (min-width: 128px) { - :root { - --PascalCase: 1; - --camelCase: 1; - --UPPERCASE: 1; - } - } - - :root[data-enabled]:focus, :root:disabled { - --x: 1; - } - } - " - `) - }) - - test('@variant inside addBase works with custom variants', async () => { - expect( - await compileCss( - css` - @plugin "my-plugin"; - - @custom-variant custom (&.custom); - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: plugin(function ({ addBase }) { - addBase({ - ':root': { - '@variant custom': { - '--x': '1', - }, - }, - }) - }), - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { - :root.custom { - --x: 1; - } - } - " - `) - }) }) describe('addVariant', () => { test('addVariant with string selector', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', '&:hover, &:focus') + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', '&:hover, &:focus') - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) { + }, + ) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " - `) - }) - - test('addVariant with `@scope`', async () => { - expect( - await run( - ['blue:underline', 'green:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('blue', '@scope ([data-theme=blue]) to ([data-theme])') - addVariant('green', '@scope ([data-theme=green]) to ([data-theme]) { & }') - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope ([data-theme="blue"]) to ([data-theme]) { - .blue\\:underline { - text-decoration-line: underline; - } - } - - @scope ([data-theme="green"]) to ([data-theme]) { - .green\\:underline { - text-decoration-line: underline; - } - } - } - " - `) - }) - - test('matchVariant with `@scope`', async () => { - expect( - await run( - ['theme-[blue]:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('theme', (value) => `@scope ([data-theme=${value}]) to ([data-theme])`) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope ([data-theme="blue"]) to ([data-theme]) { - .theme-\\[blue\\]\\:underline { - text-decoration-line: underline; - } - } - } - " + }" `) }) test('addVariant with array of selectors', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', ['&:hover', '&:focus']) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', ['&:hover', '&:focus']) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) { + }, + ) + + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('addVariant with object syntax and @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '&:hover': '@slot', + '&:focus': '@slot', + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&:hover': '@slot', - '&:focus': '@slot', - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) { + }, + ) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('addVariant with object syntax, media, nesting and multiple @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '@media (hover: hover)': { + '&:hover': '@slot', + }, + '&:focus': '@slot', + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '@media (hover: hover)': { - '&:hover': '@slot', - }, - '&:focus': '@slot', - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (hover: hover) { - :is(:where(.group):hover .group-hocus\\:flex) { + .group-hocus\\:flex:is(:where(.group):hover *) { display: flex; } } - :is(:where(.group):focus .group-hocus\\:flex) { + .group-hocus\\:flex:is(:where(.group):focus *) { display: flex; } @@ -1845,39 +1744,37 @@ describe('addVariant', () => { .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('addVariant with at-rules and placeholder', async () => { - expect( - await run( - ['potato:underline', 'potato:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ addVariant }: PluginAPI) => { + addVariant( + 'potato', + '@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }', + ) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant( - 'potato', - '@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }', - ) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['potato:underline', 'potato:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (max-width: 400px) { @supports (font: bold) { .potato\\:flex:large-potato { @@ -1889,42 +1786,40 @@ describe('addVariant', () => { } } } - } - " + }" `) }) test('@slot is preserved when used as a custom property value', async () => { - expect( - await run( - ['hocus:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '&': { + '--custom-property': '@slot', + '&:hover': '@slot', + '&:focus': '@slot', + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&': { - '--custom-property': '@slot', - '&:hover': '@slot', - '&:focus': '@slot', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['hocus:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .hocus\\:underline { --custom-property: @slot; } @@ -1932,73 +1827,74 @@ describe('addVariant', () => { .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('ignores variants that use :merge(…) and ensures `peer-*` and `group-*` rules work out of the box', async () => { - expect( - await run( - ['optional:flex', 'group-optional:flex', 'peer-optional:flex', 'group-optional/foo:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('optional', '&:optional') + addVariant('group-optional', { ':merge(.group):optional &': '@slot' }) + addVariant('peer-optional', { '&': { ':merge(.peer):optional ~ &': '@slot' } }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('optional', '&:optional') - addVariant('group-optional', { ':merge(.group):optional &': '@slot' }) - addVariant('peer-optional', { '&': { ':merge(.peer):optional ~ &': '@slot' } }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group):optional .group-optional\\:flex), :is(:where(.group\\/foo):optional .group-optional\\/foo\\:flex), :is(:where(.peer):optional ~ .peer-optional\\:flex), .optional\\:flex:optional { + }, + ) + let compiled = build([ + 'optional:flex', + 'group-optional:flex', + 'peer-optional:flex', + 'group-optional/foo:flex', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-optional\\:flex:is(:where(.group):optional *), .group-optional\\/foo\\:flex:is(:where(.group\\/foo):optional *), .peer-optional\\:flex:is(:where(.peer):optional ~ *), .optional\\:flex:optional { display: flex; } - } - " + }" `) }) }) describe('matchVariant', () => { test('partial arbitrary variants', async () => { - expect( - await run( - ['potato-[yellow]:underline', 'potato-[baked]:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `.potato-${flavor} &`) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `.potato-${flavor} &`) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['potato-[yellow]:underline', 'potato-[baked]:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .potato-baked .potato-\\[baked\\]\\:flex { display: flex; } @@ -2006,36 +1902,34 @@ describe('matchVariant', () => { .potato-yellow .potato-\\[yellow\\]\\:underline { text-decoration-line: underline; } - } - " + }" `) }) test('partial arbitrary variants with at-rules', async () => { - expect( - await run( - ['potato-[yellow]:underline', 'potato-[baked]:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['potato-[yellow]:underline', 'potato-[baked]:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (potato: baked) { .potato-\\[baked\\]\\:flex { display: flex; @@ -2047,38 +1941,38 @@ describe('matchVariant', () => { text-decoration-line: underline; } } - } - " + }" `) }) test('partial arbitrary variants with at-rules and placeholder', async () => { - expect( - await run( - ['potato-[yellow]:underline', 'potato-[baked]:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant( + 'potato', + (flavor) => + `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }`, + ) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => { - return `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }` - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['potato-[yellow]:underline', 'potato-[baked]:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (potato: baked) { @supports (font: bold) { .potato-\\[baked\\]\\:flex:large-potato { @@ -2094,41 +1988,39 @@ describe('matchVariant', () => { } } } - } - " + }" `) }) test('partial arbitrary variants with default values', async () => { - expect( - await run( - ['tooltip-bottom:underline', 'tooltip-top:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('tooltip', (side) => `&${side}`, { + values: { + bottom: '[data-location="bottom"]', + top: '[data-location="top"]', + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('tooltip', (side) => `&${side}`, { - values: { - bottom: '[data-location="bottom"]', - top: '[data-location="top"]', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['tooltip-bottom:underline', 'tooltip-top:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .tooltip-bottom\\:underline[data-location="bottom"] { text-decoration-line: underline; } @@ -2136,120 +2028,118 @@ describe('matchVariant', () => { .tooltip-top\\:flex[data-location="top"] { display: flex; } - } - " + }" `) }) test('matched variant values maintain the sort order they are registered in', async () => { - expect( - await run( - [ - 'alphabet-c:underline', - 'alphabet-a:underline', - 'alphabet-d:underline', - 'alphabet-b:underline', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('alphabet', (side) => `&${side}`, { + values: { + d: '[data-order="1"]', + a: '[data-order="2"]', + c: '[data-order="3"]', + b: '[data-order="4"]', + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('alphabet', (side) => `&${side}`, { - values: { - d: '[data-order="1"]', - a: '[data-order="2"]', - c: '[data-order="3"]', - b: '[data-order="4"]', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build([ + 'alphabet-c:underline', + 'alphabet-a:underline', + 'alphabet-d:underline', + 'alphabet-b:underline', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .alphabet-d\\:underline[data-order="1"], .alphabet-a\\:underline[data-order="2"], .alphabet-c\\:underline[data-order="3"], .alphabet-b\\:underline[data-order="4"] { text-decoration-line: underline; } - } - " + }" `) }) test('matchVariant can return an array of format strings from the function', async () => { - expect( - await run( - ['test-[a,b,c]:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('test', (selector) => + selector.split(',').map((selector) => `&.${selector} > *`), + ) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('test', (selector) => - selector.split(',').map((selector) => `&.${selector} > *`), - ) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['test-[a,b,c]:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .test-\\[a\\,b\\,c\\]\\:underline.a > *, .test-\\[a\\,b\\,c\\]\\:underline.b > *, .test-\\[a\\,b\\,c\\]\\:underline.c > * { text-decoration-line: underline; } - } - " + }" `) }) test('should be possible to sort variants', async () => { - expect( - await run( - ['testmin-[600px]:flex', 'testmin-[500px]:underline', 'testmin-[700px]:italic'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build([ + 'testmin-[600px]:flex', + 'testmin-[500px]:underline', + 'testmin-[700px]:italic', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (min-width: 500px) { .testmin-\\[500px\\]\\:underline { text-decoration-line: underline; @@ -2267,41 +2157,45 @@ describe('matchVariant', () => { font-style: italic; } } - } - " + }" `) }) test('should be possible to compare arbitrary variants and hardcoded variants', async () => { - expect( - await run( - ['testmin-[700px]:italic', 'testmin-example:italic', 'testmin-[500px]:italic'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + values: { + example: '600px', + }, + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - values: { example: '600px' }, - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build([ + 'testmin-[700px]:italic', + 'testmin-example:italic', + 'testmin-[500px]:italic', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (min-width: 500px) { .testmin-\\[500px\\]\\:italic { font-style: italic; @@ -2319,51 +2213,50 @@ describe('matchVariant', () => { font-style: italic; } } - } - " + }" `) }) test('should be possible to sort stacked arbitrary variants correctly', async () => { - expect( - await run( - [ - 'testmin-[150px]:testmax-[400px]:order-2', - 'testmin-[100px]:testmax-[350px]:order-3', - 'testmin-[100px]:testmax-[300px]:order-4', - 'testmin-[100px]:testmax-[400px]:order-1', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) - }, - } - }, + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + + let compiled = build([ + 'testmin-[150px]:testmax-[400px]:order-2', + 'testmin-[100px]:testmax-[350px]:order-3', + 'testmin-[100px]:testmax-[300px]:order-4', + 'testmin-[100px]:testmax-[400px]:order-1', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (min-width: 100px) { @media (max-width: 400px) { .testmin-\\[100px\\]\\:testmax-\\[400px\\]\\:order-1 { @@ -2393,50 +2286,48 @@ describe('matchVariant', () => { } } } - } - " + }" `) }) test('should maintain sort from other variants, if sort functions of arbitrary variants return 0', async () => { - // Expect :focus to come after :hover - expect( - await run( - [ - 'testmin-[100px]:testmax-[200px]:focus:underline', - 'testmin-[100px]:testmax-[200px]:hover:underline', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) - }, - } - }, + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build([ + 'testmin-[100px]:testmax-[200px]:focus:underline', + 'testmin-[100px]:testmax-[200px]:hover:underline', + ]) + + // Expect :focus to come after :hover + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (min-width: 100px) { @media (max-width: 200px) { @media (hover: hover) { @@ -2450,50 +2341,48 @@ describe('matchVariant', () => { } } } - } - " + }" `) }) test('should sort arbitrary variants left to right (1)', async () => { - expect( - await run( - [ - 'testmin-[200px]:testmax-[400px]:order-2', - 'testmin-[200px]:testmax-[300px]:order-4', - 'testmin-[100px]:testmax-[400px]:order-1', - 'testmin-[100px]:testmax-[300px]:order-3', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build([ + 'testmin-[200px]:testmax-[400px]:order-2', + 'testmin-[200px]:testmax-[300px]:order-4', + 'testmin-[100px]:testmax-[400px]:order-1', + 'testmin-[100px]:testmax-[300px]:order-3', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (min-width: 100px) { @media (max-width: 400px) { .testmin-\\[100px\\]\\:testmax-\\[400px\\]\\:order-1 { @@ -2525,50 +2414,48 @@ describe('matchVariant', () => { } } } - } - " + }" `) }) test('should sort arbitrary variants left to right (2)', async () => { - expect( - await run( - [ - 'testmax-[400px]:testmin-[200px]:underline', - 'testmax-[300px]:testmin-[200px]:underline', - 'testmax-[400px]:testmin-[100px]:underline', - 'testmax-[300px]:testmin-[100px]:underline', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build([ + 'testmax-[400px]:testmin-[200px]:underline', + 'testmax-[300px]:testmin-[200px]:underline', + 'testmax-[400px]:testmin-[100px]:underline', + 'testmax-[300px]:testmin-[100px]:underline', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (max-width: 400px) { @media (min-width: 100px) { .testmax-\\[400px\\]\\:testmin-\\[100px\\]\\:underline { @@ -2596,58 +2483,56 @@ describe('matchVariant', () => { } } } - } - " + }" `) }) test('should guarantee that we are not passing values from other variants to the wrong function', async () => { - expect( - await run( - [ - 'testmin-[200px]:testmax-[400px]:order-2', - 'testmin-[200px]:testmax-[300px]:order-4', - 'testmin-[100px]:testmax-[400px]:order-1', - 'testmin-[100px]:testmax-[300px]:order-3', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + let lookup = ['100px', '200px'] + if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { + throw new Error('We are seeing values that should not be there!') + } + return lookup.indexOf(a.value) - lookup.indexOf(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + let lookup = ['300px', '400px'] + if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { + throw new Error('We are seeing values that should not be there!') + } + return lookup.indexOf(z.value) - lookup.indexOf(a.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - let lookup = ['100px', '200px'] - if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { - throw new Error('We are seeing values that should not be there!') - } - return lookup.indexOf(a.value) - lookup.indexOf(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - let lookup = ['300px', '400px'] - if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { - throw new Error('We are seeing values that should not be there!') - } - return lookup.indexOf(z.value) - lookup.indexOf(a.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build([ + 'testmin-[200px]:testmax-[400px]:order-2', + 'testmin-[200px]:testmax-[300px]:order-4', + 'testmin-[100px]:testmax-[400px]:order-1', + 'testmin-[100px]:testmax-[300px]:order-3', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (min-width: 100px) { @media (max-width: 400px) { .testmin-\\[100px\\]\\:testmax-\\[400px\\]\\:order-1 { @@ -2679,178 +2564,170 @@ describe('matchVariant', () => { } } } - } - " + }" `) }) test('should default to the DEFAULT value for variants', async () => { - expect( - await run( - ['foo:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value} &`, { + values: { + DEFAULT: '.bar', + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`, { - values: { DEFAULT: '.bar' }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['foo:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo.bar .foo\\:underline { text-decoration-line: underline; } - } - " + }" `) }) test('should not generate anything if the matchVariant does not have a DEFAULT value configured', async () => { - expect( - await run( - ['foo:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value} &`) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities; - " - `) + }, + ) + let compiled = build(['foo:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`"@layer utilities;"`) }) test('should be possible to use `null` as a DEFAULT value', async () => { - expect( - await run( - ['foo:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { + values: { DEFAULT: null }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { - values: { DEFAULT: null }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['foo:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo-good .foo\\:underline { text-decoration-line: underline; } - } - " + }" `) }) test('should be possible to use `undefined` as a DEFAULT value', async () => { - expect( - await run( - ['foo:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { + values: { DEFAULT: undefined }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { - values: { DEFAULT: undefined }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['foo:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo-good .foo\\:underline { text-decoration-line: underline; } - } - " + }" `) }) test('should be called with eventual modifiers', async () => { - expect( - await run( - ['my-container-[250px]:underline', 'my-container-[250px]/placement:underline'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('my-container', (value, { modifier }) => { - function parseValue(value: string) { - const numericValue = value.match(/^(\d+\.\d+|\d+|\.\d+)\D+/)?.[1] ?? null - if (numericValue === null) return null + let { build } = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('my-container', (value, { modifier }) => { + function parseValue(value: string) { + const numericValue = value.match(/^(\d+\.\d+|\d+|\.\d+)\D+/)?.[1] ?? null + if (numericValue === null) return null - return parseFloat(value) - } + return parseFloat(value) + } - const parsed = parseValue(value) - return parsed !== null ? `@container ${modifier ?? ''} (min-width: ${value})` : [] - }) - }, - } - }, + const parsed = parseValue(value) + return parsed !== null ? `@container ${modifier ?? ''} (min-width: ${value})` : [] + }) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - @container (min-width: 250px) { + }, + ) + let compiled = build([ + 'my-container-[250px]:underline', + 'my-container-[250px]/placement:underline', + ]) + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@container (min-width: 250px) { .my-container-\\[250px\\]\\:underline { text-decoration-line: underline; } @@ -2860,372 +2737,347 @@ describe('matchVariant', () => { .my-container-\\[250px\\]\\/placement\\:underline { text-decoration-line: underline; } - } - " + }" `) }) test('ignores variants that use :merge(…)', async () => { - expect( - await run( - [ - 'optional-[test]:flex', - 'group-optional-[test]:flex', - 'peer-optional-[test]:flex', - 'group-optional-[test]/foo:flex', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('optional', (flavor) => `&:optional:has(${flavor}) &`) + matchVariant('group-optional', (flavor) => `:merge(.group):optional:has(${flavor}) &`) + matchVariant('peer-optional', (flavor) => `:merge(.peer):optional:has(${flavor}) ~ &`) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('optional', (flavor) => `&:optional:has(${flavor}) &`) - matchVariant( - 'group-optional', - (flavor) => `:merge(.group):optional:has(${flavor}) &`, - ) - matchVariant( - 'peer-optional', - (flavor) => `:merge(.peer):optional:has(${flavor}) ~ &`, - ) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group):optional:has(test) :where(.group) .group-optional-\\[test\\]\\:flex), :is(:where(.group\\/foo):optional:has(test) :where(.group\\/foo) .group-optional-\\[test\\]\\/foo\\:flex), :is(:where(.peer):optional:has(test) :where(.peer) ~ .peer-optional-\\[test\\]\\:flex), .optional-\\[test\\]\\:flex:optional:has(test) .optional-\\[test\\]\\:flex { + }, + ) + let compiled = build([ + 'optional-[test]:flex', + 'group-optional-[test]:flex', + 'peer-optional-[test]:flex', + 'group-optional-[test]/foo:flex', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-optional-\\[test\\]\\:flex:is(:where(.group):optional:has(test) :where(.group) *), .group-optional-\\[test\\]\\/foo\\:flex:is(:where(.group\\/foo):optional:has(test) :where(.group\\/foo) *), .peer-optional-\\[test\\]\\:flex:is(:where(.peer):optional:has(test) :where(.peer) ~ *), .optional-\\[test\\]\\:flex:optional:has(test) .optional-\\[test\\]\\:flex { display: flex; } - } - " + }" `) }) test('ignores variants that use unknown values', async () => { - expect( - await run( - ['foo-[test]:flex', 'foo-known:flex', 'foo-unknown:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (flavor) => `&:is(${flavor})`, { + values: { + known: 'known', + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (flavor) => `&:is(${flavor})`, { - values: { known: 'known' }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + + let compiled = build(['foo-[test]:flex', 'foo-known:flex', 'foo-unknown:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo-known\\:flex:is(known), .foo-\\[test\\]\\:flex:is(test) { display: flex; } - } - " + }" `) }) test('ignores variants that produce non-string values', async () => { - expect( - await run( - ['foo-[test]:flex', 'foo-string:flex', 'foo-object:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (flavor) => `&:is(${flavor})`, { + values: { + string: 'some string', + object: { some: 'object' }, + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (flavor) => `&:is(${flavor})`, { - values: { - string: 'some string', - object: { some: 'object' }, - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + + let compiled = build(['foo-[test]:flex', 'foo-string:flex', 'foo-object:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo-string\\:flex:is(some string), .foo-\\[test\\]\\:flex:is(test) { display: flex; } - } - " + }" `) }) }) describe('addUtilities()', () => { test('custom static utility', async () => { - expect( - await run( - ['text-trim', 'lg:text-trim'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - - @theme reference { - --breakpoint-lg: 1024px; - } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, - }) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - .text-trim { - text-box-trim: both; - text-box-edge: cap alphabetic; + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } - @media (min-width: 1024px) { - .lg\\:text-trim { + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, + }) + }, + } + }, + }, + ) + + expect(optimizeCss(compiled.build(['text-trim', 'lg:text-trim'])).trim()) + .toMatchInlineSnapshot(` + "@layer utilities { + .text-trim { text-box-trim: both; text-box-edge: cap alphabetic; } - } - } - " - `) + + @media (min-width: 1024px) { + .lg\\:text-trim { + text-box-trim: both; + text-box-edge: cap alphabetic; + } + } + }" + `) }) test('return multiple rule objects from a custom utility', async () => { - expect( - await run( - ['text-trim'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.text-trim': [ - { 'text-box-trim': 'both' }, - { 'text-box-edge': 'cap alphabetic' }, - ], - }, - ]) - }, - } - }, + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.text-trim': [ + { 'text-box-trim': 'both' }, + { 'text-box-edge': 'cap alphabetic' }, + ], + }, + ]) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .text-trim { + }, + ) + + expect(optimizeCss(compiled.build(['text-trim'])).trim()).toMatchInlineSnapshot(` + ".text-trim { text-box-trim: both; text-box-edge: cap alphabetic; - } - " + }" `) }) test('define multiple utilities with array syntax', async () => { - expect( - await run( - ['text-trim', 'text-trim-2'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.text-trim': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.text-trim': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', }, - { - '.text-trim-2': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, + }, + { + '.text-trim-2': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', }, - ]) - }, - } - }, + }, + ]) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .text-trim, .text-trim-2 { + }, + ) + + expect(optimizeCss(compiled.build(['text-trim', 'text-trim-2'])).trim()).toMatchInlineSnapshot(` + ".text-trim, .text-trim-2 { text-box-trim: both; text-box-edge: cap alphabetic; - } - " + }" `) }) test('utilities can use arrays for fallback declaration values', async () => { - expect( - await run( - ['outlined'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.outlined': { - outline: ['1px solid ButtonText', '1px auto -webkit-focus-ring-color'], - }, + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.outlined': { + outline: ['1px solid ButtonText', '1px auto -webkit-focus-ring-color'], }, - ]) - }, - } - }, + }, + ]) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .outlined { + }, + ) + + expect(optimizeCss(compiled.build(['outlined'])).trim()).toMatchInlineSnapshot(` + ".outlined { outline: 1px solid buttontext; outline: 1px auto -webkit-focus-ring-color; - } - " + }" `) }) test('camel case properties are converted to kebab-case', async () => { - expect( - await run( - ['text-trim'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim': { + WebkitAppearance: 'none', + textBoxTrim: 'both', + textBoxEdge: 'cap alphabetic', + }, + }) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim': { - WebkitAppearance: 'none', - textBoxTrim: 'both', - textBoxEdge: 'cap alphabetic', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + + expect(optimizeCss(compiled.build(['text-trim'])).trim()).toMatchInlineSnapshot(` + "@layer utilities { .text-trim { -webkit-appearance: none; text-box-trim: both; text-box-edge: cap alphabetic; } - } - " + }" `) }) test('custom static utilities support `@apply`', async () => { - expect( - await run( - ['foo', 'lg:foo'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } - @theme reference { - --breakpoint-lg: 1024px; + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.foo': { + '@apply flex dark:underline': {}, + }, + }) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.foo': { - '@apply flex dark:underline': {}, - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + + expect(optimizeCss(compiled.build(['foo', 'lg:foo'])).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo { display: flex; } @@ -3247,8 +3099,7 @@ describe('addUtilities()', () => { } } } - } - " + }" `) }) @@ -3266,7 +3117,7 @@ describe('addUtilities()', () => { } `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -3282,336 +3133,399 @@ describe('addUtilities()', () => { }, }, ) - }).rejects.toThrow(/invalid utility selector/) + }).rejects.toThrowError(/invalid utility selector/) }) test('supports multiple selector names', async () => { - expect( - await run( - ['form-input', 'lg:form-textarea'], - css` - @plugin "my-plugin"; - @tailwind utilities; + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.form-input, .form-textarea': { + appearance: 'none', + 'background-color': '#fff', + }, + }) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-textarea': { - appearance: 'none', - 'background-color': '#fff', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - .form-input { - appearance: none; - background-color: #fff; - } + }, + ) - @media (min-width: 1024px) { - .lg\\:form-textarea { + expect(optimizeCss(compiled.build(['form-input', 'lg:form-textarea'])).trim()) + .toMatchInlineSnapshot(` + ".form-input { appearance: none; background-color: #fff; } - } - " - `) + + @media (min-width: 1024px) { + .lg\\:form-textarea { + appearance: none; + background-color: #fff; + } + }" + `) }) test('supports pseudo classes and pseudo elements', async () => { - expect( - await run( - ['form-input', 'lg:form-textarea'], - css` - @plugin "my-plugin"; - @tailwind utilities; + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.form-input, .form-input::placeholder, .form-textarea:hover:focus': { + 'background-color': 'red', + }, + }) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-input::placeholder, .form-textarea:hover:focus': { - 'background-color': 'red', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - .form-input, .form-input::placeholder { - background-color: red; - } + }, + ) - @media (min-width: 1024px) { - .lg\\:form-textarea:hover:focus { + expect(compiled.build(['form-input', 'lg:form-textarea']).trim()).toMatchInlineSnapshot(` + ".form-input { + background-color: red; + &::placeholder { background-color: red; } } - " + .lg\\:form-textarea { + @media (width >= 1024px) { + &:hover:focus { + background-color: red; + } + } + }" `) }) test('nests complex utility names', async () => { - expect( - await run( - ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.a .b:hover .c': { color: 'red' }, - '.d > *': { color: 'red' }, - '.e .bar:not(.f):has(.g)': { color: 'red' }, - '.h~.i': { color: 'red' }, - '.j.j': { color: 'red' }, - }) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - .j.j, .a .b:hover .c, .d > *, .e .bar:not(.f):has(.g), .h ~ .i { - color: red; + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } - } - " - `) - }) + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.a .b:hover .c': { + color: 'red', + }, + '.d > *': { + color: 'red', + }, + '.e .bar:not(.f):has(.g)': { + color: 'red', + }, + '.h~.i': { + color: 'red', + }, + '.j.j': { + color: 'red', + }, + }) + }, + } + }, + }, + ) - test('does not register class names inside `:nth-child(… of …)` as utilities', async () => { expect( - await run( - ['foo', 'mark'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + compiled.build(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']).trim(), + ).toMatchInlineSnapshot( + ` + "@layer utilities { + .j { + &.j { + color: red; + } + .j& { + color: red; } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.foo:nth-child(2 of .mark)': { color: 'red' }, - }) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - .foo:nth-child(2 of .mark) { - color: red; } - } - " - `) + .a { + & .b:hover .c { + color: red; + } + } + .b { + .a &:hover .c { + color: red; + } + } + .c { + .a .b:hover & { + color: red; + } + } + .d { + & > * { + color: red; + } + } + .e { + & .bar:not(.f):has(.g) { + color: red; + } + } + .g { + .e .bar:not(.f):has(&) { + color: red; + } + } + .h { + &~.i { + color: red; + } + } + .i { + .h~& { + color: red; + } + } + }" + `, + ) }) test('prefixes nested class names with the configured theme prefix', async () => { - expect( - await run( - ['tw:a', 'tw:b', 'tw:c', 'tw:d'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - @theme prefix(tw) { - } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ '.a .b:hover .c.d': { color: 'red' } }) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - .tw\\:a .tw\\:b:hover .tw\\:c.tw\\:d { - color: red; + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } - } - " - `) + @theme prefix(tw) { + } + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.a .b:hover .c.d': { + color: 'red', + }, + }) + }, + } + }, + }, + ) + + expect(compiled.build(['tw:a', 'tw:b', 'tw:c', 'tw:d']).trim()).toMatchInlineSnapshot( + ` + "@layer utilities { + .tw\\:a { + & .tw\\:b:hover .tw\\:c.tw\\:d { + color: red; + } + } + .tw\\:b { + .tw\\:a &:hover .tw\\:c.tw\\:d { + color: red; + } + } + .tw\\:c { + .tw\\:a .tw\\:b:hover &.tw\\:d { + color: red; + } + } + .tw\\:d { + .tw\\:a .tw\\:b:hover .tw\\:c& { + color: red; + } + } + }" + `, + ) }) test('replaces the class name with variants in nested selectors', async () => { - expect( - await run( - ['foo', 'md:foo', 'not-hover:md:foo'], - css` - @plugin "my-plugin"; - @theme { - --breakpoint-md: 768px; + let compiled = await compile( + css` + @plugin "my-plugin"; + @theme { + --breakpoint-md: 768px; + } + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.foo': { + ':where(.foo > :first-child)': { + color: 'red', + }, + }, + }) + }, } - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ '.foo': { ':where(.foo > :first-child)': { color: 'red' } } }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - .foo :where(.foo > :first-child) { - color: red; - } + }, + ) - @media (min-width: 768px) { - .md\\:foo :where(.md\\:foo > :first-child), .not-hover\\:md\\:foo:not(:hover) :where(.not-hover\\:md\\:foo > :first-child) { + expect(compiled.build(['foo', 'md:foo', 'not-hover:md:foo']).trim()).toMatchInlineSnapshot(` + ".foo { + :where(.foo > :first-child) { color: red; } } - - @media not all and (hover: hover) { - @media (min-width: 768px) { - .not-hover\\:md\\:foo :where(.not-hover\\:md\\:foo > :first-child) { + .md\\:foo { + @media (width >= 768px) { + :where(.md\\:foo > :first-child) { color: red; } } } - " + .not-hover\\:md\\:foo { + &:not(*:hover) { + @media (width >= 768px) { + :where(.not-hover\\:md\\:foo > :first-child) { + color: red; + } + } + } + @media not (hover: hover) { + @media (width >= 768px) { + :where(.not-hover\\:md\\:foo > :first-child) { + color: red; + } + } + } + }" `) }) test('values that are `false`, `null`, or `undefined` are discarded from CSS object ASTs', async () => { - expect( - await run( - ['foo'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.foo': { - a: 'red', - // @ts-ignore: While this isn't valid per the types this did work in v3 - 'z-index': 0, - // @ts-ignore - '.bar': false, - // @ts-ignore - '.baz': null, - // @ts-ignore - '.qux': undefined, - }, - }) - }, - } - }, + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.foo': { + a: 'red', + // @ts-ignore: While this isn't valid per the types this did work in v3 + 'z-index': 0, + // @ts-ignore + '.bar': false, + // @ts-ignore + '.baz': null, + // @ts-ignore + '.qux': undefined, + }, + }) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .foo { + }, + ) + + expect(compiled.build(['foo']).trim()).toMatchInlineSnapshot(` + ".foo { a: red; z-index: 0; - } - " + }" `) }) }) describe('matchUtilities()', () => { test('custom functional utility', async () => { - let input = css` - @plugin "my-plugin"; + async function run(candidates: string[]) { + let compiled = await compile( + css` + @plugin "my-plugin"; - @tailwind utilities; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; - } - ` + @theme reference { + --breakpoint-lg: 1024px; + } + `, - expect( - await run( - [ - 'border-block', - 'border-block-2', - 'border-block-[35px]', - 'border-block-[var(--foo)]', - 'lg:border-block-2', - ], - input, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( - { 'border-block': (value) => ({ 'border-block-width': value }) }, - { values: { DEFAULT: '1px', '2': '2px' } }, + { + 'border-block': (value) => ({ 'border-block-width': value }), + }, + { + values: { + DEFAULT: '1px', + '2': '2px', + }, + }, ) }, } }, }, - ), + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run([ + 'border-block', + 'border-block-2', + 'border-block-[35px]', + 'border-block-[var(--foo)]', + 'lg:border-block-2', + ]), + ).trim(), ).toMatchInlineSnapshot(` - " - .border-block { + ".border-block { border-block-width: 1px; } @@ -3631,61 +3545,55 @@ describe('matchUtilities()', () => { .lg\\:border-block-2 { border-block-width: 2px; } - } - " + }" `) expect( - await run( - [ + optimizeCss( + await run([ '-border-block', '-border-block-2', 'lg:-border-block-2', 'border-block-unknown', 'border-block/1', - ], - input, + ]), + ).trim(), + ).toEqual('') + }) + + test('custom functional utilities can start with @', async () => { + async function run(candidates: string[]) { + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( - { 'border-block': (value) => ({ 'border-block-width': value }) }, - { values: { DEFAULT: '1px', '2': '2px' } }, + { '@w': (value) => ({ width: value }) }, + { + values: { + 1: '1px', + }, + }, ) }, } }, }, - ), - ).toEqual('') - }) + ) - test('custom functional utilities can start with @', async () => { - expect( - await run( - ['@w-1', 'hover:@w-1'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities({ '@w': (value) => ({ width: value }) }, { values: { 1: '1px' } }) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .\\@w-1 { + return compiled.build(candidates) + } + + expect(optimizeCss(await run(['@w-1', 'hover:@w-1'])).trim()).toMatchInlineSnapshot(` + ".\\@w-1 { width: 1px; } @@ -3693,56 +3601,56 @@ describe('matchUtilities()', () => { .hover\\:\\@w-1:hover { width: 1px; } - } - " + }" `) }) test('custom functional utilities can return an array of rules', async () => { - expect( - await run( - ['all-but-order-bottom-left-radius'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'all-but-order-bottom-left-radius': (value) => - [ - { 'border-top-left-radius': value }, - { 'border-top-right-radius': value }, - { 'border-bottom-right-radius': value }, - ] as CssInJs[], + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'all-but-order-bottom-left-radius': (value) => + [ + { 'border-top-left-radius': value }, + { 'border-top-right-radius': value }, + { 'border-bottom-right-radius': value }, + ] as CssInJs[], + }, + { + values: { + DEFAULT: '1px', }, - { values: { DEFAULT: '1px' } }, - ) - }, - } - }, + }, + ) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .all-but-order-bottom-left-radius { - border-top-left-radius: 1px; - border-top-right-radius: 1px; - border-bottom-right-radius: 1px; - } - " - `) + }, + ) + + expect(optimizeCss(compiled.build(['all-but-order-bottom-left-radius'])).trim()) + .toMatchInlineSnapshot(` + ".all-but-order-bottom-left-radius { + border-top-left-radius: 1px; + border-top-right-radius: 1px; + border-bottom-right-radius: 1px; + }" + `) }) test('custom functional utility with any modifier', async () => { - expect( - await run( - ['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo'], + async function run(candidates: string[]) { + let compiled = await compile( css` @plugin "my-plugin"; @@ -3752,8 +3660,9 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -3765,16 +3674,30 @@ describe('matchUtilities()', () => { 'border-block-width': value, }), }, - { values: { DEFAULT: '1px', '2': '2px' }, modifiers: 'any' }, + { + values: { + DEFAULT: '1px', + '2': '2px', + }, + + modifiers: 'any', + }, ) }, } }, }, - ), + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']), + ).trim(), ).toMatchInlineSnapshot(` - " - .border-block { + ".border-block { --my-modifier: none; border-block-width: 1px; } @@ -3792,28 +3715,25 @@ describe('matchUtilities()', () => { .border-block\\/foo { --my-modifier: foo; border-block-width: 1px; - } - " + }" `) }) test('custom functional utility with known modifier', async () => { - let input = css` - @plugin "my-plugin"; + async function run(candidates: string[]) { + let compiled = await compile( + css` + @plugin "my-plugin"; - @tailwind utilities; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; - } - ` + @theme reference { + --breakpoint-lg: 1024px; + } + `, - expect( - await run( - ['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo'], - input, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -3826,18 +3746,31 @@ describe('matchUtilities()', () => { }), }, { - values: { DEFAULT: '1px', '2': '2px' }, - modifiers: { foo: 'foo' }, + values: { + DEFAULT: '1px', + '2': '2px', + }, + + modifiers: { + foo: 'foo', + }, }, ) }, } }, }, - ), + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']), + ).trim(), ).toMatchInlineSnapshot(` - " - .border-block { + ".border-block { --my-modifier: none; border-block-width: 1px; } @@ -3855,33 +3788,11 @@ describe('matchUtilities()', () => { .border-block\\/foo { --my-modifier: foo; border-block-width: 1px; - } - " + }" `) expect( - await run(['border-block/unknown', 'border-block-2/unknown'], input, { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value, { modifier }) => ({ - '--my-modifier': modifier ?? 'none', - 'border-block-width': value, - }), - }, - { - values: { DEFAULT: '1px', '2': '2px' }, - modifiers: { foo: 'foo' }, - }, - ) - }, - } - }, - }), + optimizeCss(await run(['border-block/unknown', 'border-block-2/unknown'])).trim(), ).toEqual('') }) @@ -3889,127 +3800,194 @@ describe('matchUtilities()', () => { // future don't be afraid to change what should happen in this scenario. describe('plugins that handle a specific arbitrary value type prevent falling through to other plugins if the result is invalid for that plugin', () => { test('implicit color modifier', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let options: Partial[1]> = { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { type: ['color', 'any'] }, - ) - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-width': value }) }, - { type: ['length'] }, - ) - }, - } - }, - } - - expect( - await run(['scrollbar-[2px]', 'scrollbar-[#08c]', 'scrollbar-[#08c]/50'], input, options), - ).toMatchInlineSnapshot(` - " - .scrollbar-\\[2px\\] { - scrollbar-width: 2px; - } - - .scrollbar-\\[\\#08c\\] { - scrollbar-color: #08c; - } - - .scrollbar-\\[\\#08c\\]\\/50 { - scrollbar-color: oklab(59.9824% -.067 -.124 / .5); - } - " - `) - expect(await run(['scrollbar-[2px]/50'], input, options)).toEqual('') - }) - - test('no modifiers are supported by the plugins', async () => { - expect( - await run( - ['scrollbar-[2px]/50'], + async function run(candidates: string[]) { + let compiled = await compile( css` @tailwind utilities; @plugin "my-plugin"; `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( - { scrollbar: (value) => ({ '--scrollbar-angle': value }) }, - { type: ['angle', 'any'] }, + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { type: ['color', 'any'] }, ) matchUtilities( - { scrollbar: (value) => ({ '--scrollbar-width': value }) }, + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, { type: ['length'] }, ) }, } }, }, - ), - ).toEqual('') + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run(['scrollbar-[2px]', 'scrollbar-[#08c]', 'scrollbar-[#08c]/50']), + ).trim(), + ).toMatchInlineSnapshot(` + ".scrollbar-\\[\\#08c\\] { + scrollbar-color: #08c; + } + + .scrollbar-\\[\\#08c\\]\\/50 { + scrollbar-color: oklab(59.9824% -.067 -.124 / .5); + } + + .scrollbar-\\[2px\\] { + scrollbar-width: 2px; + }" + `) + expect(optimizeCss(await run(['scrollbar-[2px]/50'])).trim()).toEqual('') }) - test('invalid named modifier', async () => { - expect( - await run( - ['scrollbar-[2px]/foo'], + test('no modifiers are supported by the plugins', async () => { + async function run(candidates: string[]) { + let compiled = await compile( css` @tailwind utilities; @plugin "my-plugin"; `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, + { + scrollbar: (value) => ({ '--scrollbar-angle': value }), + }, + { type: ['angle', 'any'] }, + ) + + matchUtilities( + { + scrollbar: (value) => ({ '--scrollbar-width': value }), + }, + { type: ['length'] }, + ) + }, + } + }, + }, + ) + + return compiled.build(candidates) + } + + expect(optimizeCss(await run(['scrollbar-[2px]/50'])).trim()).toEqual('') + }) + + test('invalid named modifier', async () => { + async function run(candidates: string[]) { + let compiled = await compile( + css` + @tailwind utilities; + @plugin "my-plugin"; + `, + + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, { type: ['color', 'any'], modifiers: { foo: 'foo' } }, ) matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-width': value }) }, + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, { type: ['length'], modifiers: { bar: 'bar' } }, ) }, } }, }, - ), - ).toEqual('') + ) + + return compiled.build(candidates) + } + + expect(optimizeCss(await run(['scrollbar-[2px]/foo'])).trim()).toEqual('') }) }) test('custom functional utilities with different types', async () => { - let input = css` - @plugin "my-plugin"; + async function run(candidates: string[]) { + let compiled = await compile( + css` + @plugin "my-plugin"; - @tailwind utilities; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; - } - ` + @theme reference { + --breakpoint-lg: 1024px; + } + `, + + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { + type: ['color', 'any'], + values: { + black: 'black', + }, + }, + ) + + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, + { + type: ['length'], + values: { + 2: '2px', + }, + }, + ) + }, + } + }, + }, + ) + + return compiled.build(candidates) + } expect( - await run( - [ + optimizeCss( + await run([ 'scrollbar-black', 'scrollbar-black/50', 'scrollbar-2', @@ -4021,38 +3999,13 @@ describe('matchUtilities()', () => { 'scrollbar-[color:var(--my-color)]', 'scrollbar-[color:var(--my-color)]/50', 'scrollbar-[length:var(--my-width)]', - ], - input, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { type: ['color', 'any'], values: { black: 'black' } }, - ) - - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-width': value }) }, - { type: ['length'], values: { 2: '2px' } }, - ) - }, - } - }, - }, - ), + ]), + ).trim(), ).toMatchInlineSnapshot(` - " - .scrollbar-2, .scrollbar-\\[2px\\] { + ".scrollbar-2 { scrollbar-width: 2px; } - .scrollbar-\\[length\\:var\\(--my-width\\)\\] { - scrollbar-width: var(--my-width); - } - .scrollbar-\\[\\#fff\\] { scrollbar-color: #fff; } @@ -4061,6 +4014,10 @@ describe('matchUtilities()', () => { scrollbar-color: oklab(100% 0 5.96046e-8 / .5); } + .scrollbar-\\[2px\\] { + scrollbar-width: 2px; + } + .scrollbar-\\[color\\:var\\(--my-color\\)\\], .scrollbar-\\[color\\:var\\(--my-color\\)\\]\\/50 { scrollbar-color: var(--my-color); } @@ -4071,6 +4028,10 @@ describe('matchUtilities()', () => { } } + .scrollbar-\\[length\\:var\\(--my-width\\)\\] { + scrollbar-width: var(--my-width); + } + .scrollbar-\\[var\\(--my-color\\)\\], .scrollbar-\\[var\\(--my-color\\)\\]\\/50 { scrollbar-color: var(--my-color); } @@ -4087,48 +4048,23 @@ describe('matchUtilities()', () => { .scrollbar-black\\/50 { scrollbar-color: oklab(0% none none / .5); - } - " + }" `) expect( - await run( - ['scrollbar-2/50', 'scrollbar-[2px]/50', 'scrollbar-[length:var(--my-width)]/50'], - input, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { type: ['color', 'any'], values: { black: 'black' } }, - ) - - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-width': value }) }, - { type: ['length'], values: { 2: '2px' } }, - ) - }, - } - }, - }, - ), + optimizeCss( + await run([ + 'scrollbar-2/50', + 'scrollbar-[2px]/50', + 'scrollbar-[length:var(--my-width)]/50', + ]), + ).trim(), ).toEqual('') }) test('functional utilities with `type: color` automatically support opacity', async () => { - expect( - await run( - [ - 'scrollbar-current', - 'scrollbar-current/45', - 'scrollbar-black', - 'scrollbar-black/33', - 'scrollbar-black/[50%]', - 'scrollbar-[var(--my-color)]/[25%]', - ], + async function run(candidates: string[]) { + let compiled = await compile( css` @plugin "my-plugin"; @@ -4138,24 +4074,46 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { type: ['color', 'any'], values: { black: 'black' } }, + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { + type: ['color', 'any'], + values: { + black: 'black', + }, + }, ) }, } }, }, - ), + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run([ + 'scrollbar-current', + 'scrollbar-current/45', + 'scrollbar-black', + 'scrollbar-black/33', + 'scrollbar-black/[50%]', + 'scrollbar-[var(--my-color)]/[25%]', + ]), + ).trim(), ).toMatchInlineSnapshot(` - " - .scrollbar-\\[var\\(--my-color\\)\\]\\/\\[25\\%\\] { + ".scrollbar-\\[var\\(--my-color\\)\\]\\/\\[25\\%\\] { scrollbar-color: var(--my-color); } @@ -4185,15 +4143,13 @@ describe('matchUtilities()', () => { .scrollbar-current\\/45 { scrollbar-color: color-mix(in oklab, currentcolor 45%, transparent); } - } - " + }" `) }) test('functional utilities with explicit modifiers', async () => { - expect( - await run( - ['scrollbar-[12px]', 'scrollbar-[12px]/foo', 'scrollbar-[12px]/bar'], + async function run(candidates: string[]) { + let compiled = await compile( css` @plugin "my-plugin"; @@ -4204,8 +4160,9 @@ describe('matchUtilities()', () => { --opacity-my-opacity: 0.5; } `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -4217,16 +4174,29 @@ describe('matchUtilities()', () => { 'scrollbar-width': value, }), }, - { type: ['any'], values: {}, modifiers: { foo: 'foo' } }, + { + type: ['any'], + values: {}, + modifiers: { + foo: 'foo', + }, + }, ) }, } }, }, - ), + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run(['scrollbar-[12px]', 'scrollbar-[12px]/foo', 'scrollbar-[12px]/bar']), + ).trim(), ).toMatchInlineSnapshot(` - " - .scrollbar-\\[12px\\] { + ".scrollbar-\\[12px\\] { --modifier: none; scrollbar-width: 12px; } @@ -4234,43 +4204,51 @@ describe('matchUtilities()', () => { .scrollbar-\\[12px\\]\\/foo { --modifier: foo; scrollbar-width: 12px; - } - " + }" `) }) test('functional utilities support `@apply`', async () => { - expect( - await run( - ['foo-bar', 'lg:foo-bar', 'foo-[12px]', 'lg:foo-[12px]'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } - @theme reference { - --breakpoint-lg: 1024px; + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + foo: (value) => ({ + '--foo': value, + [`@apply flex`]: {}, + }), + }, + { + values: { + bar: 'bar', + }, + }, + ) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { foo: (value) => ({ '--foo': value, [`@apply flex`]: {} }) }, - { values: { bar: 'bar' } }, - ) - }, - } - }, }, - ), + }, + ) + + expect( + optimizeCss(compiled.build(['foo-bar', 'lg:foo-bar', 'foo-[12px]', 'lg:foo-[12px]'])).trim(), ).toMatchInlineSnapshot(` - " - @layer utilities { + "@layer utilities { .foo-\\[12px\\] { --foo: 12px; display: flex; @@ -4292,8 +4270,7 @@ describe('matchUtilities()', () => { display: flex; } } - } - " + }" `) }) @@ -4312,7 +4289,7 @@ describe('matchUtilities()', () => { `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -4328,179 +4305,202 @@ describe('matchUtilities()', () => { }, }, ) - }).rejects.toThrow(/invalid utility name/) + }).rejects.toThrowError(/invalid utility name/) }) test('replaces the class name with variants in nested selectors', async () => { - expect( - await run( - ['foo-red', 'md:foo-red', 'not-hover:md:foo-red'], - css` - @plugin "my-plugin"; - @theme { - --breakpoint-md: 768px; - } - @tailwind utilities; - `, - { - async loadModule(base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { foo: (value) => ({ ':where(.foo > :first-child)': { color: value } }) }, - { values: { red: 'red' } }, - ) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .foo-red :where(.foo-red > :first-child) { - color: red; - } - - @media (min-width: 768px) { - .md\\:foo-red :where(.md\\:foo-red > :first-child), .not-hover\\:md\\:foo-red:not(:hover) :where(.not-hover\\:md\\:foo-red > :first-child) { - color: red; + let compiled = await compile( + css` + @plugin "my-plugin"; + @theme { + --breakpoint-md: 768px; } - } + @tailwind utilities; + `, + { + async loadModule(base) { + return { + path: '', + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + foo: (value) => ({ + ':where(.foo > :first-child)': { + color: value, + }, + }), + }, + { + values: { + red: 'red', + }, + }, + ) + }, + } + }, + }, + ) - @media not all and (hover: hover) { - @media (min-width: 768px) { - .not-hover\\:md\\:foo-red :where(.not-hover\\:md\\:foo-red > :first-child) { + expect(compiled.build(['foo-red', 'md:foo-red', 'not-hover:md:foo-red']).trim()) + .toMatchInlineSnapshot(` + ".foo-red { + :where(.foo-red > :first-child) { color: red; } } - } - " - `) + .md\\:foo-red { + @media (width >= 768px) { + :where(.md\\:foo-red > :first-child) { + color: red; + } + } + } + .not-hover\\:md\\:foo-red { + &:not(*:hover) { + @media (width >= 768px) { + :where(.not-hover\\:md\\:foo-red > :first-child) { + color: red; + } + } + } + @media not (hover: hover) { + @media (width >= 768px) { + :where(.not-hover\\:md\\:foo-red > :first-child) { + color: red; + } + } + } + }" + `) }) }) describe('addComponents()', () => { test('is an alias for addUtilities', async () => { - expect( - await run( - ['btn', 'btn-blue', 'btn-red'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addComponents }: PluginAPI) => { - addComponents({ - '.btn': { - padding: '.5rem 1rem', - borderRadius: '.25rem', - fontWeight: '600', + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addComponents }: PluginAPI) => { + addComponents({ + '.btn': { + padding: '.5rem 1rem', + borderRadius: '.25rem', + fontWeight: '600', + }, + '.btn-blue': { + backgroundColor: '#3490dc', + color: '#fff', + '&:hover': { + backgroundColor: '#2779bd', }, - '.btn-blue': { - backgroundColor: '#3490dc', - color: '#fff', - '&:hover': { - backgroundColor: '#2779bd', - }, + }, + '.btn-red': { + backgroundColor: '#e3342f', + color: '#fff', + '&:hover': { + backgroundColor: '#cc1f1a', }, - '.btn-red': { - backgroundColor: '#e3342f', - color: '#fff', - '&:hover': { - backgroundColor: '#cc1f1a', - }, - }, - }) - }, - } - }, + }, + }) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .btn { - border-radius: .25rem; - padding: .5rem 1rem; - font-weight: 600; - } + }, + ) - .btn-blue { - color: #fff; - background-color: #3490dc; - } + expect(optimizeCss(compiled.build(['btn', 'btn-blue', 'btn-red'])).trim()) + .toMatchInlineSnapshot(` + ".btn { + border-radius: .25rem; + padding: .5rem 1rem; + font-weight: 600; + } - .btn-blue:hover { - background-color: #2779bd; - } + .btn-blue { + color: #fff; + background-color: #3490dc; + } - .btn-red { - color: #fff; - background-color: #e3342f; - } + .btn-blue:hover { + background-color: #2779bd; + } - .btn-red:hover { - background-color: #cc1f1a; - } - " - `) + .btn-red { + color: #fff; + background-color: #e3342f; + } + + .btn-red:hover { + background-color: #cc1f1a; + }" + `) }) }) describe('matchComponents()', () => { test('is an alias for matchUtilities', async () => { - expect( - await run( - ['prose', 'sm:prose-sm', 'hover:prose-lg'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchComponents }: PluginAPI) => { - matchComponents( - { prose: (value) => ({ '--container-size': value }) }, - { values: { DEFAULT: 'normal', sm: 'sm', lg: 'lg' } }, - ) - }, - } - }, + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ matchComponents }: PluginAPI) => { + matchComponents( + { + prose: (value) => ({ '--container-size': value }), + }, + { + values: { + DEFAULT: 'normal', + sm: 'sm', + lg: 'lg', + }, + }, + ) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .prose { - --container-size: normal; - } + }, + ) - @media (hover: hover) { - .hover\\:prose-lg:hover { - --container-size: lg; + expect(optimizeCss(compiled.build(['prose', 'sm:prose-sm', 'hover:prose-lg'])).trim()) + .toMatchInlineSnapshot(` + ".prose { + --container-size: normal; } - } - " - `) + + @media (hover: hover) { + .hover\\:prose-lg:hover { + --container-size: lg; + } + }" + `) }) }) describe('prefix()', () => { test('is an identity function', async () => { - using fn = vi.fn() + let fn = vi.fn() await compile( css` @plugin "my-plugin"; `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -4518,13 +4518,13 @@ describe('prefix()', () => { describe('config()', () => { test('can return the resolved config when passed no arguments', async () => { - using fn = vi.fn() + let fn = vi.fn() await compile( css` @plugin "my-plugin"; `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -4546,13 +4546,13 @@ describe('config()', () => { }) test('can return part of the config', async () => { - using fn = vi.fn() + let fn = vi.fn() await compile( css` @plugin "my-plugin"; `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -4572,13 +4572,13 @@ describe('config()', () => { }) test('falls back to default value if requested path does not exist', async () => { - using fn = vi.fn() + let fn = vi.fn() await compile( css` @plugin "my-plugin"; `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { path: '', base, @@ -4592,40 +4592,4 @@ describe('config()', () => { expect(fn).toHaveBeenCalledWith('defaultvalue') }) - - // https://github.com/tailwindlabs/tailwindcss/issues/19721 - test('matchUtilities does not match Object.prototype properties as values', async ({ - expect, - }) => { - // These should not crash or produce output - expect( - await run( - [ - 'test-constructor', - 'test-hasOwnProperty', - 'test-toString', - 'test-valueOf', - 'test-__proto__', - ], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ matchUtilities }) { - matchUtilities( - { test: (value) => ({ '--test': value }) }, - { values: { foo: 'bar' } }, - ) - }), - } - }, - }, - ), - ).toEqual('') - }) }) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 11878fb30..595404001 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -1,6 +1,6 @@ import type { Features } from '..' import { substituteAtApply } from '../apply' -import { atRule, cloneAstNode, context, decl, rule, type AstNode } from '../ast' +import { atRule, cloneAstNode, decl, rule, type AstNode } from '../ast' import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candidate' import { substituteFunctions } from '../css-functions' import * as CSS from '../css-parser' @@ -202,9 +202,6 @@ export function buildPluginApi({ ruleNodes.nodes, ) } else if (variant.value.kind === 'named' && options?.values) { - if (!Object.hasOwn(options.values, variant.value.value)) { - return null - } let defaultValue = options.values[variant.value.value] if (typeof defaultValue !== 'string') { return null @@ -226,14 +223,8 @@ export function buildPluginApi({ let aValueKey = a.value ? a.value.value : 'DEFAULT' let zValueKey = z.value ? z.value.value : 'DEFAULT' - let aValue = - (options?.values && Object.hasOwn(options.values, aValueKey) - ? options.values[aValueKey] - : undefined) ?? aValueKey - let zValue = - (options?.values && Object.hasOwn(options.values, zValueKey) - ? options.values[zValueKey] - : undefined) ?? zValueKey + let aValue = options?.values?.[aValueKey] ?? aValueKey + let zValue = options?.values?.[zValueKey] ?? zValueKey if (options && typeof options.sort === 'function') { return options.sort( @@ -310,14 +301,7 @@ export function buildPluginApi({ return } - if ( - node.kind === 'function' && - (node.value === ':not' || - // A class inside `:nth-child(… of )` is part of the - // condition, not a utility being defined. - node.value === ':nth-child' || - node.value === ':nth-last-child') - ) { + if (node.kind === 'function' && node.value === ':not') { return WalkAction.Skip } }) @@ -422,13 +406,10 @@ export function buildPluginApi({ value = values.DEFAULT ?? null } else if (candidate.value.kind === 'arbitrary') { value = candidate.value.value - } else if ( - candidate.value.fraction && - Object.hasOwn(values, candidate.value.fraction) - ) { + } else if (candidate.value.fraction && values[candidate.value.fraction]) { value = values[candidate.value.fraction] ignoreModifier = true - } else if (Object.hasOwn(values, candidate.value.value)) { + } else if (values[candidate.value.value]) { value = values[candidate.value.value] } else if (values.__BARE_VALUE__) { value = values.__BARE_VALUE__(candidate.value) ?? null @@ -449,7 +430,7 @@ export function buildPluginApi({ modifier = null } else if (modifiers === 'any' || candidate.modifier.kind === 'arbitrary') { modifier = candidate.modifier.value - } else if (modifiers && Object.hasOwn(modifiers, candidate.modifier.value)) { + } else if (modifiers?.[candidate.modifier.value]) { modifier = modifiers[candidate.modifier.value] } else if (isColor && !Number.isNaN(Number(candidate.modifier.value))) { modifier = `${candidate.modifier.value}%` @@ -608,28 +589,14 @@ export function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] { function parseVariantValue(resolved: string | string[], nodes: AstNode[]): AstNode[] { let resolvedArray = typeof resolved === 'string' ? [resolved] : resolved - return resolvedArray.flatMap((r): AstNode | AstNode[] => { + return resolvedArray.flatMap((r) => { if (r.trim().endsWith('}')) { - let usesAtScope = r.includes('@scope') let updatedCSS = r.replace('}', '{@slot}}') let ast = CSS.parse(updatedCSS) - - if (usesAtScope) { - substituteAtSlot(ast, [context({ source: 'user' }, nodes)]) - return context({ source: 'variant' }, ast) - } - substituteAtSlot(ast, nodes) return ast } else { - let node = rule(r, nodes) - - if (node.kind === 'at-rule' && node.name === '@scope') { - node.nodes = [context({ source: 'user' }, nodes)] - return context({ source: 'variant' }, [node]) - } - - return node + return rule(r, nodes) } }) } diff --git a/packages/tailwindcss/src/compat/plugin-functions.ts b/packages/tailwindcss/src/compat/plugin-functions.ts index bbd5fac46..f311ad1e8 100644 --- a/packages/tailwindcss/src/compat/plugin-functions.ts +++ b/packages/tailwindcss/src/compat/plugin-functions.ts @@ -100,28 +100,6 @@ export function createThemeFn( return [base, extra] } - // If `--color-foo` and `--color-foo-bar` are both defined, `colors.foo` - // produces a synthetic object: - // - // ```ts - // { DEFAULT: 'red', bar: 'blue', __CSS_VALUES__: { DEFAULT: 0, bar: 0 } } - // ``` - // - // Prefer `DEFAULT` instead of exposing the object to plugin code. - // - // This only applies to lookups of a specific key (e.g. `colors.foo`). A - // namespace root lookup (e.g. `theme('shadow')`) resolves to the whole - // namespace as an object, like in v3. - if ( - keypath.length > 1 && - cssValue !== null && - typeof cssValue === 'object' && - !Array.isArray(cssValue) && - 'DEFAULT' in cssValue - ) { - return cssValue.DEFAULT - } - // Values from CSS take precedence over values from the config return cssValue ?? configValue })() @@ -230,12 +208,7 @@ function readFromCss( // The request looked like `theme('animation.spin')` and was turned into a // lookup for `--animation-spin-*` which had only one entry which means it // should be returned directly. - // - // This only applies to lookups of a specific key (`path.length > 1`). A - // namespace root lookup like `theme('shadow')` resolves to the whole - // namespace as an object, like in v3, so it must keep the object shape (`{ - // DEFAULT: … }`). - if (path.length > 1 && 'DEFAULT' in obj && Object.keys(obj).length === 1) { + if ('DEFAULT' in obj && Object.keys(obj).length === 1) { return [obj.DEFAULT as any, optionsObj.DEFAULT ?? ThemeOptions.NONE] as const } @@ -250,10 +223,8 @@ function get(obj: any, path: string[]) { for (let i = 0; i < path.length; ++i) { let key = path[i] - // The key does not exist so concatenate it with the next key. - // We use Object.hasOwn to avoid matching inherited prototype properties - // (e.g. "constructor", "toString") when traversing config objects. - if (obj === null || obj === undefined || typeof obj !== 'object' || !Object.hasOwn(obj, key)) { + // The key does not exist so concatenate it with the next key + if (obj?.[key] === undefined) { if (path[i + 1] === undefined) { return undefined } @@ -262,6 +233,11 @@ function get(obj: any, path: string[]) { continue } + // We never want to index into strings + if (typeof obj === 'string') { + return undefined + } + obj = obj[key] } diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index d81cd8de6..26dac0632 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -1,89 +1,86 @@ import { describe, expect, test } from 'vitest' -import { run } from '../test-utils/run' +import { compile } from '..' const css = String.raw test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { - expect( - await run( - [ - 'sm:flex', - 'md:flex', - 'lg:flex', - 'min-sm:max-md:underline', - 'min-md:max-lg:underline', - 'max-w-screen-sm', - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @theme { - --breakpoint-md: 50rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: '44rem', - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @theme { + --breakpoint-md: 50rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '44rem', }, }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + path: '', + }), + }) + + expect( + compiler.build([ + 'sm:flex', + 'md:flex', + 'lg:flex', + 'min-sm:max-md:underline', + 'min-md:max-lg:underline', + 'max-w-screen-sm', + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - .max-w-screen-sm { + ".max-w-screen-sm { max-width: 44rem; } - - @media (min-width: 44rem) { - .sm\\:flex { + .sm\\:flex { + @media (width >= 44rem) { display: flex; } - - @media not all and (min-width: 50rem) { - .min-sm\\:max-md\\:underline { + } + .min-sm\\:max-md\\:underline { + @media (width >= 44rem) { + @media (width < 50rem) { text-decoration-line: underline; } } } - - @media (min-width: 50rem) { - .md\\:flex { + .md\\:flex { + @media (width >= 50rem) { display: flex; } - - @media not all and (min-width: 64rem) { - .min-md\\:max-lg\\:underline { + } + .min-md\\:max-lg\\:underline { + @media (width >= 50rem) { + @media (width < 64rem) { text-decoration-line: underline; } } } - - @media (min-width: 64rem) { - .lg\\:flex { + .lg\\:flex { + @media (width >= 64rem) { display: flex; } } - - @media print { - .print\\:items-end { + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -92,98 +89,101 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { }) test('JS config `screens` extend CSS `--breakpoint-*`', async () => { - expect( - await run( - [ - // Order is messed up on purpose - 'md:flex', - 'sm:flex', - 'lg:flex', - 'xs:flex', - 'min-md:max-lg:underline', - 'min-sm:max-md:underline', - 'min-xs:flex', - 'min-xs:max-md:underline', + let input = css` + @theme default { + --breakpoint-xs: 39rem; + --breakpoint-md: 49rem; + } + @theme { + --breakpoint-md: 50rem; + } + @config "./config.js"; + @tailwind utilities; + ` - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @theme default { - --breakpoint-xs: 39rem; - --breakpoint-md: 49rem; - } - @theme { - --breakpoint-md: 50rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - xs: '30rem', - sm: '40rem', - md: '48rem', - lg: '60rem', - }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + xs: '30rem', + sm: '40rem', + md: '48rem', + lg: '60rem', }, }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + path: '', + }), + }) + + expect( + compiler.build([ + // Order is messed up on purpose + 'md:flex', + 'sm:flex', + 'lg:flex', + 'xs:flex', + 'min-md:max-lg:underline', + 'min-sm:max-md:underline', + 'min-xs:flex', + 'min-xs:max-md:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 30rem) { - .min-xs\\:flex, .xs\\:flex { + ".min-xs\\:flex { + @media (width >= 30rem) { display: flex; } - - @media not all and (min-width: 50rem) { - .min-xs\\:max-md\\:underline { + } + .xs\\:flex { + @media (width >= 30rem) { + display: flex; + } + } + .min-xs\\:max-md\\:underline { + @media (width >= 30rem) { + @media (width < 50rem) { text-decoration-line: underline; } } } - - @media (min-width: 40rem) { - .sm\\:flex { + .sm\\:flex { + @media (width >= 40rem) { display: flex; } - - @media not all and (min-width: 50rem) { - .min-sm\\:max-md\\:underline { + } + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 50rem) { text-decoration-line: underline; } } } - - @media (min-width: 50rem) { - .md\\:flex { + .md\\:flex { + @media (width >= 50rem) { display: flex; } - - @media not all and (min-width: 60rem) { - .min-md\\:max-lg\\:underline { + } + .min-md\\:max-lg\\:underline { + @media (width >= 50rem) { + @media (width < 60rem) { text-decoration-line: underline; } } } - - @media (min-width: 60rem) { - .lg\\:flex { + .lg\\:flex { + @media (width >= 60rem) { display: flex; } } - - @media print { - .print\\:items-end { + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -192,73 +192,71 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { }) test('JS config `screens` only setup, even if those match the default-theme export', async () => { - expect( - await run( - [ - // Order is messed up on purpose - 'md:flex', - 'sm:flex', - 'lg:flex', - 'min-md:max-lg:underline', - 'min-sm:max-md:underline', + let input = css` + @config "./config.js"; + @tailwind utilities; + ` - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - screens: { - sm: '40rem', - md: '48rem', - lg: '64rem', - }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + screens: { + sm: '40rem', + md: '48rem', + lg: '64rem', }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + path: '', + }), + }) + + expect( + compiler.build([ + // Order is messed up on purpose + 'md:flex', + 'sm:flex', + 'lg:flex', + 'min-md:max-lg:underline', + 'min-sm:max-md:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 40rem) { - .sm\\:flex { + ".sm\\:flex { + @media (width >= 40rem) { display: flex; } - - @media not all and (min-width: 48rem) { - .min-sm\\:max-md\\:underline { + } + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { text-decoration-line: underline; } } } - - @media (min-width: 48rem) { - .md\\:flex { + .md\\:flex { + @media (width >= 48rem) { display: flex; } - - @media not all and (min-width: 64rem) { - .min-md\\:max-lg\\:underline { + } + .min-md\\:max-lg\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { text-decoration-line: underline; } } } - - @media (min-width: 64rem) { - .lg\\:flex { + .lg\\:flex { + @media (width >= 64rem) { display: flex; } } - - @media print { - .print\\:items-end { + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -267,85 +265,107 @@ test('JS config `screens` only setup, even if those match the default-theme expo }) test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => { - expect( - await run( - [ - 'sm:flex', - 'md:flex', - 'mini:flex', - 'midi:flex', - 'maxi:flex', - 'min-md:max-lg:underline', - 'min-sm:max-md:underline', - 'min-midi:max-maxi:underline', - 'min-mini:max-midi:underline', + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @config "./config.js"; + @tailwind utilities; + ` - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', - }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', }, }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + path: '', + }), + }) + + expect( + compiler.build([ + 'sm:flex', + 'md:flex', + 'mini:flex', + 'midi:flex', + 'maxi:flex', + 'min-md:max-lg:underline', + 'min-sm:max-md:underline', + 'min-midi:max-maxi:underline', + 'min-mini:max-midi:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 40rem) { - .mini\\:flex, .sm\\:flex { + ".mini\\:flex { + @media (width >= 40rem) { display: flex; } - - @media not all and (min-width: 48rem) { - .min-mini\\:max-midi\\:underline, .min-sm\\:max-md\\:underline { + } + .sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .min-mini\\:max-midi\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { text-decoration-line: underline; } } } - - @media (min-width: 48rem) { - .md\\:flex, .midi\\:flex { - display: flex; - } - - @media not all and (min-width: 64rem) { - .min-md\\:max-lg\\:underline, .min-midi\\:max-maxi\\:underline { + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { text-decoration-line: underline; } } } - - @media (min-width: 64rem) { - .maxi\\:flex { + .md\\:flex { + @media (width >= 48rem) { display: flex; } } - - @media print { - .print\\:items-end { + .midi\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .min-md\\:max-lg\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .min-midi\\:max-maxi\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .maxi\\:flex { + @media (width >= 64rem) { + display: flex; + } + } + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -359,91 +379,71 @@ test('JS config with `theme: { extends }` should not include the `default-config @tailwind utilities; ` - expect( - await run(['sm:flex', 'md:flex', 'min-md:max-lg:underline', 'min-sm:max-md:underline'], input, { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', }, }, }, - base: '/root', - path: '', - }), + }, + base: '/root', + path: '', }), + }) + + expect( + compiler.build(['sm:flex', 'md:flex', 'min-md:max-lg:underline', 'min-sm:max-md:underline']), ).toBe('') expect( - await run( - [ - 'mini:flex', - 'midi:flex', - 'maxi:flex', - 'min-midi:max-maxi:underline', - 'min-mini:max-midi:underline', + compiler.build([ + 'mini:flex', + 'midi:flex', + 'maxi:flex', + 'min-midi:max-maxi:underline', + 'min-mini:max-midi:underline', - // Ensure other core variants appear at the end - 'print:items-end', - ], - input, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 40rem) { - .mini\\:flex { + ".mini\\:flex { + @media (width >= 40rem) { display: flex; } - - @media not all and (min-width: 48rem) { - .min-mini\\:max-midi\\:underline { + } + .min-mini\\:max-midi\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { text-decoration-line: underline; } } } - - @media (min-width: 48rem) { - .midi\\:flex { + .midi\\:flex { + @media (width >= 48rem) { display: flex; } - - @media not all and (min-width: 64rem) { - .min-midi\\:max-maxi\\:underline { + } + .min-midi\\:max-maxi\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { text-decoration-line: underline; } } } - - @media (min-width: 64rem) { - .maxi\\:flex { + .maxi\\:flex { + @media (width >= 64rem) { display: flex; } } - - @media print { - .print\\:items-end { + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -458,104 +458,77 @@ describe('complex screen configs', () => { @tailwind utilities; ` - expect( - await run(['min-sm:flex', 'min-md:flex', 'min-xl:flex', 'min-tall:flex'], input, { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: { max: '639px' }, - md: [ - // - { min: '668px', max: '767px' }, - '868px', - ], - lg: { min: '868px' }, - xl: { min: '1024px', max: '1279px' }, - tall: { raw: '(min-height: 800px)' }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: { max: '639px' }, + md: [ + // + { min: '668px', max: '767px' }, + '868px', + ], + lg: { min: '868px' }, + xl: { min: '1024px', max: '1279px' }, + tall: { raw: '(min-height: 800px)' }, }, }, }, - base: '/root', - path: '', - }), + }, + base: '/root', + path: '', }), - ).toBe('') + }) + + expect(compiler.build(['min-sm:flex', 'min-md:flex', 'min-xl:flex', 'min-tall:flex'])).toBe('') expect( - await run( - [ - 'sm:flex', - 'md:flex', - 'lg:flex', - 'min-lg:flex', - 'xl:flex', - 'tall:flex', + compiler.build([ + 'sm:flex', + 'md:flex', + 'lg:flex', + 'min-lg:flex', + 'xl:flex', + 'tall:flex', - // Ensure other core variants appear at the end - 'print:items-end', - ], - input, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: { max: '639px' }, - md: [ - // - { min: '668px', max: '767px' }, - '868px', - ], - lg: { min: '868px' }, - xl: { min: '1024px', max: '1279px' }, - tall: { raw: '(min-height: 800px)' }, - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 868px) { - .lg\\:flex, .min-lg\\:flex { + ".lg\\:flex { + @media (width >= 868px) { display: flex; } } - - @media (max-width: 639px) { - .sm\\:flex { + .min-lg\\:flex { + @media (width >= 868px) { display: flex; } } - - @media (max-width: 767px) and (min-width: 668px), (min-width: 868px) { - .md\\:flex { + .sm\\:flex { + @media (639px >= width) { display: flex; } } - - @media (max-width: 1279px) and (min-width: 1024px) { - .xl\\:flex { + .md\\:flex { + @media (767px >= width >= 668px), (width >= 868px) { display: flex; } } - - @media (min-height: 800px) { - .tall\\:flex { + .xl\\:flex { + @media (1279px >= width >= 1024px) { display: flex; } } - - @media print { - .print\\:items-end { + .tall\\:flex { + @media (min-height: 800px) { + display: flex; + } + } + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -564,65 +537,71 @@ describe('complex screen configs', () => { }) test("don't interfere with `min-*` and `max-*` variants of non-complex screen configs", async () => { - expect( - await run( - [ - 'sm:flex', - 'md:flex', - 'portrait:flex', - 'min-sm:flex', - 'min-md:flex', - 'min-portrait:flex', - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @theme default { - --breakpoint-sm: 39rem; - --breakpoint-md: 48rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: '40rem', - portrait: { raw: 'screen and (orientation: portrait)' }, - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 39rem; + --breakpoint-md: 48rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '40rem', + portrait: { raw: 'screen and (orientation: portrait)' }, }, }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + path: '', + }), + }) + + expect( + compiler.build([ + 'sm:flex', + 'md:flex', + 'portrait:flex', + 'min-sm:flex', + 'min-md:flex', + 'min-portrait:flex', + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 40rem) { - .min-sm\\:flex, .sm\\:flex { + ".min-sm\\:flex { + @media (width >= 40rem) { display: flex; } } - - @media (min-width: 48rem) { - .md\\:flex, .min-md\\:flex { + .sm\\:flex { + @media (width >= 40rem) { display: flex; } } - - @media screen and (orientation: portrait) { - .portrait\\:flex { + .md\\:flex { + @media (width >= 48rem) { display: flex; } } - - @media print { - .print\\:items-end { + .min-md\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .portrait\\:flex { + @media screen and (orientation: portrait) { + display: flex; + } + } + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -632,38 +611,38 @@ describe('complex screen configs', () => { }) test('JS config `screens` can overwrite default CSS `--breakpoint-*`', async () => { + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', + }, + }, + }, + base: '/root', + path: '', + }), + }) + // Note: The `sm`, `md`, and other variants are still there because they are // created before the compat layer can intercept. We do not remove them // currently. expect( - await run( - ['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-2xl:flex'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toEqual('') + compiler.build(['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-2xl:flex']), + ).toMatchInlineSnapshot(`""`) }) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 46f562918..82ac77eb1 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -1,4 +1,4 @@ -import { atRule, context, decl, rule, type AstNode, type Rule, type StyleRule } from './ast' +import { atRule, decl, rule, type AstNode, type Rule, type StyleRule } from './ast' import { type Candidate, type Variant } from './candidate' import { CompileAstFlags, type DesignSystem } from './design-system' import GLOBAL_PROPERTY_ORDER from './property-order' @@ -187,18 +187,7 @@ export function applyVariant( // E.g. `[>img]:flex` is not valid, but `has-[>img]:flex` is if (variant.relative && depth === 0) return null - let child: AstNode = rule(variant.selector, node.nodes) - - // Wrap variant-generated `@scope` rules in a `context` marker, so they are - // hoisted with the variant semantics instead of their native CSS nesting - // semantics. The slotted content is marked as `user` content, so `@scope` - // rules defined there keep their native CSS nesting semantics. - if (child.kind === 'at-rule' && child.name === '@scope') { - child.nodes = [context({ source: 'user' }, node.nodes)] - child = context({ source: 'variant' }, [child]) - } - - node.nodes = [child] + node.nodes = [rule(variant.selector, node.nodes)] return } @@ -298,19 +287,7 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) { - // `null` means that the result is invalid and that this plugin should not - // result in any CSS, but that doesn't mean that subsequent plugins are - // invalid as well. - // - // However, for backwards compatibility with `matchUtilities` this means - // that we do need to bail entirely: plugins that handle a specific - // arbitrary value type prevent falling through to other plugins if the - // result is invalid for that plugin - if (utility.options?.types?.length) return asts - - continue - } + if (compiledNodes === null) return asts asts.push(compiledNodes) } @@ -322,19 +299,7 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) { - // `null` means that the result is invalid and that this plugin should not - // result in any CSS, but that doesn't mean that subsequent plugins are - // invalid as well. - // - // However, for backwards compatibility with `matchUtilities` this means - // that we do need to bail entirely: plugins that handle a specific - // arbitrary value type prevent falling through to other plugins if the - // result is invalid for that plugin - if (utility.options?.types?.length) return asts - - continue - } + if (compiledNodes === null) return asts asts.push(compiledNodes) } diff --git a/packages/tailwindcss/src/constant-fold-declaration.test.ts b/packages/tailwindcss/src/constant-fold-declaration.test.ts index 5a0c867b6..b95ed74a3 100644 --- a/packages/tailwindcss/src/constant-fold-declaration.test.ts +++ b/packages/tailwindcss/src/constant-fold-declaration.test.ts @@ -24,30 +24,9 @@ it.each([ ['calc(3rem * 6)', '18rem'], ['calc(5rem / 2)', '2.5rem'], - // Negating with units - ['calc(2rem * -1)', '-2rem'], - ['calc(-1 * 2rem)', '-2rem'], - // Nested partial evaluation ['calc(calc(1 + 2) + 2rem)', 'calc(3 + 2rem)'], - // Nested multiplication with unknown values - ['calc(2 * calc(3 * var(--foo)))', 'calc(6 * var(--foo))'], - ['calc(calc(3 * var(--foo)) * 2)', 'calc(6 * var(--foo))'], - ['calc(2rem * calc(3 * var(--foo)))', 'calc(6rem * var(--foo))'], - - // Nested addition with unknown values - ['calc(1rem + calc(2rem + var(--foo)))', 'calc(3rem + var(--foo))'], - ['calc(calc(2rem + var(--foo)) + 1rem)', 'calc(3rem + var(--foo))'], - - // Nested multiplication can collapse away entirely - ['calc(-1 * calc(-1 * var(--foo)))', 'var(--foo)'], - ['calc(calc(-1 * var(--foo)) * -1)', 'var(--foo)'], - ['calc(-1 * calc(var(--foo) * -1))', 'var(--foo)'], - ['calc(-1 * (-1 * var(--foo)))', 'var(--foo)'], - ['calc(1 * var(--foo))', 'var(--foo)'], - ['calc(var(--foo) * 1)', 'var(--foo)'], - // Evaluation only handles two operands right now, this can change in the future ['calc(1 + 2 + 3)', 'calc(1 + 2 + 3)'], ])('should constant fold `%s` into `%s`', (input, expected) => { @@ -61,8 +40,6 @@ it.each([ ['calc(3rem * 3dvw)'], ['calc(3rem * 2dvh)'], ['calc(5rem / 17px)'], - ['calc(2rem * calc(3px * var(--foo)))'], - ['calc(1rem + 0px + var(--foo))'], ])('should not constant fold different units `%s`', (input) => { expect(constantFoldDeclaration(input)).toBe(input) }) @@ -78,6 +55,7 @@ it.each([ ['calc(var(--foo) * 0)'], ['calc(calc(var(--spacing, 0.25rem) * 32) * 0)'], ['calc(var(--spacing, 0.25rem) * -0)'], + ['calc(-0px * -1)'], // Zeroes ['0px'], @@ -88,24 +66,11 @@ it.each([ ['+0'], ['-0.0rem'], ['+0.00rem'], - - // Expressions - ['calc(-0px * -1)'], - ['calc(-1 * -0px)'], -])('should constant fold `%s` to `0` (%#)', (input) => { +])('should constant fold `%s` to `0`', (input) => { expect(constantFoldDeclaration(input)).toBe('0') }) it.each([ - // Expressions, keep unit when they are nested - // - // TODO: We might be able to fold this further to just `1rem`, but we can't do - // that for any `0`. E.g.: `calc(0s + 1rem)` which is invalid, would - // become valid if we just use `1rem`. - ['calc(calc(0px * -1) + 1rem)', 'calc(0px + 1rem)'], - ['calc(calc(-1 * 0px) + 1rem)', 'calc(0px + 1rem)'], - - // Non-foldable units ['0deg', '0deg'], ['0rad', '0deg'], ['0%', '0%'], @@ -127,11 +92,3 @@ it.each([ it('should not constant fold when dividing by `0`', () => { expect(constantFoldDeclaration('calc(123rem / 0)')).toBe('calc(123rem / 0)') }) - -it('should not constant fold when a computation has a high-precision result', () => { - expect(constantFoldDeclaration('calc(100% / 3.5)')).toBe('calc(100% / 3.5)') -}) - -it('should constant fold division results with floating-point error after scaling', () => { - expect(constantFoldDeclaration('calc(29% / 100)')).toBe('0.29%') -}) diff --git a/packages/tailwindcss/src/constant-fold-declaration.ts b/packages/tailwindcss/src/constant-fold-declaration.ts index c15420150..9c33e73e9 100644 --- a/packages/tailwindcss/src/constant-fold-declaration.ts +++ b/packages/tailwindcss/src/constant-fold-declaration.ts @@ -5,55 +5,24 @@ import { walk, WalkAction } from './walk' // Assumption: We already assume that we receive somewhat valid `calc()` // expressions. So we will see `calc(1 + 1)` and not `calc(1+1)` -export function constantFoldDeclaration( - input: string, - rem: number | null = null, - normalizeUnit = true, -): string { - let [folded, valueAst] = constantFoldDeclarationAst(ValueParser.parse(input), rem, normalizeUnit) - - return folded ? ValueParser.toCss(valueAst) : input -} - -export function constantFoldDeclarationAst( - ast: ValueParser.ValueAstNode[], - rem: number | null = null, - normalizeUnit = true, -): [folded: boolean, ast: ValueParser.ValueAstNode[]] { +export function constantFoldDeclaration(input: string, rem: number | null = null): string { let folded = false + let valueAst = ValueParser.parse(input) - walk(ast, { - exit(valueNode, ctx) { + walk(valueAst, { + exit(valueNode) { // Canonicalize dimensions to their simplest form. This includes: // - Convert `-0`, `+0`, `0.0`, … to `0` - // - Convert `-0px`, `+0em`, `0.0rem`, … to `0` + // - Convert `-0px`, `+0em`, `0.0rem`, … to `0` // - Convert units to an equivalent unit if ( valueNode.kind === 'word' && valueNode.value !== '0' // Already `0`, nothing to do ) { - let canonical = canonicalizeDimension(valueNode.value, rem, normalizeUnit) + let canonical = canonicalizeDimension(valueNode.value, rem) if (canonical === null) return // Couldn't be canonicalized, nothing to do if (canonical === valueNode.value) return // Already in canonical form, nothing to do - // We need to be careful with `0` because `0` can only be - // converted to `0` if we're dealing with a `` type. - if (canonical === '0') { - // When used inside of a function such as `calc(…)`, then this isn't - // always safe to convert to `0`. - // - // E.g.: - // - `calc(0px + 1rem)` → `calc(0 + 1rem)` this goes from valid to invalid - // - `calc(0px * 1rem)` → `calc(0 * 1rem)` this goes from invalid to valid - if (ctx.parent?.kind === 'function') { - let withUnit = canonicalizeDimension(valueNode.value, rem, false) - if (withUnit === null) return - - folded = true - return WalkAction.ReplaceSkip(ValueParser.word(withUnit)) - } - } - folded = true return WalkAction.ReplaceSkip(ValueParser.word(canonical)) } @@ -71,14 +40,10 @@ export function constantFoldDeclarationAst( // { kind: 'word', value: '256' } 4 // ] if (valueNode.nodes.length !== 5) return - if (valueNode.nodes[2].kind !== 'word') return - let lhsNode = valueNode.nodes[0] + let lhs = dimensions.get(valueNode.nodes[0].value) let operator = valueNode.nodes[2].value - let rhsNode = valueNode.nodes[4] - - let lhs = lhsNode.kind === 'word' ? dimensions.get(lhsNode.value) : null - let rhs = rhsNode.kind === 'word' ? dimensions.get(rhsNode.value) : null + let rhs = dimensions.get(valueNode.nodes[4].value) // Nullify entire expression when multiplying by `0`, e.g.: `calc(0 * 100vw)` -> `0` // @@ -92,154 +57,6 @@ export function constantFoldDeclarationAst( return WalkAction.ReplaceSkip(ValueParser.word('0')) } - // Fold `0 * something-without-unit` to just `0`, inside of a function such as `calc(…)` - if (operator === '*' && lhs?.[0] === 0 && lhs?.[1] !== null && rhs?.[1] === null) { - folded = true - if (ctx.parent?.kind === 'function') { - return WalkAction.ReplaceSkip(ValueParser.word(`0${lhs[1]}`)) - } else { - return WalkAction.ReplaceSkip(ValueParser.word('0')) - } - } - - // Fold `something-without-unit * 0` to just `0`, inside of a function such as `calc(…)` - if (operator === '*' && rhs?.[0] === 0 && rhs?.[1] !== null && lhs?.[1] === null) { - folded = true - if (ctx.parent?.kind === 'function') { - return WalkAction.ReplaceSkip(ValueParser.word(`0${rhs[1]}`)) - } else { - return WalkAction.ReplaceSkip(ValueParser.word('0')) - } - } - - if (operator === '*') { - // Multiplying by `1` can always unwrap the other side, even when that - // side is an expression like `var(--foo)` that we can't fully fold. - if (lhs?.[0] === 1 && lhs?.[1] === null) { - folded = true - return WalkAction.ReplaceSkip(rhsNode) - } - - if (rhs?.[0] === 1 && rhs?.[1] === null) { - folded = true - return WalkAction.ReplaceSkip(lhsNode) - } - } - - if (operator === '*' || operator === '+') { - // If only one side is known, and the unknown side is itself another - // binary expression with the same operator, try to combine the two - // known parts and keep the unknown part in place. - // - // E.g.: - // - // - `calc(2rem * calc(3 * var(--foo)))` → `calc(6rem * var(--foo))` - // - `calc(2rem + calc(3rem + var(--foo)))` → `calc(5rem + var(--foo))` - // - // At this point `lhs` and `rhs` are dimensions ([value, unit] | - // null). The `null` one is the unknown node. - let constant = lhs ?? rhs - let nestedNode = lhs === null ? lhsNode : rhs === null ? rhsNode : null - - if ( - constant !== null && - nestedNode !== null && - nestedNode.kind === 'function' && - (nestedNode.value === 'calc' || nestedNode.value === '') && - nestedNode.nodes.length === 5 && - nestedNode.nodes[2].kind === 'word' && - nestedNode.nodes[2].value === operator - ) { - let nestedLhsNode = nestedNode.nodes[0] - let nestedRhsNode = nestedNode.nodes[4] - let nestedLhs = - nestedLhsNode.kind === 'word' ? dimensions.get(nestedLhsNode.value) : null - let nestedRhs = - nestedRhsNode.kind === 'word' ? dimensions.get(nestedRhsNode.value) : null - - let known = nestedLhs ?? nestedRhs - let unknown = - nestedLhs === null ? nestedLhsNode : nestedRhs === null ? nestedRhsNode : null - - if (known !== null && unknown !== null) { - // `*` requires both values being unitless, or one of them. Both - // values having a unit is invalid. - // - // - `2 * 3` → valid - // - `4rem * 5` → valid - // - `6 * 7rem` → valid - // - `8rem * 9rem` → invalid - if ( - operator === '*' && - !( - (constant[1] === null && known[1] === null) || // Both can be unitless - (constant[1] === null && known[1] !== null) || // One of them can be unitless, but the other can't - (constant[1] !== null && known[1] === null) // One of them can be unitless, but the other can't - ) - ) { - return - } - - // `+` requires that both values being unitless, or both values - // have a unit. Mixed units are valid, but we won't fold these. - // - // - `2 + 3` → valid - // - `4rem + 5` → invalid - // - `6 + 7rem` → invalid - // - `8rem + 9rem` → valid - if ( - operator === '+' && - !( - constant[1] === known[1] // Only same unit is allowed - ) - ) { - return - } - - // Re-associate nested expressions so we can still fold the known - // part of `x op (y op z)` when `z` is unknown. - // - // Examples: - // - `calc(2 * calc(3 * var(--foo)))` -> `calc(6 * var(--foo))` - // - `calc(1rem + calc(2rem + var(--foo)))` -> `calc(3rem + var(--foo))` - let combined: string - switch (operator) { - case '*': { - combined = `${constant[0] * known[0]}${constant[1] ?? known[1] ?? ''}` - break - } - case '+': { - combined = `${constant[0] + known[0]}${constant[1] ?? known[1] ?? ''}` - break - } - - default: - return - } - - folded = true - - if (operator === '*' && combined === '1') { - return WalkAction.ReplaceSkip(unknown) - } - - let replacement: ValueParser.ValueFunctionNode = { - kind: 'function', - value: valueNode.value, - nodes: [ - ValueParser.word(combined), - valueNode.nodes[1], - valueNode.nodes[2], - valueNode.nodes[3], - unknown, - ], - } - - return WalkAction.ReplaceSkip(replacement) - } - } - } - // We're not dealing with dimensions, so we can't fold this if (lhs === null || rhs === null) { return @@ -253,9 +70,7 @@ export function constantFoldDeclarationAst( (lhs[1] !== null && rhs[1] === null) // Unit * Unitless, e.g.: `1rem * 2` ) { folded = true - return WalkAction.ReplaceSkip( - ValueParser.word(`${lhs[0] * rhs[0]}${lhs[1] ?? rhs[1] ?? ''}`), - ) + return WalkAction.ReplaceSkip(ValueParser.word(`${lhs[0] * rhs[0]}${lhs[1] ?? ''}`)) } break } @@ -286,19 +101,8 @@ export function constantFoldDeclarationAst( ((lhs[1] === null && rhs[1] === null) || // Unitless / Unitless, e.g.: `8 / 2` (lhs[1] !== null && rhs[1] === null)) // Unit / Unitless, e.g.: `1rem / 2` ) { - let computed = lhs[0] / rhs[0] - - // Only fold with .xx precision, anything beyond that might be a bit too much. - // - // E.g. 100% / 3.5 = 28.571428571428573%, which is correct but not - // as user friendly. Especially when going from - // `w-[calc(100%/3.5)]` → `w-[28.571428571428573%]` - if (Math.round(computed * 100) / 100 !== computed) { - break - } - folded = true - return WalkAction.ReplaceSkip(ValueParser.word(`${computed}${lhs[1] ?? ''}`)) + return WalkAction.ReplaceSkip(ValueParser.word(`${lhs[0] / rhs[0]}${lhs[1] ?? ''}`)) } break } @@ -307,14 +111,10 @@ export function constantFoldDeclarationAst( }, }) - return [folded, ast] + return folded ? ValueParser.toCss(valueAst) : input } -function canonicalizeDimension( - input: string, - rem: number | null = null, - normalizeUnit = true, -): string | null { +function canonicalizeDimension(input: string, rem: number | null = null): string | null { let dimension = dimensions.get(input) if (dimension === null) return null // This shouldn't happen @@ -322,13 +122,7 @@ function canonicalizeDimension( if (unit === null) return `${value}` // Already unitless, nothing to do // Replace `0` units with just `0` - if (value === 0 && isLength(input)) { - if (normalizeUnit) return '0' - else return `0${unit}` // Keep unit - } - - // Only normalize into base units when necessary - if (!normalizeUnit) return `${input}` + if (value === 0 && isLength(input)) return '0' // prettier-ignore switch (unit.toLowerCase()) { diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index 8cab2949f..464f05864 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -3,7 +3,7 @@ import path from 'node:path' import { describe, expect, test } from 'vitest' import { compile } from '.' import plugin from './plugin' -import { compileCss, pretty, run } from './test-utils/run' +import { compileCss, optimizeCss } from './test-utils/run' const css = String.raw @@ -16,11 +16,9 @@ describe('--alpha(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .foo { + ".foo { margin: oklab(62.7955% .224 .125 / .5); - } - " + }" `) }) @@ -74,15 +72,13 @@ describe('--spacing(…)', () => { } `), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; } .foo { margin: calc(var(--spacing) * 4); - } - " + }" `) }) @@ -98,115 +94,12 @@ describe('--spacing(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .foo { + ".foo { margin: 1rem; - } - " + }" `) }) - describe('optimizations', () => { - // Use custom `compileCss` such that we can use a version without Lightning - // CSS. This way we test _our_ optimizations instead of Lightning's - // optimizations. - async function compileCss(css: string, options: Parameters[1] = {}) { - let { build } = await compile(css, options) - return pretty(build([])) - } - - test('--spacing(…) optimizes the output when the input is `0`', async () => { - expect( - await compileCss(css` - @theme { - --spacing: 0.25rem; - } - - .foo { - margin: --spacing(0); - padding: --spacing(0px); - } - `), - ).toMatchInlineSnapshot(` - " - .foo { - margin: 0px; - padding: 0px; - } - " - `) - }) - - test('--spacing(…) optimizes the output when the input is `0` (with an inlined theme value)', async () => { - expect( - await compileCss(css` - @theme inline { - --spacing: 0.25rem; - } - - .foo { - margin: --spacing(0); - padding: --spacing(0px); - } - `), - ).toMatchInlineSnapshot(` - " - .foo { - margin: 0px; - padding: 0px; - } - " - `) - }) - - test('--spacing(…) optimizes the output when the input is `1`', async () => { - expect( - await compileCss(css` - @theme { - --spacing: 0.25rem; - } - - .foo { - margin: --spacing(1); - padding: --spacing(1px); - } - `), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing: 0.25rem; - } - .foo { - margin: var(--spacing); - padding: var(--spacing); - } - " - `) - }) - - test('--spacing(…) optimizes the output when the input is `1` (with an inlined theme value)', async () => { - expect( - await compileCss(css` - @theme inline { - --spacing: 0.25rem; - } - - .foo { - margin: --spacing(1); - padding: --spacing(1px); - } - `), - ).toMatchInlineSnapshot(` - " - .foo { - margin: 0.25rem; - padding: 0.25rem; - } - " - `) - }) - }) - test('--spacing(…) relies on `--spacing` to be defined', async () => { await expect(() => compileCss(css` @@ -260,15 +153,13 @@ describe('--theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: red; } .red { color: var(--color-red-500); - } - " + }" `) }) @@ -283,11 +174,9 @@ describe('--theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -302,8 +191,7 @@ describe('--theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: red; } @@ -315,8 +203,7 @@ describe('--theme(…)', () => { .red { color: color-mix(in oklab, var(--color-red-500) 50%, transparent); } - } - " + }" `) }) @@ -331,11 +218,9 @@ describe('--theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: oklab(62.7955% .224863 .125846); - } - " + }" `) }) @@ -350,11 +235,9 @@ describe('--theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { font-family: var(--tw-default-font-family, Potato Sans, sans-serif); - } - " + }" `) }) @@ -369,11 +252,9 @@ describe('--theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { font-family: var(--tw-default-font-family, Potato Sans, sans-serif); - } - " + }" `) }) @@ -388,11 +269,9 @@ describe('--theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { font-family: Potato Sans, sans-serif; - } - " + }" `) }) @@ -407,18 +286,15 @@ describe('--theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { font-family: Potato Sans, sans-serif; - } - " + }" `) }) test('--theme(…) does not inject the fallback if the fallback is `initial`', async () => { expect( - await run( - ['tw:font-sans'], + await compileCss( css` @theme prefix(tw) { --font-sans: @@ -433,12 +309,11 @@ describe('--theme(…)', () => { } @tailwind utilities; `, + ['tw:font-sans'], ), ).toMatchInlineSnapshot(` - " - :root, :host { - --tw-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", - "Segoe UI Symbol", "Noto Color Emoji"; + ":root, :host { + --tw-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --tw-default-font-family: var(--tw-font-sans); } @@ -450,8 +325,7 @@ describe('--theme(…)', () => { .tw\\:font-sans { font-family: var(--tw-font-sans); - } - " + }" `) }) @@ -475,8 +349,7 @@ describe('--theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - @media (min-width: 48rem) { + "@media (min-width: 48rem) { .blue { color: #00f; } @@ -486,8 +359,7 @@ describe('--theme(…)', () => { .red { color: red; } - } - " + }" `) }) @@ -537,6 +409,7 @@ describe('--theme(…)', () => { } @plugin "my-plugin.js"; `, + [], { loadModule: async () => ({ path: '', @@ -546,8 +419,7 @@ describe('--theme(…)', () => { }, ), ).toMatchInlineSnapshot(` - " - @layer base { + "@layer base { html, :host { font-family: var(--default-font-family, system-ui); } @@ -558,8 +430,7 @@ describe('--theme(…)', () => { --font-sans: sans-serif; --default-font-family: var(--font-sans); } - } - " + }" `) }) }) @@ -578,11 +449,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -597,11 +466,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -616,11 +483,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -635,11 +500,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -654,11 +517,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -673,11 +534,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -692,11 +551,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: oklab(62.7955% .224 .125 / .75); - } - " + }" `) }) @@ -711,11 +568,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: oklab(62.7955% .224 .125 / .75); - } - " + }" `) }) @@ -730,11 +585,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: oklab(62.7955% .224 .125 / .75); - } - " + }" `) }) @@ -749,8 +602,7 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; } @@ -758,8 +610,7 @@ describe('theme(…)', () => { .red { color: color-mix(in oklab, red var(--opacity), transparent); } - } - " + }" `) }) @@ -775,8 +626,7 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; } @@ -784,8 +634,7 @@ describe('theme(…)', () => { .red { color: color-mix(in oklab, red var(--opacity, 50%), transparent); } - } - " + }" `) }) @@ -800,11 +649,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .space-on-the-left { + ".space-on-the-left { margin-left: 3rem; - } - " + }" `) }) @@ -819,11 +666,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .space-on-the-left { + ".space-on-the-left { margin-left: .625rem; - } - " + }" `) }) @@ -838,11 +683,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .space-on-the-left { + ".space-on-the-left { margin-left: calc(100vh - .625rem); - } - " + }" `) }) @@ -857,11 +700,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .radius { + ".radius { border-radius: .5rem; - } - " + }" `) }) @@ -877,11 +718,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .default-blur { + ".default-blur { filter: blur(8px); - } - " + }" `) }) @@ -898,12 +737,10 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .text { + ".text { font-size: 1337.75rem; line-height: 1337rem; - } - " + }" `) }) @@ -912,40 +749,41 @@ describe('theme(…)', () => { await compileCss(css` @theme default reference { --font-family-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; + ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, + Segoe UI Symbol, Noto Color Emoji; } .fam { font-family: theme(fontFamily.sans); } `), ).toMatchInlineSnapshot(` - " - .fam { - 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; - } - " + ".fam { + font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + }" `) }) test('theme(fontFamily.sans) (config)', async () => { - expect( - await compileCss( - css` - @config "./my-config.js"; - .fam { - font-family: theme(fontFamily.sans); - } - `, - { loadModule: async () => ({ path: '', base: '/root', module: {} }) }, - ), - ).toMatchInlineSnapshot(` - " - .fam { - 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; - } - " + let compiled = await compile( + css` + @config "./my-config.js"; + .fam { + font-family: theme(fontFamily.sans); + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: {}, + }), + }, + ) + + expect(optimizeCss(compiled.build([])).trim()).toMatchInlineSnapshot(` + ".fam { + font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + }" `) }) }) @@ -971,11 +809,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -990,11 +826,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: oklab(62.7955% .224 .125 / .25); - } - " + }" `) }) @@ -1006,11 +840,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .fam { + ".fam { font-family: Helvetica Neue, Helvetica, sans-serif; - } - " + }" `) }) @@ -1028,11 +860,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .fam { + ".fam { font-family: Helvetica Neue, Helvetica, sans-serif; - } - " + }" `) }) }) @@ -1050,11 +880,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -1070,11 +898,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: oklab(62.7955% .224 .125 / .25); - } - " + }" `) }) }) @@ -1091,11 +917,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) @@ -1110,11 +934,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: oklab(62.7955% .224 .125 / .5); - } - " + }" `) }) @@ -1129,11 +951,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) }) @@ -1150,11 +970,9 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - .blur { + ".blur { filter: blur(8px); - } - " + }" `) }) }) @@ -1163,7 +981,7 @@ describe('theme(…)', () => { test.each([ [ 'fontFamily.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"', + 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"', ], ['maxWidth.xs', '20rem'], ['transitionTimingFunction.in-out', 'cubic-bezier(.4, 0, .2, 1)'], @@ -1196,8 +1014,7 @@ describe('theme(…)', () => { describe('in candidates', () => { test('sm:[--color:theme(colors.red[500])]', async () => { expect( - await run( - ['sm:[--color:theme(colors.red[500])]'], + await compileCss( css` @tailwind utilities; @theme { @@ -1205,47 +1022,50 @@ describe('theme(…)', () => { --color-red-500: #f00; } `, + ['sm:[--color:theme(colors.red[500])]'], ), ).toMatchInlineSnapshot(` - " - @media (min-width: 40rem) { + "@media (min-width: 40rem) { .sm\\:\\[--color\\:theme\\(colors\\.red\\[500\\]\\)\\] { --color: red; } - } - " + }" `) }) test("values that don't exist don't produce candidates", async () => { - let input = css` - @tailwind utilities; - @theme reference { - --radius-sm: 2rem; - } - ` - // This guarantees that valid candidates still make it through when some are invalid expect( - await run( + await compileCss( + css` + @tailwind utilities; + @theme reference { + --radius-sm: 2rem; + } + `, [ 'rounded-[theme(--radius-sm)]', 'rounded-[theme(i.do.not.exist)]', 'rounded-[theme(--i-do-not-exist)]', ], - input, ), ).toMatchInlineSnapshot(` - " - .rounded-\\[theme\\(--radius-sm\\)\\] { + ".rounded-\\[theme\\(--radius-sm\\)\\] { border-radius: 2rem; - } - " + }" `) // This guarantees no output for the following candidates expect( - await run(['rounded-[theme(i.do.not.exist)]', 'rounded-[theme(--i-do-not-exist)]'], input), + await compileCss( + css` + @tailwind utilities; + @theme reference { + --radius-sm: 2rem; + } + `, + ['rounded-[theme(i.do.not.exist)]', 'rounded-[theme(--i-do-not-exist)]'], + ), ).toEqual('') }) }) @@ -1266,13 +1086,11 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - @media (min-width: 48rem) and (max-width: 64rem) { + "@media (min-width: 48rem) and (max-width: 64rem) { .red { color: red; } - } - " + }" `) }) @@ -1290,13 +1108,11 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - @media (min-width: 48rem) and (not (min-width: 64rem)) { + "@media (min-width: 48rem) and (not (min-width: 64rem)) { .red { color: red; } - } - " + }" `) }) }) @@ -1315,13 +1131,11 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - @media (min-width: 48rem) { + "@media (min-width: 48rem) { .red { color: red; } - } - " + }" `) }) @@ -1338,13 +1152,11 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - @container not (max-width: 48rem) { + "@container not (max-width: 48rem) { .red { color: red; } - } - " + }" `) }) @@ -1361,63 +1173,59 @@ describe('theme(…)', () => { } `), ).toMatchInlineSnapshot(` - " - @supports (text-stroke: 0.75rem) { + "@supports (text-stroke: 0.75rem) { .red { color: red; } - } - " + }" `) }) }) describe('in plugins', () => { test('CSS theme functions in plugins are properly evaluated', async () => { - expect( - await run( - ['my-utility'], - css` - @layer base, utilities; - @plugin "my-plugin"; - @theme reference { - --color-red: oklch(62% 0.25 30); - --color-orange: oklch(79% 0.17 70); - --color-blue: oklch(45% 0.31 264); - --color-pink: oklch(87% 0.07 7); - } - @layer utilities { - @tailwind utilities; - } - `, - { - async loadModule() { - return { - path: '', - base: '/root', - module: plugin(({ addBase, addUtilities }) => { - addBase({ - '.my-base-rule': { - color: 'theme(colors.red)', - 'outline-color': 'theme(colors.orange / 15%)', - 'background-color': 'theme(--color-blue)', - 'border-color': 'theme(--color-pink / 10%)', - }, - }) + let compiled = await compile( + css` + @layer base, utilities; + @plugin "my-plugin"; + @theme reference { + --color-red: oklch(62% 0.25 30); + --color-orange: oklch(79% 0.17 70); + --color-blue: oklch(45% 0.31 264); + --color-pink: oklch(87% 0.07 7); + } + @layer utilities { + @tailwind utilities; + } + `, + { + async loadModule() { + return { + path: '', + base: '/root', + module: plugin(({ addBase, addUtilities }) => { + addBase({ + '.my-base-rule': { + color: 'theme(colors.red)', + 'outline-color': 'theme(colors.orange / 15%)', + 'background-color': 'theme(--color-blue)', + 'border-color': 'theme(--color-pink / 10%)', + }, + }) - addUtilities({ - '.my-utility': { - color: 'theme(colors.red)', - }, - }) - }), - } - }, + addUtilities({ + '.my-utility': { + color: 'theme(colors.red)', + }, + }) + }), + } }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + }, + ) + + expect(optimizeCss(compiled.build(['my-utility'])).trim()).toMatchInlineSnapshot(` + "@layer base { .my-base-rule { color: oklch(62% .25 30); background-color: oklch(45% .31 264); @@ -1430,64 +1238,61 @@ describe('in plugins', () => { .my-utility { color: oklch(62% .25 30); } - } - " + }" `) }) }) describe('in JS config files', () => { test('CSS theme functions in config files are properly evaluated', async () => { - expect( - await run( - ['my-utility'], - css` - @layer base, utilities; - @config "./my-config.js"; - @theme reference { - --color-red: red; - --color-orange: orange; - } - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: { - theme: { - extend: { - colors: { - primary: 'theme(colors.red)', - secondary: 'theme(--color-orange)', - }, + let compiled = await compile( + css` + @layer base, utilities; + @config "./my-config.js"; + @theme reference { + --color-red: red; + --color-orange: orange; + } + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: { + theme: { + extend: { + colors: { + primary: 'theme(colors.red)', + secondary: 'theme(--color-orange)', }, }, - plugins: [ - plugin(({ addBase, addUtilities }) => { - addBase({ - '.my-base-rule': { - background: 'theme(colors.primary)', - color: 'theme(colors.secondary)', - }, - }) - - addUtilities({ - '.my-utility': { - color: 'theme(colors.red)', - }, - }) - }), - ], }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + plugins: [ + plugin(({ addBase, addUtilities }) => { + addBase({ + '.my-base-rule': { + background: 'theme(colors.primary)', + color: 'theme(colors.secondary)', + }, + }) + + addUtilities({ + '.my-utility': { + color: 'theme(colors.red)', + }, + }) + }), + ], + }, + }), + }, + ) + + expect(optimizeCss(compiled.build(['my-utility'])).trim()).toMatchInlineSnapshot(` + "@layer base { .my-base-rule { color: orange; background: red; @@ -1498,8 +1303,7 @@ describe('in JS config files', () => { .my-utility { color: red; } - } - " + }" `) }) }) @@ -1513,6 +1317,7 @@ test('replaces CSS theme() function with values inside imported stylesheets', as } @import './bar.css'; `, + [], { async loadStylesheet() { return { @@ -1528,46 +1333,46 @@ test('replaces CSS theme() function with values inside imported stylesheets', as }, ), ).toMatchInlineSnapshot(` - " - .red { + ".red { color: red; - } - " + }" `) }) test('resolves paths ending with a 1', async () => { expect( - await compileCss(css` - @theme { - --spacing-1: 0.25rem; - } + await compileCss( + css` + @theme { + --spacing-1: 0.25rem; + } - .foo { - margin: theme(spacing.1); - } - `), + .foo { + margin: theme(spacing.1); + } + `, + [], + ), ).toMatchInlineSnapshot(` - " - .foo { + ".foo { margin: .25rem; - } - " + }" `) }) -test('upgrades to a full JS compat theme lookup if a value cannot be mapped to a CSS variable', async () => { +test('upgrades to a full JS compat theme lookup if a value can not be mapped to a CSS variable', async () => { expect( - await compileCss(css` - .semi { - font-weight: theme(fontWeight.semibold); - } - `), + await compileCss( + css` + .semi { + font-weight: theme(fontWeight.semibold); + } + `, + [], + ), ).toMatchInlineSnapshot(` - " - .semi { + ".semi { font-weight: 600; - } - " + }" `) }) diff --git a/packages/tailwindcss/src/css-functions.ts b/packages/tailwindcss/src/css-functions.ts index 32fba455f..bb3e305e4 100644 --- a/packages/tailwindcss/src/css-functions.ts +++ b/packages/tailwindcss/src/css-functions.ts @@ -2,7 +2,6 @@ import { Features } from '.' import { type AstNode } from './ast' import type { DesignSystem } from './design-system' import { withAlpha } from './utilities' -import { dimensions } from './utils/dimensions' import { segment } from './utils/segment' import * as ValueParser from './value-parser' import { walk, WalkAction } from './walk' @@ -63,24 +62,6 @@ function spacing( ) } - // Optimization: - // - // - We know that at this point the `--spacing` value must be set. - // - We know that `--spacing` must be set to a `` unit, such as `0.25rem` - // - We can assume that the `--spacing` value is not set to a `0`-like value. - // Otherwise `p-` would calculate as `0` which wouldn't make sense. - // - // - That means that a value of `0` can be replaced by `0px`. It's important - // to keep the unit so the value stays a ``, e.g. when assigned to - // a custom property and later used inside `calc(…)`. - // - That means that a value of `1` can be replaced by `multiplier` - let valueDimension = dimensions.get(value) - if (valueDimension) { - if (valueDimension[0] === 0) return '0px' - if (valueDimension[0] === 1) return multiplier - } - - // No known optimizations available, use full calculation return `calc(${multiplier} * ${value})` } diff --git a/packages/tailwindcss/src/css-parser.test.ts b/packages/tailwindcss/src/css-parser.test.ts index 0dcbc0a41..25657d9db 100644 --- a/packages/tailwindcss/src/css-parser.test.ts +++ b/packages/tailwindcss/src/css-parser.test.ts @@ -8,12 +8,6 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { return CSS.parse(string.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n')) } - function parseWithLoc(string: string) { - return CSS.parse(string.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'), { - from: 'input.css', - }) - } - describe('comments', () => { it('should parse a comment and ignore it', () => { expect( @@ -31,23 +25,6 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { ).toEqual([]) }) - it('should end a comment at `*/` even when it is preceded by a `\\`', () => { - expect( - parse(css` - /* C:\temp\*/ - .foo { - color: red; - } - `), - ).toEqual([ - { - kind: 'rule', - selector: '.foo', - nodes: [{ kind: 'declaration', property: 'color', value: 'red', important: false }], - }, - ]) - }) - it('should parse a comment inside of a selector and ignore it', () => { expect( parse(css` @@ -220,7 +197,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { ]) }) - it('should parse declarations with strings and escaped string endings', () => { + it('should parse declarations with with strings and escaped string endings', () => { expect( parse(css` content: 'These are not the end "\' of the string'; @@ -229,7 +206,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { { kind: 'declaration', property: 'content', - value: `'These are not the end "\\' of the string'`, + value: `'These are not the end \"\\' of the string'`, important: false, }, ]) @@ -465,28 +442,6 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { ]) }) - it('should end a comment in a custom property at `*/` even when it is preceded by a `\\`', () => { - expect( - parse(css` - --foo: /* C:\temp\*/ bar; - --bar: /* baz */ qux; - `), - ).toEqual([ - { - kind: 'declaration', - property: '--foo', - value: '/* C:\\temp\\*/ bar', - important: false, - }, - { - kind: 'declaration', - property: '--bar', - value: '/* baz */ qux', - important: false, - }, - ]) - }) - it('should parse empty custom properties', () => { expect( parse(css` @@ -1190,20 +1145,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { color: blue; } `), - ).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Missing opening {]`) - - expect(() => - parseWithLoc(` - .foo { - color: red; - } - - .bar - /* ^ Missing opening { */ - color: blue; - } - `), - ).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: input.css:9:11: Missing opening {]`) + ).toThrowErrorMatchingInlineSnapshot(`[Error: Missing opening {]`) }) it('should error when curly brackets are unbalanced (closing)', () => { @@ -1218,105 +1160,59 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { /* ^ Missing closing } */ `), - ).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Missing closing } at .bar]`) - - expect(() => - parseWithLoc(` - .foo { - color: red; - } - - .bar { - color: blue; - - /* ^ Missing closing } */ - `), - ).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: input.css:6:11: Missing closing } at .bar]`, - ) + ).toThrowErrorMatchingInlineSnapshot(`[Error: Missing closing } at .bar]`) }) it('should error when an unterminated string is used', () => { - let input = css` - .foo { - content: "Hello world! - /* ^ missing " */ - font-weight: bold; - } - ` - - expect(() => parse(input)).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: Unterminated string: "Hello world!"]`, - ) - - expect(() => parseWithLoc(input)).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: input.css:3:20: Unterminated string: "Hello world!"]`, - ) + expect(() => + parse(css` + .foo { + content: "Hello world! + /* ^ missing " */ + font-weight: bold; + } + `), + ).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!"]`) }) it('should error when an unterminated string is used with a `;`', () => { - let input = css` - .foo { - content: "Hello world!; - /* ^ missing " */ - font-weight: bold; - } - ` - - expect(() => parse(input)).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: Unterminated string: "Hello world!;"]`, - ) - - expect(() => parseWithLoc(input)).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: input.css:3:20: Unterminated string: "Hello world!;"]`, - ) + expect(() => + parse(css` + .foo { + content: "Hello world!; + /* ^ missing " */ + font-weight: bold; + } + `), + ).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!;"]`) }) it('should error when incomplete custom properties are used', () => { expect(() => parse('--foo')).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: Invalid custom property, expected a value]`, - ) - - expect(() => parseWithLoc('--foo')).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: input.css:1:1: Invalid custom property, expected a value]`, + `[Error: Invalid custom property, expected a value]`, ) }) it('should error when incomplete custom properties are used inside rules', () => { expect(() => parse('.foo { --bar }')).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: Invalid custom property, expected a value]`, - ) - - expect(() => parseWithLoc('.foo { --bar }')).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: input.css:1:8: Invalid custom property, expected a value]`, + `[Error: Invalid custom property, expected a value]`, ) }) it('should error when an unterminated string is used in a custom property', () => { - // prettier-ignore - let input = css` - .foo { - --bar: 'Hello world! - /* ^ missing ' * /; - } - ` - - expect(() => parse(input)).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: Unterminated string: 'Hello world!']`, - ) - - expect(() => parseWithLoc(input)).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: input.css:3:18: Unterminated string: 'Hello world!']`, - ) + expect(() => + parse(css` + .foo { + --bar: 'Hello world! + /* ^ missing ' * /; + } + `), + ).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: 'Hello world!']`) }) it('should error when a declaration is incomplete', () => { expect(() => parse('.foo { bar }')).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: Invalid declaration: \`bar\`]`, - ) - - expect(() => parseWithLoc('.foo { bar }')).toThrowErrorMatchingInlineSnapshot( - `[CssSyntaxError: input.css:1:8: Invalid declaration: \`bar\`]`, + `[Error: Invalid declaration: \`bar\`]`, ) }) }) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index 0896881e6..1653c3789 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -9,8 +9,7 @@ import { type Declaration, type Rule, } from './ast' -import { createLineTable } from './source-maps/line-table' -import type { Source, SourceLocation } from './source-maps/source' +import type { Source } from './source-maps/source' const BACKSLASH = 0x5c const SLASH = 0x2f @@ -37,30 +36,6 @@ export interface ParseOptions { from?: string } -/** - * CSS syntax error with source location information. - */ -export class CssSyntaxError extends Error { - loc: SourceLocation | null - - constructor(message: string, loc: SourceLocation | null) { - if (loc) { - let source = loc[0] - let start = createLineTable(source.code).find(loc[1]) - message = `${source.file}:${start.line}:${start.column + 1}: ${message}` - } - - super(message) - - this.name = 'CssSyntaxError' - this.loc = loc - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, CssSyntaxError) - } - } -} - export function parse(input: string, opts?: ParseOptions) { let source: Source | null = opts?.from ? { file: opts.from, code: input } : null @@ -128,24 +103,19 @@ export function parse(input: string, opts?: ParseOptions) { // ^^^^^^^^^^^^^ // } // ``` - // - // The escape character `\` in comments are ignored, this means that a - // the end of a comment preceded by `\` does _not_ mean that the end of the - // comment is escaped and therefore we have to keep parsing. - // - // This is a valid comment: - // ``` - // /*C:\*/ - // ``` - // See: https://www.w3.org/TR/css-syntax-3/#consume-comment else if (currentChar === SLASH && input.charCodeAt(i + 1) === ASTERISK) { let start = i for (let j = i + 2; j < input.length; j++) { peekChar = input.charCodeAt(j) - // End of the comment. - if (peekChar === ASTERISK && input.charCodeAt(j + 1) === SLASH) { + // Current character is a `\` therefore the next character is escaped. + if (peekChar === BACKSLASH) { + j += 1 + } + + // End of the comment + else if (peekChar === ASTERISK && input.charCodeAt(j + 1) === SLASH) { i = j + 1 break } @@ -168,7 +138,7 @@ export function parse(input: string, opts?: ParseOptions) { // Start of a string. else if (currentChar === SINGLE_QUOTE || currentChar === DOUBLE_QUOTE) { - let end = parseString(input, i, currentChar, source) + let end = parseString(input, i, currentChar) // Adjust `buffer` to include the string. buffer += input.slice(i, end + 1) @@ -222,16 +192,20 @@ export function parse(input: string, opts?: ParseOptions) { // Start of a string. else if (peekChar === SINGLE_QUOTE || peekChar === DOUBLE_QUOTE) { - j = parseString(input, j, peekChar, source) + j = parseString(input, j, peekChar) } // Start of a comment. else if (peekChar === SLASH && input.charCodeAt(j + 1) === ASTERISK) { for (let k = j + 2; k < input.length; k++) { peekChar = input.charCodeAt(k) + // Current character is a `\` therefore the next character is escaped. + if (peekChar === BACKSLASH) { + k += 1 + } - // End of the comment. - if (peekChar === ASTERISK && input.charCodeAt(k + 1) === SLASH) { + // End of the comment + else if (peekChar === ASTERISK && input.charCodeAt(k + 1) === SLASH) { j = k + 1 break } @@ -295,12 +269,7 @@ export function parse(input: string, opts?: ParseOptions) { } let declaration = parseDeclaration(buffer, colonIdx) - if (!declaration) { - throw new CssSyntaxError( - `Invalid custom property, expected a value`, - source ? [source, start, i] : null, - ) - } + if (!declaration) throw new Error(`Invalid custom property, expected a value`) if (source) { declaration.src = [source, start, i] @@ -365,10 +334,7 @@ export function parse(input: string, opts?: ParseOptions) { let declaration = parseDeclaration(buffer) if (!declaration) { if (buffer.length === 0) continue - throw new CssSyntaxError( - `Invalid declaration: \`${buffer.trim()}\``, - source ? [source, bufferStart, i] : null, - ) + throw new Error(`Invalid declaration: \`${buffer.trim()}\``) } if (source) { @@ -425,7 +391,7 @@ export function parse(input: string, opts?: ParseOptions) { closingBracketStack[closingBracketStack.length - 1] !== ')' ) { if (closingBracketStack === '') { - throw new CssSyntaxError('Missing opening {', source ? [source, i, i] : null) + throw new Error('Missing opening {') } closingBracketStack = closingBracketStack.slice(0, -1) @@ -487,12 +453,7 @@ export function parse(input: string, opts?: ParseOptions) { // Attach the declaration to the parent. if (parent) { let node = parseDeclaration(buffer, colonIdx) - if (!node) { - throw new CssSyntaxError( - `Invalid declaration: \`${buffer.trim()}\``, - source ? [source, bufferStart, i] : null, - ) - } + if (!node) throw new Error(`Invalid declaration: \`${buffer.trim()}\``) if (source) { node.src = [source, bufferStart, i] @@ -531,7 +492,7 @@ export function parse(input: string, opts?: ParseOptions) { // `)` else if (currentChar === CLOSE_PAREN) { if (closingBracketStack[closingBracketStack.length - 1] !== ')') { - throw new CssSyntaxError('Missing opening (', source ? [source, i, i] : null) + throw new Error('Missing opening (') } closingBracketStack = closingBracketStack.slice(0, -1) @@ -573,17 +534,10 @@ export function parse(input: string, opts?: ParseOptions) { // have a leftover `parent`, then it means that we have an unterminated block. if (closingBracketStack.length > 0 && parent) { if (parent.kind === 'rule') { - throw new CssSyntaxError( - `Missing closing } at ${parent.selector}`, - parent.src ? [parent.src[0], parent.src[1], parent.src[1]] : null, - ) + throw new Error(`Missing closing } at ${parent.selector}`) } - if (parent.kind === 'at-rule') { - throw new CssSyntaxError( - `Missing closing } at ${parent.name} ${parent.params}`, - parent.src ? [parent.src[0], parent.src[1], parent.src[1]] : null, - ) + throw new Error(`Missing closing } at ${parent.name} ${parent.params}`) } } @@ -640,12 +594,7 @@ function parseDeclaration( ) } -function parseString( - input: string, - startIdx: number, - quoteChar: number, - source: Source | null = null, -): number { +function parseString(input: string, startIdx: number, quoteChar: number): number { let peekChar: number // We need to ensure that the closing quote is the same as the opening @@ -687,9 +636,8 @@ function parseString( (input.charCodeAt(i + 1) === LINE_BREAK || (input.charCodeAt(i + 1) === CARRIAGE_RETURN && input.charCodeAt(i + 2) === LINE_BREAK)) ) { - throw new CssSyntaxError( + throw new Error( `Unterminated string: ${input.slice(startIdx, i + 1) + String.fromCharCode(quoteChar)}`, - source ? [source, startIdx, i + 1] : null, ) } @@ -707,9 +655,8 @@ function parseString( peekChar === LINE_BREAK || (peekChar === CARRIAGE_RETURN && input.charCodeAt(i + 1) === LINE_BREAK) ) { - throw new CssSyntaxError( + throw new Error( `Unterminated string: ${input.slice(startIdx, i) + String.fromCharCode(quoteChar)}`, - source ? [source, startIdx, i + 1] : null, ) } } diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 7786790f6..384746b57 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -1,5 +1,5 @@ import { Polyfills } from '.' -import { optimizeAst, toCss, type AstNode } from './ast' +import { optimizeAst, toCss } from './ast' import { parseCandidate, parseVariant, @@ -19,13 +19,11 @@ import { type VariantEntry, } from './intellisense' import { getClassOrder } from './sort' -import type { SourceLocation } from './source-maps/source' import { Theme, ThemeOptions, type ThemeKey } from './theme' import { Utilities, createUtilities, withAlpha } from './utilities' import { DefaultMap } from './utils/default-map' import { extractUsedVariables } from './utils/variables' import { Variants, createVariants, substituteAtVariant } from './variants' -import { WalkAction, walk } from './walk' export const enum CompileAstFlags { None = 0, @@ -61,22 +59,16 @@ export type DesignSystem = { // Used by IntelliSense candidatesToCss(classes: string[]): (string | null)[] - candidatesToAst(classes: string[]): AstNode[][] // General purpose storage storage: Record } -export function buildDesignSystem( - theme: Theme, - utilitiesSrc?: SourceLocation | undefined, -): DesignSystem { +export function buildDesignSystem(theme: Theme): DesignSystem { let utilities = createUtilities(theme) let variants = createVariants(theme) let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem)) - let cachedVariantOrder: Map | null = null - let cachedVariantOrderSize = -1 let parsedCandidates = new DefaultMap((candidate) => Array.from(parseCandidate(candidate, designSystem)), ) @@ -86,16 +78,20 @@ export function buildDesignSystem( let ast = compileAstNodes(candidate, designSystem, flags) try { - let nodes = ast.map((value) => value.node) - // Arbitrary values (`text-[theme(--color-red-500)]`) and arbitrary // properties (`[--my-var:theme(--color-red-500)]`) can contain function // calls so we need evaluate any functions we find there that weren't in // the source CSS. - substituteFunctions(nodes, designSystem) + substituteFunctions( + ast.map(({ node }) => node), + designSystem, + ) // JS plugins might contain an `@variant` inside a generated utility - substituteAtVariant(nodes, designSystem) + substituteAtVariant( + ast.map(({ node }) => node), + designSystem, + ) } catch (err) { // If substitution fails then the candidate likely contains a call to // `theme()` that is invalid which may be because of incorrect usage, @@ -113,44 +109,6 @@ export function buildDesignSystem( } }) - function candidatesToAst(classes: string[]): AstNode[][] { - let result: AstNode[][] = [] - - for (let className of classes) { - let wasValid = true - - let { astNodes } = compileCandidates([className], designSystem, { - onInvalidCandidate() { - wasValid = false - }, - }) - - if (utilitiesSrc) { - walk(astNodes, (node) => { - // We do this conditionally to preserve source locations from both - // `@utility` and `@custom-variant`. Even though generated nodes are - // cached this should be fine because `utilitiesNode.src` should not - // change without a full rebuild which destroys the cache. - node.src ??= utilitiesSrc - return WalkAction.Continue - }) - } - - // Disable all polyfills to not unnecessarily pollute IntelliSense output - astNodes = optimizeAst(astNodes, designSystem, Polyfills.None) - - result.push(wasValid ? astNodes : []) - } - - return result - } - - function candidatesToCss(classes: string[]): (string | null)[] { - return candidatesToAst(classes).map((nodes) => { - return nodes.length > 0 ? toCss(nodes) : null - }) - } - let designSystem: DesignSystem = { theme, utilities, @@ -159,8 +117,30 @@ export function buildDesignSystem( invalidCandidates: new Set(), important: false, - candidatesToCss, - candidatesToAst, + candidatesToCss(classes: string[]) { + let result: (string | null)[] = [] + + for (let className of classes) { + let wasInvalid = false + + let { astNodes } = compileCandidates([className], this, { + onInvalidCandidate() { + wasInvalid = true + }, + }) + + // Disable all polyfills to not unnecessarily pollute IntelliSense output + astNodes = optimizeAst(astNodes, designSystem, Polyfills.None) + + if (astNodes.length === 0 || wasInvalid) { + result.push(null) + } else { + result.push(toCss(astNodes)) + } + } + + return result + }, getClassOrder(classes) { return getClassOrder(this, classes) @@ -190,11 +170,6 @@ export function buildDesignSystem( }, getVariantOrder() { - // parsedVariants only grows, so the cache should be reset only when the size changes - if (cachedVariantOrder !== null && cachedVariantOrderSize === parsedVariants.size) { - return cachedVariantOrder - } - let variants = Array.from(parsedVariants.values()) variants.sort((a, z) => this.variants.compare(a, z)) @@ -216,9 +191,6 @@ export function buildDesignSystem( prevVariant = variant } - cachedVariantOrder = order - cachedVariantOrderSize = parsedVariants.size - return order }, diff --git a/packages/tailwindcss/src/expand-declaration.test.ts b/packages/tailwindcss/src/expand-declaration.test.ts index eb3d5ffb2..1f9e71561 100644 --- a/packages/tailwindcss/src/expand-declaration.test.ts +++ b/packages/tailwindcss/src/expand-declaration.test.ts @@ -23,7 +23,7 @@ function expand(input: string, options: SignatureFeatures): string { describe('expand declarations', () => { let options = SignatureFeatures.ExpandProperties - test('inset', () => { + test('expand to 4 properties', () => { let input = css` .one { inset: 10px; @@ -71,7 +71,7 @@ describe('expand declarations', () => { `) }) - test('gap', () => { + test('expand to 2 properties', () => { let input = css` .one { gap: 10px; @@ -95,294 +95,6 @@ describe('expand declarations', () => { `) }) - test('border-width', () => { - let input = css` - .one { - border-width: 1px; - } - - .two { - border-width: 1px 2px; - } - - .three { - border-width: 1px 2px 3px; - } - - .four { - border-width: 1px 2px 3px 4px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".one { - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - } - .two { - border-top-width: 1px; - border-right-width: 2px; - border-bottom-width: 1px; - border-left-width: 2px; - } - .three { - border-top-width: 1px; - border-right-width: 2px; - border-bottom-width: 3px; - border-left-width: 2px; - } - .four { - border-top-width: 1px; - border-right-width: 2px; - border-bottom-width: 3px; - border-left-width: 4px; - } - " - `) - }) - - test('border-style', () => { - let input = css` - .one { - border-style: solid; - } - - .two { - border-style: solid dashed; - } - - .three { - border-style: solid dashed dotted; - } - - .four { - border-style: solid dashed dotted double; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".one { - border-top-style: solid; - border-right-style: solid; - border-bottom-style: solid; - border-left-style: solid; - } - .two { - border-top-style: solid; - border-right-style: dashed; - border-bottom-style: solid; - border-left-style: dashed; - } - .three { - border-top-style: solid; - border-right-style: dashed; - border-bottom-style: dotted; - border-left-style: dashed; - } - .four { - border-top-style: solid; - border-right-style: dashed; - border-bottom-style: dotted; - border-left-style: double; - } - " - `) - }) - - test('border-color', () => { - let input = css` - .one { - border-color: red; - } - - .two { - border-color: red green; - } - - .three { - border-color: red green blue; - } - - .four { - border-color: red green blue black; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".one { - border-top-color: red; - border-right-color: red; - border-bottom-color: red; - border-left-color: red; - } - .two { - border-top-color: red; - border-right-color: green; - border-bottom-color: red; - border-left-color: green; - } - .three { - border-top-color: red; - border-right-color: green; - border-bottom-color: blue; - border-left-color: green; - } - .four { - border-top-color: red; - border-right-color: green; - border-bottom-color: blue; - border-left-color: black; - } - " - `) - }) - - test('scroll-margin', () => { - let input = css` - .one { - scroll-margin: 1px; - } - - .two { - scroll-margin: 1px 2px; - } - - .three { - scroll-margin: 1px 2px 3px; - } - - .four { - scroll-margin: 1px 2px 3px 4px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".one { - scroll-margin-top: 1px; - scroll-margin-right: 1px; - scroll-margin-bottom: 1px; - scroll-margin-left: 1px; - } - .two { - scroll-margin-top: 1px; - scroll-margin-right: 2px; - scroll-margin-bottom: 1px; - scroll-margin-left: 2px; - } - .three { - scroll-margin-top: 1px; - scroll-margin-right: 2px; - scroll-margin-bottom: 3px; - scroll-margin-left: 2px; - } - .four { - scroll-margin-top: 1px; - scroll-margin-right: 2px; - scroll-margin-bottom: 3px; - scroll-margin-left: 4px; - } - " - `) - }) - - test('scroll-padding', () => { - let input = css` - .one { - scroll-padding: 1px; - } - - .two { - scroll-padding: 1px 2px; - } - - .three { - scroll-padding: 1px 2px 3px; - } - - .four { - scroll-padding: 1px 2px 3px 4px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".one { - scroll-padding-top: 1px; - scroll-padding-right: 1px; - scroll-padding-bottom: 1px; - scroll-padding-left: 1px; - } - .two { - scroll-padding-top: 1px; - scroll-padding-right: 2px; - scroll-padding-bottom: 1px; - scroll-padding-left: 2px; - } - .three { - scroll-padding-top: 1px; - scroll-padding-right: 2px; - scroll-padding-bottom: 3px; - scroll-padding-left: 2px; - } - .four { - scroll-padding-top: 1px; - scroll-padding-right: 2px; - scroll-padding-bottom: 3px; - scroll-padding-left: 4px; - } - " - `) - }) - - test('overflow', () => { - let input = css` - .one { - overflow: clip; - } - - .two { - overflow: hidden visible; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".one { - overflow-x: clip; - overflow-y: clip; - } - .two { - overflow-x: hidden; - overflow-y: visible; - } - " - `) - }) - - test('overscroll-behavior', () => { - let input = css` - .one { - overscroll-behavior: none; - } - - .two { - overscroll-behavior: auto contain; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".one { - overscroll-behavior-x: none; - overscroll-behavior-y: none; - } - .two { - overscroll-behavior-x: auto; - overscroll-behavior-y: contain; - } - " - `) - }) - test('expansion with `!important`', () => { let input = css` .one { @@ -435,22 +147,6 @@ describe('expand declarations', () => { describe('expand logical properties', () => { let options = SignatureFeatures.ExpandProperties | SignatureFeatures.LogicalToPhysical - test('margin-inline', () => { - let input = css` - .example { - margin-inline: 10px 20px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - margin-left: 10px; - margin-right: 20px; - } - " - `) - }) - test('margin-block', () => { let input = css` .example { @@ -466,164 +162,4 @@ describe('expand logical properties', () => { " `) }) - - test('padding-inline', () => { - let input = css` - .example { - padding-inline: 10px 20px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - padding-left: 10px; - padding-right: 20px; - } - " - `) - }) - - test('padding-block', () => { - let input = css` - .example { - padding-block: 10px 20px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - padding-top: 10px; - padding-bottom: 20px; - } - " - `) - }) - - test('scroll-margin-inline', () => { - let input = css` - .example { - scroll-margin-inline: 10px 20px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - scroll-margin-left: 10px; - scroll-margin-right: 20px; - } - " - `) - }) - - test('scroll-margin-block', () => { - let input = css` - .example { - scroll-margin-block: 10px 20px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - scroll-margin-top: 10px; - scroll-margin-bottom: 20px; - } - " - `) - }) - - test('scroll-padding-inline', () => { - let input = css` - .example { - scroll-padding-inline: 10px 20px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - scroll-padding-left: 10px; - scroll-padding-right: 20px; - } - " - `) - }) - - test('scroll-padding-block', () => { - let input = css` - .example { - scroll-padding-block: 10px 20px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - scroll-padding-top: 10px; - scroll-padding-bottom: 20px; - } - " - `) - }) - - test('border-inline-width', () => { - let input = css` - .example { - border-inline-width: 1px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - border-left-width: 1px; - border-right-width: 1px; - } - " - `) - }) - - test('border-block-width', () => { - let input = css` - .example { - border-block-width: 1px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - border-bottom-width: 1px; - border-top-width: 1px; - } - " - `) - }) - - test('border-inline-style', () => { - let input = css` - .example { - border-inline-style: 1px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - border-left-style: 1px; - border-right-style: 1px; - } - " - `) - }) - - test('border-block-style', () => { - let input = css` - .example { - border-block-style: 1px; - } - ` - - expect(expand(input, options)).toMatchInlineSnapshot(` - ".example { - border-bottom-style: 1px; - border-top-style: 1px; - } - " - `) - }) }) diff --git a/packages/tailwindcss/src/expand-declaration.ts b/packages/tailwindcss/src/expand-declaration.ts index 7cef98952..628848ab4 100644 --- a/packages/tailwindcss/src/expand-declaration.ts +++ b/packages/tailwindcss/src/expand-declaration.ts @@ -35,32 +35,7 @@ let VARIADIC_EXPANSION_MAP: Record { // This is the v4 equivalent of `important: "#app"` from v3 - expect( - await run( - ['underline', 'hover:line-through'], - css` - #app { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " - #app .underline { - text-decoration-line: underline; + let input = css` + #app { + @tailwind utilities; } + ` - @media (hover: hover) { - #app .hover\\:line-through:hover { - text-decoration-line: line-through; + let compiler = await compile(input) + + expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + "#app { + .underline { + text-decoration-line: underline; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } } } " @@ -31,29 +33,27 @@ test('Utilities can be wrapped in a selector', async () => { test('Utilities can be marked with important', async () => { // This is the v4 equivalent of `important: true` from v3 - expect( - await run( - ['underline', 'hover:line-through'], - css` - @import 'tailwindcss/utilities' important; - `, - { - loadStylesheet: async (_id: string, base: string) => ({ - base, - content: '@tailwind utilities;', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .underline { + let input = css` + @import 'tailwindcss/utilities' important; + ` + + let compiler = await compile(input, { + loadStylesheet: async (id: string, base: string) => ({ + base, + content: '@tailwind utilities;', + path: '', + }), + }) + + expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + ".underline { text-decoration-line: underline !important; } - - @media (hover: hover) { - .hover\\:line-through:hover { - text-decoration-line: line-through !important; + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through !important; + } } } " @@ -63,26 +63,27 @@ test('Utilities can be marked with important', async () => { test('Utilities can be wrapped with a selector and marked as important', async () => { // This does not have a direct equivalent in v3 but works as a consequence of // the new APIs - expect( - await run( - ['underline', 'hover:line-through'], - css` - @media important { - #app { - @tailwind utilities; + let input = css` + @media important { + #app { + @tailwind utilities; + } + } + ` + + let compiler = await compile(input) + + expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + "#app { + .underline { + text-decoration-line: underline !important; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through !important; } } - `, - ), - ).toMatchInlineSnapshot(` - " - #app .underline { - text-decoration-line: underline !important; - } - - @media (hover: hover) { - #app .hover\\:line-through:hover { - text-decoration-line: line-through !important; } } " @@ -91,18 +92,17 @@ test('Utilities can be wrapped with a selector and marked as important', async ( test('variables in utilities should not be marked as important', async () => { expect( - await run( - ['ease-out!', 'z-10!'], + await compileCss( css` @theme { --ease-out: cubic-bezier(0, 0, 0.2, 1); } @tailwind utilities; `, + ['ease-out!', 'z-10!'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-ease: initial; @@ -126,7 +126,6 @@ test('variables in utilities should not be marked as important', async () => { @property --tw-ease { syntax: "*"; inherits: false - } - " + }" `) }) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 7f4a0eef3..5cd5282b0 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -2,18 +2,16 @@ import fs from 'node:fs' import path from 'node:path' import { describe, expect, it, test } from 'vitest' import { compile, Features, Polyfills } from '.' -import { cartesian } from './cartesian' import type { PluginAPI } from './compat/plugin-api' import plugin from './plugin' -import { compileCss, run } from './test-utils/run' +import { compileCss, optimizeCss, run } from './test-utils/run' const css = String.raw describe('compiling CSS', () => { test('`@tailwind utilities` is replaced with the generated utility classes', async () => { expect( - await run( - ['flex', 'md:grid', 'hover:underline', 'dark:bg-black'], + await compileCss( css` @theme { --color-black: #000; @@ -24,10 +22,10 @@ describe('compiling CSS', () => { @tailwind utilities; } `, + ['flex', 'md:grid', 'hover:underline', 'dark:bg-black'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-black: #000; } @@ -53,15 +51,13 @@ describe('compiling CSS', () => { background-color: var(--color-black); } } - } - " + }" `) }) test('that only CSS variables are allowed', () => { return expect( - run( - ['bg-primary'], + compileCss( css` @theme { --color-primary: red; @@ -71,6 +67,7 @@ describe('compiling CSS', () => { } @tailwind utilities; `, + ['bg-primary'], ), ).rejects.toThrowErrorMatchingInlineSnapshot(` [Error: \`@theme\` blocks must only contain custom properties or \`@keyframes\`. @@ -86,23 +83,21 @@ describe('compiling CSS', () => { test('`@tailwind utilities` is only processed once', async () => { expect( - await run( - ['flex', 'grid'], + await compileCss( css` @tailwind utilities; @tailwind utilities; `, + ['flex', 'grid'], ), ).toMatchInlineSnapshot(` - " - .flex { + ".flex { display: flex; } .grid { display: grid; - } - " + }" `) }) @@ -110,24 +105,24 @@ describe('compiling CSS', () => { let defaultTheme = fs.readFileSync(path.resolve(__dirname, '..', 'theme.css'), 'utf-8') expect( - await run( - ['bg-red-500', 'w-4', 'sm:flex', 'shadow-sm'], + await compileCss( css` ${defaultTheme} @tailwind utilities; `, + ['bg-red-500', 'w-4', 'sm:flex', 'shadow-sm'], ), ).toMatchSnapshot() }) test('prefix all CSS variables inside preflight', async () => { expect( - await run( - ['font-mono'], + await compileCss( css` @import 'tailwindcss' prefix(tw); @tailwind utilities; `, + ['font-mono'], { async loadStylesheet(id) { return { @@ -146,12 +141,7 @@ describe('compiling CSS', () => { test('unescapes underscores to spaces inside arbitrary values except for `url()` and first argument of `var()` and `theme()`', async () => { expect( - await run( - [ - 'bg-[no-repeat_url(./my_file.jpg)]', - 'ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]', - 'ml-[theme(--spacing-1_5,theme(--spacing-2_5,_1rem)))]', - ], + await compileCss( css` @theme { --spacing-1_5: 1.5rem; @@ -159,10 +149,14 @@ describe('compiling CSS', () => { } @tailwind utilities; `, + [ + 'bg-[no-repeat_url(./my_file.jpg)]', + 'ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]', + 'ml-[theme(--spacing-1_5,theme(--spacing-2_5,_1rem)))]', + ], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-1_5: 1.5rem; --spacing-2_5: 2.5rem; } @@ -177,15 +171,13 @@ describe('compiling CSS', () => { .bg-\\[no-repeat_url\\(\\.\\/my_file\\.jpg\\)\\] { background-color: no-repeat url("./my_file.jpg"); - } - " + }" `) }) test('unescapes theme variables and handles dots as underscore', async () => { expect( - await run( - ['m-1.5', 'm-2.5', 'm-2_5', 'm-3.5', 'm-foo/bar'], + await compileCss( css` @theme { --spacing-*: initial; @@ -197,10 +189,10 @@ describe('compiling CSS', () => { } @tailwind utilities; `, + ['m-1.5', 'm-2.5', 'm-2_5', 'm-3.5', 'm-foo/bar'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-1\\.5: 1.5px; --spacing-2_5: 2.5px; --spacing-3\\.5: 3.5px; @@ -221,20 +213,24 @@ describe('compiling CSS', () => { .m-foo\\/bar { margin: var(--spacing-foo\\/bar); - } - " + }" `) }) test('adds vendor prefixes', async () => { - expect(await run(['[text-size-adjust:none]'])).toMatchInlineSnapshot(` - " - .\\[text-size-adjust\\:none\\] { + expect( + await compileCss( + css` + @tailwind utilities; + `, + ['[text-size-adjust:none]'], + ), + ).toMatchInlineSnapshot(` + ".\\[text-size-adjust\\:none\\] { -webkit-text-size-adjust: none; -moz-text-size-adjust: none; text-size-adjust: none; - } - " + }" `) }) }) @@ -242,32 +238,27 @@ describe('compiling CSS', () => { describe('arbitrary properties', () => { it('should generate arbitrary properties', async () => { expect(await run(['[color:red]'])).toMatchInlineSnapshot(` - " - .\\[color\\:red\\] { + ".\\[color\\:red\\] { color: red; - } - " + }" `) }) it('should generate arbitrary properties with modifiers', async () => { expect(await run(['[color:red]/50'])).toMatchInlineSnapshot(` - " - .\\[color\\:red\\]\\/50 { + ".\\[color\\:red\\]\\/50 { color: oklab(62.7955% .224 .125 / .5); - } - " + }" `) }) it('should not generate arbitrary properties with invalid modifiers', async () => { - expect(await run(['[color:red]/not-a-percentage'])).toEqual('') + expect(await run(['[color:red]/not-a-percentage'])).toMatchInlineSnapshot(`""`) }) it('should generate arbitrary properties with variables and with modifiers', async () => { expect(await run(['[color:var(--my-color)]/50'])).toMatchInlineSnapshot(` - " - .\\[color\\:var\\(--my-color\\)\\]\\/50 { + ".\\[color\\:var\\(--my-color\\)\\]\\/50 { color: var(--my-color); } @@ -275,8 +266,7 @@ describe('arbitrary properties', () => { .\\[color\\:var\\(--my-color\\)\\]\\/50 { color: color-mix(in oklab, var(--my-color) 50%, transparent); } - } - " + }" `) }) }) @@ -318,6 +308,7 @@ describe('@apply', () => { @apply p-2; } `, + [], { async loadStylesheet() { return { @@ -334,15 +325,13 @@ describe('@apply', () => { }, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; } .foo { padding: calc(var(--spacing) * 2); - } - " + }" `) }) @@ -356,6 +345,7 @@ describe('@apply', () => { @apply p-2; } `, + [], { async loadStylesheet() { return { @@ -372,11 +362,9 @@ describe('@apply', () => { }, ), ).toMatchInlineSnapshot(` - " - .foo { + ".foo { padding: calc(var(--spacing, .25rem) * 2); - } - " + }" `) }) @@ -410,8 +398,7 @@ describe('@apply', () => { } `), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-translate-x: 0; @@ -482,8 +469,7 @@ describe('@apply', () => { to { transform: rotate(360deg); } - } - " + }" `) }) @@ -494,6 +480,7 @@ describe('@apply', () => { @import './bar.css'; @tailwind utilities; `, + [], { async loadStylesheet() { return { @@ -509,11 +496,9 @@ describe('@apply', () => { }, ), ).toMatchInlineSnapshot(` - " - .foo { + ".foo { text-decoration-line: underline; - } - " + }" `) }) @@ -531,8 +516,7 @@ describe('@apply', () => { } `), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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: ""; @@ -549,8 +533,7 @@ describe('@apply', () => { syntax: "*"; inherits: false; initial-value: ""; - } - " + }" `) }) @@ -568,8 +551,7 @@ describe('@apply', () => { } `), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-leading: initial; @@ -595,8 +577,7 @@ describe('@apply', () => { @property --tw-leading { syntax: "*"; inherits: false - } - " + }" `) }) @@ -641,11 +622,9 @@ describe('@apply', () => { } `), ).toMatchInlineSnapshot(` - " - .foo { + ".foo { display: flex; - } - " + }" `) }) @@ -672,22 +651,19 @@ describe('@apply', () => { @tailwind utilities; `), ).toMatchInlineSnapshot(` - " - .foo:before { + ".foo:before { content: "bar"; } .foo:after { content: "baz"; - } - " + }" `) }) it('should recursively apply with custom `@utility`, which is used before it is defined', async () => { expect( - await run( - ['a', 'b', 'c', 'flex', 'my-flex'], + await compileCss( css` @tailwind utilities; @@ -713,10 +689,10 @@ describe('@apply', () => { @apply flex; } `, + ['a', 'b', 'c', 'flex', 'my-flex'], ), ).toMatchInlineSnapshot(` - " - .a:focus, .b:focus, .c { + ".a:focus, .b:focus, .c { display: flex !important; } @@ -728,16 +704,14 @@ describe('@apply', () => { body:focus { display: flex !important; } - } - " + }" `) }) // https://github.com/tailwindlabs/tailwindcss/issues/16935 it('should not swallow @utility declarations when @apply is used in nested rules', async () => { expect( - await run( - ['custom-utility'], + await compileCss( css` @tailwind utilities; @@ -752,10 +726,10 @@ describe('@apply', () => { @apply flex; } `, + ['custom-utility'], ), ).toMatchInlineSnapshot(` - " - .custom-utility { + ".custom-utility { display: flex; } @@ -765,16 +739,14 @@ describe('@apply', () => { .ignore-me div { display: flex; - } - " + }" `) }) // https://github.com/tailwindlabs/tailwindcss/issues/17924 it('should correctly apply nested usages of @apply when one @utility applies another', async () => { expect( - await run( - ['foo', 'test', 'test2'], + await compileCss( css` @theme { --color-green-500: green; @@ -802,10 +774,10 @@ describe('@apply', () => { @apply test2; } `, + ['foo', 'test', 'test2'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-green-500: green; --color-red-500: red; --color-indigo-500: indigo; @@ -845,16 +817,14 @@ describe('@apply', () => { .foo:disabled { background-color: var(--color-indigo-500); - } - " + }" `) }) // https://github.com/tailwindlabs/tailwindcss/issues/18400 it('should ignore the design systems `important` flag when using @apply', async () => { expect( - await run( - ['flex'], + await compileCss( css` @import 'tailwindcss/utilities' important; .flex-explicitly-important { @@ -864,6 +834,7 @@ describe('@apply', () => { @apply flex; } `, + ['flex'], { async loadStylesheet(_, base) { return { @@ -875,108 +846,33 @@ describe('@apply', () => { }, ), ).toMatchInlineSnapshot(` - " - .flex, .flex-explicitly-important { + ".flex, .flex-explicitly-important { display: flex !important; } .flex-not-important { display: flex; - } - " + }" `) }) - - it('should be usable with CSS mixins', async () => { - let input = css` - .foo { - /* Utility usage */ - @apply underline; - - /* CSS mixin usage */ - @apply --my-mixin-1; - @apply --my-mixin-1(); - @apply --my-mixin-1 --my-mixin-2; - @apply --my-mixin-1() --my-mixin-2(); - @apply --my-mixin-3 { - color: red; - } - } - ` - - expect(await compileCss(input)).toMatchInlineSnapshot(` - " - .foo { - text-decoration-line: underline; - @apply --my-mixin-1; - @apply --my-mixin-1(); - @apply --my-mixin-1 --my-mixin-2; - @apply --my-mixin-1() --my-mixin-2(); - @apply --my-mixin-3 { - color: red; - } - } - " - `) - }) - - it('should error when trying to use mixins and utilities together', async () => { - await expect( - compile(css` - .foo { - @apply underline --my-mixin-1; - } - `), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply --my-mixin-1\` into a separate rule.]`, - ) - - await expect( - compile(css` - .foo { - @apply --my-mixin-1 underline; - } - `), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply --my-mixin-1\` into a separate rule.]`, - ) - }) - - it('should error when used with a body', async () => { - await expect( - compile(css` - .foo { - @apply underline { - color: red; - } - } - `), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: The rule \`@apply underline\` must not have a body.]`, - ) - }) }) describe('arbitrary variants', () => { it('should generate arbitrary variants', async () => { expect(await run(['[&_p]:flex'])).toMatchInlineSnapshot(` - " - .\\[\\&_p\\]\\:flex p { + ".\\[\\&_p\\]\\:flex p { display: flex; - } - " + }" `) }) it('should generate arbitrary at-rule variants', async () => { expect(await run(['[@media(width>=123px)]:flex'])).toMatchInlineSnapshot(` - " - @media (min-width: 123px) { + "@media (min-width: 123px) { .\\[\\@media\\(width\\>\\=123px\\)\\]\\:flex { display: flex; } - } - " + }" `) }) @@ -988,44 +884,37 @@ describe('arbitrary variants', () => { describe('variant stacking', () => { it('should stack simple variants', async () => { expect(await run(['focus:hover:flex'])).toMatchInlineSnapshot(` - " - @media (hover: hover) { + "@media (hover: hover) { .focus\\:hover\\:flex:focus:hover { display: flex; } - } - " + }" `) }) it('should stack arbitrary variants and simple variants', async () => { expect(await run(['[&_p]:hover:flex'])).toMatchInlineSnapshot(` - " - @media (hover: hover) { + "@media (hover: hover) { .\\[\\&_p\\]\\:hover\\:flex p:hover { display: flex; } - } - " + }" `) }) it('should stack multiple arbitrary variants', async () => { expect(await run(['[&_p]:[@media(width>=123px)]:flex'])).toMatchInlineSnapshot(` - " - @media (min-width: 123px) { + "@media (min-width: 123px) { .\\[\\&_p\\]\\:\\[\\@media\\(width\\>\\=123px\\)\\]\\:flex p { display: flex; } - } - " + }" `) }) it('pseudo element variants are re-ordered', async () => { expect(await run(['before:hover:flex', 'hover:before:flex'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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: ""; @@ -1052,8 +941,7 @@ describe('variant stacking', () => { syntax: "*"; inherits: false; initial-value: ""; - } - " + }" `) }) }) @@ -1061,28 +949,23 @@ describe('variant stacking', () => { describe('important', () => { it('should generate an important utility', async () => { expect(await run(['underline!'])).toMatchInlineSnapshot(` - " - .underline\\! { + ".underline\\! { text-decoration-line: underline !important; - } - " + }" `) }) it('should generate an important utility with legacy syntax', async () => { expect(await run(['!underline'])).toMatchInlineSnapshot(` - " - .\\!underline { + ".\\!underline { text-decoration-line: underline !important; - } - " + }" `) }) it('should not mark declarations inside of @keyframes as important', async () => { expect( - await run( - ['animate-spin!'], + await compileCss( css` @theme { --animate-spin: spin 1s linear infinite; @@ -1095,10 +978,10 @@ describe('important', () => { } @tailwind utilities; `, + ['animate-spin!'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --animate-spin: spin 1s linear infinite; } @@ -1110,18 +993,15 @@ describe('important', () => { to { transform: rotate(360deg); } - } - " + }" `) }) it('should generate an important arbitrary property utility', async () => { expect(await run(['[color:red]!'])).toMatchInlineSnapshot(` - " - .\\[color\\:red\\]\\! { + ".\\[color\\:red\\]\\! { color: red !important; - } - " + }" `) }) }) @@ -1129,18 +1009,17 @@ describe('important', () => { describe('sorting', () => { it('should sort utilities based on their property order', async () => { expect( - await run( - ['pointer-events-none', 'flex', 'p-1', 'px-1', 'pl-1'].sort(() => Math.random() - 0.5), + await compileCss( css` @theme { --spacing-1: 0.25rem; } @tailwind utilities; `, + ['pointer-events-none', 'flex', 'p-1', 'px-1', 'pl-1'].sort(() => Math.random() - 0.5), ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-1: .25rem; } @@ -1162,15 +1041,13 @@ describe('sorting', () => { .pl-1 { padding-left: var(--spacing-1); - } - " + }" `) }) it('should sort based on amount of properties', async () => { expect(await run(['text-clip', 'truncate', 'overflow-scroll'])).toMatchInlineSnapshot(` - " - .truncate { + ".truncate { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; @@ -1182,8 +1059,7 @@ describe('sorting', () => { .text-clip { text-overflow: clip; - } - " + }" `) }) @@ -1194,8 +1070,7 @@ describe('sorting', () => { */ it('should sort utilities with a custom internal --tw-sort correctly', async () => { expect( - await run( - ['mx-0', 'gap-4', 'space-x-2'].sort(() => Math.random() - 0.5), + await compileCss( css` @theme { --spacing-0: 0px; @@ -1204,10 +1079,10 @@ describe('sorting', () => { } @tailwind utilities; `, + ['mx-0', 'gap-4', 'space-x-2'].sort(() => Math.random() - 0.5), ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-space-x-reverse: 0; @@ -1239,14 +1114,21 @@ describe('sorting', () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) }) it('should sort individual logical properties later than left/right pairs', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-1: 1px; + --spacing-2: 2px; + --spacing-3: 3px; + } + @tailwind utilities; + `, [ // scroll-margin 'scroll-ms-1', @@ -1268,18 +1150,9 @@ describe('sorting', () => { 'pe-2', 'px-3', ].sort(() => Math.random() - 0.5), - css` - @theme { - --spacing-1: 1px; - --spacing-2: 2px; - --spacing-3: 3px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-1: 1px; --spacing-2: 2px; --spacing-3: 3px; @@ -1331,8 +1204,7 @@ describe('sorting', () => { .pe-2 { padding-inline-end: var(--spacing-2); - } - " + }" `) }) @@ -1344,8 +1216,7 @@ describe('sorting', () => { ), ), ).toMatchInlineSnapshot(` - " - .pointer-events-none { + ".pointer-events-none { pointer-events: none; } @@ -1361,8 +1232,7 @@ describe('sorting', () => { .focus\\:pointer-events-none:focus { pointer-events: none; - } - " + }" `) }) @@ -1389,8 +1259,7 @@ describe('sorting', () => { ), ), ).toMatchInlineSnapshot(` - " - .flex { + ".flex { display: flex; } @@ -1412,8 +1281,7 @@ describe('sorting', () => { .disabled\\:flex:disabled { display: flex; - } - " + }" `) }) @@ -1442,44 +1310,43 @@ describe('sorting', () => { ].sort(() => Math.random() - 0.5), ), ).toMatchInlineSnapshot(` - " - @media (hover: hover) { - :is(:where(.group):hover .group-hover\\:flex) { + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { display: flex; } } - :is(:where(.group):focus .group-focus\\:flex) { + .group-focus\\:flex:is(:where(.group):focus *) { display: flex; } @media (hover: hover) { - :is(:where(.peer):hover ~ .peer-hover\\:flex) { + .peer-hover\\:flex:is(:where(.peer):hover ~ *) { display: flex; } @media (hover: hover) { - :is(:where(.peer):hover ~ :is(:where(.group):hover .group-hover\\:peer-hover\\:flex)), :is(:where(.group):hover :is(:where(.peer):hover ~ .peer-hover\\:group-hover\\:flex)) { + .group-hover\\:peer-hover\\:flex:is(:where(.group):hover *):is(:where(.peer):hover ~ *), .peer-hover\\:group-hover\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):hover *) { display: flex; } } - :is(:where(.peer):hover ~ :is(:where(.group):focus .group-focus\\:peer-hover\\:flex)), :is(:where(.group):focus :is(:where(.peer):hover ~ .peer-hover\\:group-focus\\:flex)) { + .group-focus\\:peer-hover\\:flex:is(:where(.group):focus *):is(:where(.peer):hover ~ *), .peer-hover\\:group-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):focus *) { display: flex; } } - :is(:where(.peer):focus ~ .peer-focus\\:flex) { + .peer-focus\\:flex:is(:where(.peer):focus ~ *) { display: flex; } @media (hover: hover) { - :is(:where(.peer):focus ~ :is(:where(.group):hover .group-hover\\:peer-focus\\:flex)), :is(:where(.group):hover :is(:where(.peer):focus ~ .peer-focus\\:group-hover\\:flex)) { + .group-hover\\:peer-focus\\:flex:is(:where(.group):hover *):is(:where(.peer):focus ~ *), .peer-focus\\:group-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.group):hover *) { display: flex; } } - :is(:where(.peer):focus ~ :is(:where(.group):focus .group-focus\\:peer-focus\\:flex)), :is(:where(.group):focus :is(:where(.peer):focus ~ .peer-focus\\:group-focus\\:flex)) { + .group-focus\\:peer-focus\\:flex:is(:where(.group):focus *):is(:where(.peer):focus ~ *), .peer-focus\\:group-focus\\:flex:is(:where(.peer):focus ~ *):is(:where(.group):focus *) { display: flex; } @@ -1487,16 +1354,14 @@ describe('sorting', () => { .hover\\:flex:hover { display: flex; } - } - " + }" `) }) // https://github.com/tailwindlabs/tailwindcss/issues/16973 it('should not take undefined values into account when sorting', async () => { expect( - await run( - ['fancy-text', 'text-sm'], + await compileCss( css` @theme { --text-sm: 0.875rem; @@ -1509,10 +1374,10 @@ describe('sorting', () => { font-weight: var(--font-weight-bold); } `, + ['fancy-text', 'text-sm'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --text-sm: .875rem; --text-sm--line-height: calc(1.25 / .875); } @@ -1526,8 +1391,7 @@ describe('sorting', () => { .text-sm { font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); - } - " + }" `) }) }) @@ -1535,32 +1399,29 @@ describe('sorting', () => { describe('Parsing theme values from CSS', () => { test('Can read values from `@theme`', async () => { expect( - await run( - ['accent-red-500'], + await compileCss( css` @theme { --color-red-500: #f00; } @tailwind utilities; `, + ['accent-red-500'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: red; } .accent-red-500 { accent-color: var(--color-red-500); - } - " + }" `) }) test('Later values from `@theme` override earlier ones', async () => { expect( - await run( - ['accent-red-500'], + await compileCss( css` @theme { --color-red-500: #f00; @@ -1568,24 +1429,22 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['accent-red-500'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #f10; } .accent-red-500 { accent-color: var(--color-red-500); - } - " + }" `) }) test('Multiple `@theme` blocks are merged', async () => { expect( - await run( - ['accent-red-500', 'accent-blue-500'], + await compileCss( css` @theme { --color-red-500: #f00; @@ -1595,10 +1454,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['accent-red-500', 'accent-blue-500'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: red; --color-blue-500: #00f; } @@ -1609,15 +1468,13 @@ describe('Parsing theme values from CSS', () => { .accent-red-500 { accent-color: var(--color-red-500); - } - " + }" `) }) test('`@theme` values with escaped forward slashes map to unescaped slashes in candidate values', async () => { expect( - await run( - ['w-1/2', 'w-75%'], + await compileCss( css` @theme { /* Cursed but we want this to work */ @@ -1626,10 +1483,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['w-1/2', 'w-75%'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --width-1\\/2: 75%; --width-75\\%: 50%; } @@ -1640,23 +1497,13 @@ describe('Parsing theme values from CSS', () => { .w-75\\% { width: var(--width-75\\%); - } - " + }" `) }) - // https://github.com/tailwindlabs/tailwindcss/issues/19786 - test('out-of-range escaped CSS variable candidates do not crash the build', async () => { - // Shouldn't crash - await run([ - String.raw`--Coding-Projects-CharacterMapper-Master-Workspace\d8819554-4725-4235-9d22-2d0ed572e924`, - ]) - }) - test('`@keyframes` in `@theme` are hoisted', async () => { expect( - await run( - ['accent-red', 'text-lg'], + await compileCss( css` @theme { --color-red: red; @@ -1672,10 +1519,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['accent-red', 'text-lg'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red: red; --text-lg: 20px; } @@ -1686,15 +1533,13 @@ describe('Parsing theme values from CSS', () => { .accent-red { accent-color: var(--color-red); - } - " + }" `) }) test('`@keyframes` in `@theme` are generated when name contains a new line', async () => { expect( - await run( - ['animate-very-long-animation-name'], + await compileCss( css` @theme { --animate-very-long-animation-name: very-long-animation-name @@ -1712,12 +1557,11 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['animate-very-long-animation-name'], ), ).toMatchInlineSnapshot(` - " - :root, :host { - --animate-very-long-animation-name: very-long-animation-name - var(--very-long-animation-name-configuration, 2.5s ease-in-out 0s infinite normal none running); + ":root, :host { + --animate-very-long-animation-name: very-long-animation-name var(--very-long-animation-name-configuration, 2.5s ease-in-out 0s infinite normal none running); } .animate-very-long-animation-name { @@ -1728,15 +1572,13 @@ describe('Parsing theme values from CSS', () => { to { opacity: 1; } - } - " + }" `) }) test('`@theme` values can be unset', async () => { expect( - await run( - ['accent-red', 'accent-blue', 'accent-green', 'text-sm', 'text-md'], + await compileCss( css` @theme { --color-red: #f00; @@ -1763,10 +1605,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['accent-red', 'accent-blue', 'accent-green', 'text-sm', 'text-md'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --text-sm: 13px; --color-green: #0f0; } @@ -1777,15 +1619,13 @@ describe('Parsing theme values from CSS', () => { .accent-green { accent-color: var(--color-green); - } - " + }" `) }) test('`@theme` values can be unset (using the escaped syntax)', async () => { expect( - await run( - ['accent-red', 'accent-blue', 'accent-green', 'text-sm', 'text-md'], + await compileCss( css` @theme { --color-red: #f00; @@ -1812,10 +1652,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['accent-red', 'accent-blue', 'accent-green', 'text-sm', 'text-md'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --text-sm: 13px; --color-green: #0f0; } @@ -1826,15 +1666,13 @@ describe('Parsing theme values from CSS', () => { .accent-green { accent-color: var(--color-green); - } - " + }" `) }) test('all `@theme` values can be unset at once', async () => { expect( - await run( - ['accent-red', 'accent-blue', 'accent-green', 'text-sm', 'text-md'], + await compileCss( css` @theme { --color-red: #f00; @@ -1850,24 +1688,22 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['accent-red', 'accent-blue', 'accent-green', 'text-sm', 'text-md'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-green: #0f0; } .accent-green { accent-color: var(--color-green); - } - " + }" `) }) test('unsetting `--font-*` does not unset `--font-weight-*`', async () => { expect( - await run( - ['font-bold', 'font-sans', 'font-serif', 'font-body'], + await compileCss( css` @theme { --font-weight-bold: bold; @@ -1880,10 +1716,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['font-bold', 'font-sans', 'font-serif', 'font-body'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-font-weight: initial; @@ -1908,15 +1744,13 @@ describe('Parsing theme values from CSS', () => { @property --tw-font-weight { syntax: "*"; inherits: false - } - " + }" `) }) test('unsetting `--inset-*` does not unset `--inset-shadow-*`', async () => { expect( - await run( - ['inset-shadow-sm', 'inset-ring-thick', 'inset-lg', 'inset-sm', 'inset-md'], + await compileCss( css` @theme { --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05); @@ -1929,10 +1763,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['inset-shadow-sm', 'inset-ring-thick', 'inset-lg', 'inset-sm', 'inset-md'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-shadow: 0 0 #0000; @@ -2043,22 +1877,13 @@ describe('Parsing theme values from CSS', () => { syntax: "*"; inherits: false; initial-value: 0 0 #0000; - } - " + }" `) }) test('unsetting `--text-*` does not unset `--text-color-*`, `--text-underline-offset-*`, `--text-indent-*`, `--text-decoration-thickness-*` or `--text-decoration-color-*`', async () => { expect( - await run( - [ - 'text-potato', - 'underline-offset-potato', - 'indent-potato', - 'decoration-potato', - 'decoration-salad', - 'text-lg', - ], + await compileCss( css` @theme { --text-color-potato: brown; @@ -2074,10 +1899,17 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + [ + 'text-potato', + 'underline-offset-potato', + 'indent-potato', + 'decoration-potato', + 'decoration-salad', + 'text-lg', + ], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --text-color-potato: brown; --text-underline-offset-potato: 4px; --text-indent-potato: 6px; @@ -2110,15 +1942,13 @@ describe('Parsing theme values from CSS', () => { .underline-offset-potato { text-underline-offset: var(--text-underline-offset-potato); - } - " + }" `) }) test('unused keyframes are removed when an animation is unset', async () => { expect( - await run( - ['animate-foo', 'animate-foobar'], + await compileCss( css` @theme { --animate-foo: foo 1s infinite; @@ -2141,10 +1971,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['animate-foo', 'animate-foobar'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --animate-foobar: foobar 1s infinite; } @@ -2156,15 +1986,13 @@ describe('Parsing theme values from CSS', () => { to { opacity: 0; } - } - " + }" `) }) test('keyframes are generated when used in an animation', async () => { expect( - await run( - ['animate-foo'], + await compileCss( css` @theme { --animate-foo: used 1s infinite; @@ -2185,10 +2013,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['animate-foo'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --animate-foo: used 1s infinite; } @@ -2200,15 +2028,13 @@ describe('Parsing theme values from CSS', () => { to { opacity: 1; } - } - " + }" `) }) test('keyframes are generated when used in an animation within a prefixed setup', async () => { expect( - await run( - ['tw:animate-foo'], + await compileCss( css` @theme prefix(tw) { --animate-foo: used 1s infinite; @@ -2229,10 +2055,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['tw:animate-foo'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --tw-animate-foo: used 1s infinite; } @@ -2244,40 +2070,39 @@ describe('Parsing theme values from CSS', () => { to { opacity: 1; } - } - " + }" `) }) test('custom properties are generated when used from a CSS var with a prefixed setup', async () => { expect( - await compileCss(css` - @theme prefix(tw) { - --color-tomato: #e10c04; - } - @tailwind utilities; - .red { - color: var(--tw-color-tomato); - } - `), + await compileCss( + css` + @theme prefix(tw) { + --color-tomato: #e10c04; + } + @tailwind utilities; + .red { + color: var(--tw-color-tomato); + } + `, + [], + ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --tw-color-tomato: #e10c04; } .red { color: var(--tw-color-tomato); - } - " + }" `) }) // https://github.com/tailwindlabs/tailwindcss/issues/16374 test('custom properties in keyframes preserved', async () => { expect( - await run( - ['animate-foo'], + await compileCss( css` @theme { --animate-foo: used 1s infinite; @@ -2292,10 +2117,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['animate-foo'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --animate-foo: used 1s infinite; } @@ -2308,15 +2133,13 @@ describe('Parsing theme values from CSS', () => { --other: var(--angle); --angle: 360deg; } - } - " + }" `) }) test('keyframes are generated when used in an animation using `@theme inline`', async () => { expect( - await run( - ['animate-foo'], + await compileCss( css` @theme inline { --animate-foo: used 1s infinite; @@ -2337,10 +2160,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['animate-foo'], ), ).toMatchInlineSnapshot(` - " - .animate-foo { + ".animate-foo { animation: 1s infinite used; } @@ -2348,15 +2171,13 @@ describe('Parsing theme values from CSS', () => { to { opacity: 1; } - } - " + }" `) }) test('keyframes are generated when used in an animation using `@theme static`', async () => { expect( - await run( - ['animate-foo'], + await compileCss( css` @theme static { --animate-foo: used 1s infinite; @@ -2377,10 +2198,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['animate-foo'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --animate-foo: used 1s infinite; --animate-bar: unused-but-kept 1s infinite; } @@ -2399,37 +2220,38 @@ describe('Parsing theme values from CSS', () => { to { opacity: 0; } - } - " + }" `) }) test('keyframes are generated when used in user CSS', async () => { expect( - await compileCss(css` - @theme { - @keyframes used { - to { - opacity: 1; + await compileCss( + css` + @theme { + @keyframes used { + to { + opacity: 1; + } + } + + @keyframes unused { + to { + opacity: 0; + } } } - @keyframes unused { - to { - opacity: 0; - } + .foo { + animation: used 1s infinite; } - } - .foo { - animation: used 1s infinite; - } - - @tailwind utilities; - `), + @tailwind utilities; + `, + [], + ), ).toMatchInlineSnapshot(` - " - .foo { + ".foo { animation: 1s infinite used; } @@ -2437,16 +2259,14 @@ describe('Parsing theme values from CSS', () => { to { opacity: 1; } - } - " + }" `) }) // https://github.com/tailwindlabs/tailwindcss/issues/17332 test('extracts keyframe names followed by comma', async () => { expect( - await run( - ['animate-test'], + await compileCss( css` @theme { --animate-test: 500ms both fade-in, 1000ms linear 500ms spin infinite; @@ -2463,10 +2283,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['animate-test'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --animate-test: .5s both fade-in, 1s linear .5s spin infinite; } @@ -2482,37 +2302,38 @@ describe('Parsing theme values from CSS', () => { to { opacity: 1; } - } - " + }" `) }) test('keyframes outside of `@theme are always preserved', async () => { expect( - await compileCss(css` - @theme { - @keyframes used { - to { - opacity: 1; + await compileCss( + css` + @theme { + @keyframes used { + to { + opacity: 1; + } } } - } - @keyframes unused { - to { - opacity: 0; + @keyframes unused { + to { + opacity: 0; + } } - } - .foo { - animation: used 1s infinite; - } + .foo { + animation: used 1s infinite; + } - @tailwind utilities; - `), + @tailwind utilities; + `, + [], + ), ).toMatchInlineSnapshot(` - " - @keyframes unused { + "@keyframes unused { to { opacity: 0; } @@ -2526,15 +2347,13 @@ describe('Parsing theme values from CSS', () => { to { opacity: 1; } - } - " + }" `) }) test('theme values added as reference are not included in the output as variables but emit fallback values', async () => { expect( - await run( - ['bg-tomato', 'bg-potato'], + await compileCss( css` @theme { --color-tomato: #e10c04; @@ -2544,10 +2363,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['bg-tomato', 'bg-potato'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-tomato: #e10c04; } @@ -2557,15 +2376,13 @@ describe('Parsing theme values from CSS', () => { .bg-tomato { background-color: var(--color-tomato); - } - " + }" `) }) test('`@keyframes` added in `@theme reference` should not be emitted', async () => { return expect( - await run( - ['animate-foo'], + await compileCss( css` @theme reference { --animate-foo: foo 1s infinite; @@ -2582,10 +2399,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['animate-foo'], ), ).toMatchInlineSnapshot(` - " - .animate-foo { + ".animate-foo { animation: var(--animate-foo, foo 1s infinite); } @@ -2597,15 +2414,13 @@ describe('Parsing theme values from CSS', () => { 50% { color: #00f; } - } - " + }" `) }) test('`@keyframes` added in `@theme reference` should not be emitted, even if another `@theme` block exists', async () => { return expect( - await run( - ['bg-pink', 'animate-foo'], + await compileCss( css` @theme reference { --animate-foo: foo 1s infinite; @@ -2627,10 +2442,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-pink', 'animate-foo'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-pink: pink; } @@ -2650,15 +2465,13 @@ describe('Parsing theme values from CSS', () => { 50% { color: #00f; } - } - " + }" `) }) test('theme values added as reference that override existing theme value suppress the output of the original theme value as a variable', async () => { expect( - await run( - ['bg-potato'], + await compileCss( css` @theme { --color-potato: #ac855b; @@ -2668,20 +2481,18 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['bg-potato'], ), ).toMatchInlineSnapshot(` - " - .bg-potato { + ".bg-potato { background-color: var(--color-potato, #c794aa); - } - " + }" `) }) test('overriding a reference theme value with a non-reference theme value includes it in the output as a variable', async () => { expect( - await run( - ['bg-potato'], + await compileCss( css` @theme reference { --color-potato: #ac855b; @@ -2691,24 +2502,22 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['bg-potato'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-potato: #c794aa; } .bg-potato { background-color: var(--color-potato); - } - " + }" `) }) test('wrapping `@theme` with `@media reference` behaves like `@theme reference` to support `@import` statements', async () => { expect( - await run( - ['bg-tomato', 'bg-potato', 'bg-avocado'], + await compileCss( css` @theme { --color-tomato: #e10c04; @@ -2723,10 +2532,10 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['bg-tomato', 'bg-potato', 'bg-avocado'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-tomato: #e10c04; } @@ -2740,18 +2549,17 @@ describe('Parsing theme values from CSS', () => { .bg-tomato { background-color: var(--color-tomato); - } - " + }" `) }) test('`@import "tailwindcss" theme(static)` will always generate theme values, regardless of whether they were used or not', async () => { expect( - await run( - ['bg-tomato'], + await compileCss( css` @import 'tailwindcss' theme(static); `, + ['bg-tomato'], { async loadStylesheet() { return { @@ -2771,8 +2579,7 @@ describe('Parsing theme values from CSS', () => { }, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-tomato: #e10c04; --color-potato: #ac855b; --color-primary: var(--primary); @@ -2780,15 +2587,13 @@ describe('Parsing theme values from CSS', () => { .bg-tomato { background-color: var(--color-tomato); - } - " + }" `) }) test('`@media theme(reference)` can only contain `@theme` rules', () => { return expect( - run( - ['bg-tomato', 'bg-potato', 'bg-avocado'], + compileCss( css` @media theme(reference) { .not-a-theme-rule { @@ -2797,6 +2602,7 @@ describe('Parsing theme values from CSS', () => { } @tailwind utilities; `, + ['bg-tomato', 'bg-potato', 'bg-avocado'], ), ).rejects.toThrowErrorMatchingInlineSnapshot( ` @@ -2808,8 +2614,7 @@ describe('Parsing theme values from CSS', () => { test('theme values added as `inline` are not wrapped in `var(…)` when used as utility values', async () => { expect( - await run( - ['bg-tomato', 'bg-potato', 'bg-primary'], + await compileCss( css` @theme inline { --color-tomato: #e10c04; @@ -2819,10 +2624,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-tomato', 'bg-potato', 'bg-primary'], ), ).toMatchInlineSnapshot(` - " - .bg-potato { + ".bg-potato { background-color: #ac855b; } @@ -2832,18 +2637,17 @@ describe('Parsing theme values from CSS', () => { .bg-tomato { background-color: #e10c04; - } - " + }" `) }) test('`@import "tailwindcss" theme(inline)` theme values added as `inline` are not wrapped in `var(…)` when used as utility values', async () => { expect( - await run( - ['bg-tomato'], + await compileCss( css` @import 'tailwindcss' theme(inline); `, + ['bg-tomato'], { async loadStylesheet() { return { @@ -2863,18 +2667,15 @@ describe('Parsing theme values from CSS', () => { }, ), ).toMatchInlineSnapshot(` - " - .bg-tomato { + ".bg-tomato { background-color: #e10c04; - } - " + }" `) }) test('theme values added as `static` will always be generated, regardless of whether they were used or not', async () => { expect( - await run( - ['bg-tomato'], + await compileCss( css` @theme static { --color-tomato: #e10c04; @@ -2884,10 +2685,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-tomato'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-tomato: #e10c04; --color-potato: #ac855b; --color-primary: var(--primary); @@ -2895,15 +2696,13 @@ describe('Parsing theme values from CSS', () => { .bg-tomato { background-color: var(--color-tomato); - } - " + }" `) }) test('when no theme values are emitted, empty layers can be removed', async () => { expect( - await run( - ['underline'], + await compileCss( css` @layer theme1 { @layer theme2 { @@ -2916,20 +2715,18 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['underline'], ), ).toMatchInlineSnapshot(` - " - .underline { + ".underline { text-decoration-line: underline; - } - " + }" `) }) test('wrapping `@theme` with `@media theme(inline)` behaves like `@theme inline` to support `@import` statements', async () => { expect( - await run( - ['bg-tomato', 'bg-potato', 'bg-primary'], + await compileCss( css` @media theme(inline) { @theme { @@ -2941,10 +2738,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-tomato', 'bg-potato', 'bg-primary'], ), ).toMatchInlineSnapshot(` - " - .bg-potato { + ".bg-potato { background-color: #ac855b; } @@ -2954,15 +2751,13 @@ describe('Parsing theme values from CSS', () => { .bg-tomato { background-color: #e10c04; - } - " + }" `) }) test('`inline` and `reference` can be used together', async () => { expect( - await run( - ['bg-tomato', 'bg-potato', 'bg-primary'], + await compileCss( css` @theme reference inline { --color-tomato: #e10c04; @@ -2972,10 +2767,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-tomato', 'bg-potato', 'bg-primary'], ), ).toMatchInlineSnapshot(` - " - .bg-potato { + ".bg-potato { background-color: #ac855b; } @@ -2985,15 +2780,13 @@ describe('Parsing theme values from CSS', () => { .bg-tomato { background-color: #e10c04; - } - " + }" `) }) test('`inline` and `reference` can be used together in `media(…)`', async () => { expect( - await run( - ['bg-tomato', 'bg-potato', 'bg-primary'], + await compileCss( css` @media theme(reference inline) { @theme { @@ -3005,10 +2798,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-tomato', 'bg-potato', 'bg-primary'], ), ).toMatchInlineSnapshot(` - " - .bg-potato { + ".bg-potato { background-color: #ac855b; } @@ -3018,15 +2811,13 @@ describe('Parsing theme values from CSS', () => { .bg-tomato { background-color: #e10c04; - } - " + }" `) }) test('`default` theme values can be overridden by regular theme values`', async () => { expect( - await run( - ['bg-potato'], + await compileCss( css` @theme { --color-potato: #ac855b; @@ -3037,24 +2828,22 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-potato'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-potato: #ac855b; } .bg-potato { background-color: var(--color-potato); - } - " + }" `) }) test('`default` and `inline` can be used together', async () => { expect( - await run( - ['bg-potato'], + await compileCss( css` @theme default inline { --color-potato: #efb46b; @@ -3062,20 +2851,18 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-potato'], ), ).toMatchInlineSnapshot(` - " - .bg-potato { + ".bg-potato { background-color: #efb46b; - } - " + }" `) }) test('`default` and `reference` can be used together', async () => { expect( - await run( - ['bg-potato'], + await compileCss( css` @theme default reference { --color-potato: #efb46b; @@ -3083,20 +2870,18 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-potato'], ), ).toMatchInlineSnapshot(` - " - .bg-potato { + ".bg-potato { background-color: var(--color-potato, #efb46b); - } - " + }" `) }) test('`default`, `inline`, and `reference` can be used together', async () => { expect( - await run( - ['bg-potato'], + await compileCss( css` @theme default reference inline { --color-potato: #efb46b; @@ -3104,20 +2889,18 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-potato'], ), ).toMatchInlineSnapshot(` - " - .bg-potato { + ".bg-potato { background-color: #efb46b; - } - " + }" `) }) test('`default` can be used in `media(…)`', async () => { expect( - await run( - ['bg-potato', 'bg-tomato'], + await compileCss( css` @media theme() { @theme { @@ -3133,10 +2916,10 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, + ['bg-potato', 'bg-tomato'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-potato: #ac855b; --color-tomato: tomato; } @@ -3147,47 +2930,44 @@ describe('Parsing theme values from CSS', () => { .bg-tomato { background-color: var(--color-tomato); - } - " + }" `) }) test('`default` theme values can be overridden by plugin theme values', async () => { - expect( - await run( - ['text-red', 'text-orange'], - css` - @theme default { - --color-red: red; - } - @theme { - --color-orange: orange; - } - @plugin "my-plugin"; - @tailwind utilities; - `, - { - loadModule: async () => { - return { - path: '', - base: '/root', - module: plugin(({}) => {}, { - theme: { - extend: { - colors: { - red: 'tomato', - orange: '#f28500', - }, + let { build } = await compile( + css` + @theme default { + --color-red: red; + } + @theme { + --color-orange: orange; + } + @plugin "my-plugin"; + @tailwind utilities; + `, + { + loadModule: async () => { + return { + path: '', + base: '/root', + module: plugin(({}) => {}, { + theme: { + extend: { + colors: { + red: 'tomato', + orange: '#f28500', }, }, - }), - } - }, + }, + }), + } }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + }, + ) + + expect(optimizeCss(build(['text-red', 'text-orange'])).trim()).toMatchInlineSnapshot(` + ":root, :host { --color-orange: orange; } @@ -3197,47 +2977,44 @@ describe('Parsing theme values from CSS', () => { .text-red { color: tomato; - } - " + }" `) }) test('`default` theme values can be overridden by config theme values', async () => { - expect( - await run( - ['text-red', 'text-orange'], - css` - @theme default { - --color-red: red; - } - @theme { - --color-orange: orange; - } - @config "./my-config.js"; - @tailwind utilities; - `, - { - loadModule: async () => { - return { - path: '', - base: '/root', - module: { - theme: { - extend: { - colors: { - red: 'tomato', - orange: '#f28500', - }, + let { build } = await compile( + css` + @theme default { + --color-red: red; + } + @theme { + --color-orange: orange; + } + @config "./my-config.js"; + @tailwind utilities; + `, + { + loadModule: async () => { + return { + path: '', + base: '/root', + module: { + theme: { + extend: { + colors: { + red: 'tomato', + orange: '#f28500', }, }, }, - } - }, + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + }, + ) + + expect(optimizeCss(build(['text-red', 'text-orange'])).trim()).toMatchInlineSnapshot(` + ":root, :host { --color-orange: orange; } @@ -3247,42 +3024,40 @@ describe('Parsing theme values from CSS', () => { .text-red { color: tomato; - } - " + }" `) }) test('only emits theme variables that are used outside of being defined by another variable', async () => { - expect( - await run( - ['get-var-b', 'get-var-two'], - css` - @theme { - --var-a: var(--var-b); - --var-b: var(--var-c); - --var-c: var(--var-d); - --var-d: red; + let { build } = await compile( + css` + @theme { + --var-a: var(--var-b); + --var-b: var(--var-c); + --var-c: var(--var-d); + --var-d: red; - --var-four: green; - --var-three: var(--var-four); - --var-two: var(--var-three); - --var-one: var(--var-two); + --var-four: green; + --var-three: var(--var-four); + --var-two: var(--var-three); + --var-one: var(--var-two); - --var-eins: var(--var-zwei); - --var-zwei: var(--var-drei); - --var-drei: var(--var-vier); - --var-vier: blue; - } + --var-eins: var(--var-zwei); + --var-zwei: var(--var-drei); + --var-drei: var(--var-vier); + --var-vier: blue; + } - @utility get-var-* { - color: --value(--var-\*); - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + @utility get-var-* { + color: --value(--var-\*); + } + @tailwind utilities; + `, + {}, + ) + + expect(optimizeCss(build(['get-var-b', 'get-var-two'])).trim()).toMatchInlineSnapshot(` + ":root, :host { --var-b: var(--var-c); --var-c: var(--var-d); --var-d: red; @@ -3297,8 +3072,7 @@ describe('Parsing theme values from CSS', () => { .get-var-two { color: var(--var-two); - } - " + }" `) }) }) @@ -3323,7 +3097,7 @@ describe('plugins', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`) }) - test('@plugin cannot have an empty path', () => { + test('@plugin can not have an empty path', () => { return expect( compile( css` @@ -3366,41 +3140,40 @@ describe('plugins', () => { test('@plugin can accept options', async () => { expect.hasAssertions() - expect( - await run( - ['text-primary'], - css` - @tailwind utilities; - @plugin "my-plugin" { - color: red; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: plugin.withOptions((options) => { - expect(options).toEqual({ - color: 'red', - }) + let { build } = await compile( + css` + @tailwind utilities; + @plugin "my-plugin" { + color: red; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: plugin.withOptions((options) => { + expect(options).toEqual({ + color: 'red', + }) - return ({ addUtilities }) => { - addUtilities({ - '.text-primary': { - color: options.color, - }, - }) - } - }), + return ({ addUtilities }) => { + addUtilities({ + '.text-primary': { + color: options.color, + }, + }) + } }), - }, - ), - ).toMatchInlineSnapshot(` - " - .text-primary { + }), + }, + ) + + let compiled = build(['text-primary']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + ".text-primary { color: red; - } - " + }" `) }) @@ -3579,148 +3352,142 @@ describe('plugins', () => { }) test('addVariant with string selector', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', '&:hover, &:focus') - }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', '&:hover, &:focus') + }, + }), + }, + ) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('addVariant with array of selectors', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', ['&:hover', '&:focus']) - }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', ['&:hover', '&:focus']) + }, + }), + }, + ) + + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('addVariant with object syntax and @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&:hover': '@slot', - '&:focus': '@slot', - }) - }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '&:hover': '@slot', + '&:focus': '@slot', + }) + }, + }), + }, + ) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('addVariant with object syntax, media, nesting and multiple @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '@media (hover: hover)': { - '&:hover': '@slot', - }, - '&:focus': '@slot', - }) - }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '@media (hover: hover)': { + '&:hover': '@slot', + }, + '&:focus': '@slot', + }) + }, + }), + }, + ) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (hover: hover) { - :is(:where(.group):hover .group-hocus\\:flex) { + .group-hocus\\:flex:is(:where(.group):hover *) { display: flex; } } - :is(:where(.group):focus .group-hocus\\:flex) { + .group-hocus\\:flex:is(:where(.group):focus *) { display: flex; } @@ -3733,40 +3500,38 @@ describe('plugins', () => { .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('@slot is preserved when used as a custom property value', async () => { - expect( - await run( - ['hocus:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&': { - '--custom-property': '@slot', - '&:hover': '@slot', - '&:focus': '@slot', - }, - }) - }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '&': { + '--custom-property': '@slot', + '&:hover': '@slot', + '&:focus': '@slot', + }, + }) + }, + }), + }, + ) + let compiled = build(['hocus:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .hocus\\:underline { --custom-property: @slot; } @@ -3774,36 +3539,36 @@ describe('plugins', () => { .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('built-in variants can be overridden while keeping their order', async () => { - expect( - await run( - // Make sure the order does not change by including the variants - // immediately before and after `dark` - ['rtl:flex', 'dark:flex', 'starting:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: ({ addVariant }: PluginAPI) => { - addVariant('dark', '&:is([data-theme=dark] *)') - }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: ({ addVariant }: PluginAPI) => { + addVariant('dark', '&:is([data-theme=dark] *)') + }, + }), + }, + ) + let compiled = build( + // Make sure the order does not change by including the variants + // immediately before and after `dark` + ['rtl:flex', 'dark:flex', 'starting:flex'], + ) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .rtl\\:flex:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *), .dark\\:flex:is([data-theme="dark"] *) { display: flex; } @@ -3813,8 +3578,7 @@ describe('plugins', () => { display: flex; } } - } - " + }" `) }) }) @@ -3863,14 +3627,16 @@ describe('@source', () => { describe('@source inline(…)', () => { test('always includes the candidate', async () => { - expect( - await compileCss(css` + let { build } = await compile( + css` @source inline("underline"); @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - .underline { + `, + { base: '/root' }, + ) + + expect(build([])).toMatchInlineSnapshot(` + ".underline { text-decoration-line: underline; } " @@ -3878,8 +3644,8 @@ describe('@source', () => { }) test('applies brace expansion', async () => { - expect( - await compileCss(css` + let { build } = await compile( + css` @theme { --color-red-50: oklch(0.971 0.013 17.38); --color-red-100: oklch(0.936 0.032 17.717); @@ -3895,63 +3661,54 @@ describe('@source', () => { } @source inline("bg-red-{50,{100..900..100},950}"); @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - :root, :host { - --color-red-50: oklch(97.1% .013 17.38); - --color-red-100: oklch(93.6% .032 17.717); - --color-red-200: oklch(88.5% .062 18.334); - --color-red-300: oklch(80.8% .114 19.571); - --color-red-400: oklch(70.4% .191 22.216); - --color-red-500: oklch(63.7% .237 25.331); - --color-red-600: oklch(57.7% .245 27.325); - --color-red-700: oklch(50.5% .213 27.518); - --color-red-800: oklch(44.4% .177 26.899); - --color-red-900: oklch(39.6% .141 25.723); - --color-red-950: oklch(25.8% .092 26.042); - } + `, + { base: '/root' }, + ) + expect(build([])).toMatchInlineSnapshot(` + ":root, :host { + --color-red-50: oklch(0.971 0.013 17.38); + --color-red-100: oklch(0.936 0.032 17.717); + --color-red-200: oklch(0.885 0.062 18.334); + --color-red-300: oklch(0.808 0.114 19.571); + --color-red-400: oklch(0.704 0.191 22.216); + --color-red-500: oklch(0.637 0.237 25.331); + --color-red-600: oklch(0.577 0.245 27.325); + --color-red-700: oklch(0.505 0.213 27.518); + --color-red-800: oklch(0.444 0.177 26.899); + --color-red-900: oklch(0.396 0.141 25.723); + --color-red-950: oklch(0.258 0.092 26.042); + } .bg-red-50 { background-color: var(--color-red-50); } - .bg-red-100 { background-color: var(--color-red-100); } - .bg-red-200 { background-color: var(--color-red-200); } - .bg-red-300 { background-color: var(--color-red-300); } - .bg-red-400 { background-color: var(--color-red-400); } - .bg-red-500 { background-color: var(--color-red-500); } - .bg-red-600 { background-color: var(--color-red-600); } - .bg-red-700 { background-color: var(--color-red-700); } - .bg-red-800 { background-color: var(--color-red-800); } - .bg-red-900 { background-color: var(--color-red-900); } - .bg-red-950 { background-color: var(--color-red-950); } @@ -3960,30 +3717,29 @@ describe('@source', () => { }) test('adds multiple inline sources separated by spaces', async () => { - expect( - await compileCss(css` + let { build } = await compile( + css` @theme { --color-red-100: oklch(0.936 0.032 17.717); --color-red-200: oklch(0.885 0.062 18.334); } @source inline("block bg-red-{100..200..100}"); @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - :root, :host { - --color-red-100: oklch(93.6% .032 17.717); - --color-red-200: oklch(88.5% .062 18.334); - } + `, + { base: '/root' }, + ) + expect(build([])).toMatchInlineSnapshot(` + ":root, :host { + --color-red-100: oklch(0.936 0.032 17.717); + --color-red-200: oklch(0.885 0.062 18.334); + } .block { display: block; } - .bg-red-100 { background-color: var(--color-red-100); } - .bg-red-200 { background-color: var(--color-red-200); } @@ -3992,90 +3748,59 @@ describe('@source', () => { }) test('ignores invalid inline candidates', async () => { - expect( - await compileCss(css` + let { build } = await compile( + css` @source inline("my-cucumber"); @tailwind utilities; - `), - ).toEqual('') + `, + { base: '/root' }, + ) + + expect(build([])).toMatchInlineSnapshot(`""`) }) test('can be negated', async () => { - expect( - await run( - ['container'], - css` - @theme { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @source not inline("container"); - @tailwind utilities; - `, - ), - ).toEqual('') + let { build } = await compile( + css` + @theme { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @source not inline("container"); + @tailwind utilities; + `, + { base: '/root' }, + ) + + expect(build(['container'])).toMatchInlineSnapshot(`""`) }) test('applies brace expansion to negated sources', async () => { - expect( - await run( - ['bg-red-500', 'bg-red-700'], - css` - @theme { - --color-red-50: oklch(0.971 0.013 17.38); - --color-red-100: oklch(0.936 0.032 17.717); - --color-red-200: oklch(0.885 0.062 18.334); - --color-red-300: oklch(0.808 0.114 19.571); - --color-red-400: oklch(0.704 0.191 22.216); - --color-red-500: oklch(0.637 0.237 25.331); - --color-red-600: oklch(0.577 0.245 27.325); - --color-red-700: oklch(0.505 0.213 27.518); - --color-red-800: oklch(0.444 0.177 26.899); - --color-red-900: oklch(0.396 0.141 25.723); - --color-red-950: oklch(0.258 0.092 26.042); - } - @source not inline("bg-red-{50,{100..900..100},950}"); - @tailwind utilities; - `, - ), - ).toEqual('') - }) - - test('works with whitespace around the argument', async () => { - expect( - await compileCss(css` - /* prettier-ignore */ - @source inline( "underline" ); + let { build } = await compile( + css` + @theme { + --color-red-50: oklch(0.971 0.013 17.38); + --color-red-100: oklch(0.936 0.032 17.717); + --color-red-200: oklch(0.885 0.062 18.334); + --color-red-300: oklch(0.808 0.114 19.571); + --color-red-400: oklch(0.704 0.191 22.216); + --color-red-500: oklch(0.637 0.237 25.331); + --color-red-600: oklch(0.577 0.245 27.325); + --color-red-700: oklch(0.505 0.213 27.518); + --color-red-800: oklch(0.444 0.177 26.899); + --color-red-900: oklch(0.396 0.141 25.723); + --color-red-950: oklch(0.258 0.092 26.042); + } + @source not inline("bg-red-{50,{100..900..100},950}"); @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - .underline { - text-decoration-line: underline; - } - " - `) - }) + `, + { base: '/root' }, + ) - test('works with newlines around the argument', async () => { - expect( - await compileCss(css` - /* prettier-ignore */ - @source inline( - "underline" - ); - @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - .underline { - text-decoration-line: underline; - } - " - `) + expect(build(['bg-red-500', 'bg-red-700'])).toMatchInlineSnapshot(`""`) }) }) }) @@ -4115,10 +3840,10 @@ describe('@custom-variant', () => { [`@custom-variant foo_ (&);`], [`@custom-variant foo__ (&);`], ])('@custom-variant must have a valid name', (input) => { - return expect(compileCss(input)).rejects.toThrow() + return expect(compileCss(input)).rejects.toThrowError() }) - test('@custom-variant must not contain special characters', () => { + test('@custom-variant must not container special characters', () => { return expect( compileCss(css` .foo { @@ -4185,47 +3910,40 @@ describe('@custom-variant', () => { describe('body-less syntax', () => { test('selector variant', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @custom-variant hocus (&:hover, &:focus); + let { build } = await compile(css` + @custom-variant hocus (&:hover, &:focus); - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " @layer utilities { - :is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) { + @tailwind utilities; + } + `) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('at-rule variant', async () => { - expect( - await run( - ['any-hover:hover:underline'], - css` - @custom-variant any-hover (@media (any-hover: hover)); + let { build } = await compile(css` + @custom-variant any-hover (@media (any-hover: hover)); - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['any-hover:hover:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (any-hover: hover) { @media (hover: hover) { .any-hover\\:hover\\:underline:hover { @@ -4233,26 +3951,22 @@ describe('@custom-variant', () => { } } } - } - " + }" `) }) test('style-rules and at-rules', async () => { - expect( - await run( - ['cant-hover:focus:underline'], - css` - @custom-variant cant-hover (&:not(:hover), &:not(:active), @media not (any-hover: hover), @media not (pointer: fine)); + let { build } = await compile(css` + @custom-variant cant-hover (&:not(:hover), &:not(:active), @media not (any-hover: hover), @media not (pointer: fine)); - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['cant-hover:focus:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { :is(.cant-hover\\:focus\\:underline:not(:hover), .cant-hover\\:focus\\:underline:not(:active)):focus { text-decoration-line: underline; } @@ -4268,121 +3982,105 @@ describe('@custom-variant', () => { text-decoration-line: underline; } } - } - " + }" `) }) }) describe('body with @slot syntax', () => { test('selector with @slot', async () => { - expect( - await run( - ['selected:underline', 'group-selected:underline'], - css` - @custom-variant selected { - &[data-selected] { - @slot; - } - } - - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group)[data-selected] .group-selected\\:underline), .selected\\:underline[data-selected] { - text-decoration-line: underline; + let { build } = await compile(css` + @custom-variant selected { + &[data-selected] { + @slot; } } - " + + @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['selected:underline', 'group-selected:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-selected\\:underline:is(:where(.group)[data-selected] *), .selected\\:underline[data-selected] { + text-decoration-line: underline; + } + }" `) }) test('grouped selectors with @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:underline'], - css` - @custom-variant hocus { - &:hover, - &:focus { - @slot; - } - } - - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:underline), .hocus\\:underline:hover, .hocus\\:underline:focus { - text-decoration-line: underline; + let { build } = await compile(css` + @custom-variant hocus { + &:hover, + &:focus { + @slot; } } - " + + @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['hocus:underline', 'group-hocus:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:underline:is(:is(:where(.group):hover, :where(.group):focus) *), .hocus\\:underline:hover, .hocus\\:underline:focus { + text-decoration-line: underline; + } + }" `) }) test('multiple selectors with @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:underline'], - css` - @custom-variant hocus { - &:hover { - @slot; - } + let { build } = await compile(css` + @custom-variant hocus { + &:hover { + @slot; + } - &:focus { - @slot; - } - } - - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group):hover .group-hocus\\:underline), :is(:where(.group):focus .group-hocus\\:underline), .hocus\\:underline:hover, .hocus\\:underline:focus { - text-decoration-line: underline; + &:focus { + @slot; } } - " + + @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['hocus:underline', 'group-hocus:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:underline:is(:where(.group):hover *), .group-hocus\\:underline:is(:where(.group):focus *), .hocus\\:underline:hover, .hocus\\:underline:focus { + text-decoration-line: underline; + } + }" `) }) test('nested selector with @slot', async () => { - expect( - await run( - ['custom-before:underline'], - css` - @custom-variant custom-before { - & { - --has-before: 1; - &::before { - @slot; - } - } + let { build } = await compile(css` + @custom-variant custom-before { + & { + --has-before: 1; + &::before { + @slot; } + } + } - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['custom-before:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .custom-before\\:underline { --has-before: 1; } @@ -4390,36 +4088,32 @@ describe('@custom-variant', () => { .custom-before\\:underline:before { text-decoration-line: underline; } - } - " + }" `) }) test('grouped nested selectors with @slot', async () => { - expect( - await run( - ['custom-before:underline'], - css` - @custom-variant custom-before { - & { - --has-before: 1; - &::before { - &:hover, - &:focus { - @slot; - } - } + let { build } = await compile(css` + @custom-variant custom-before { + & { + --has-before: 1; + &::before { + &:hover, + &:focus { + @slot; } } + } + } - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['custom-before:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .custom-before\\:underline { --has-before: 1; } @@ -4427,43 +4121,39 @@ describe('@custom-variant', () => { .custom-before\\:underline:before:hover, .custom-before\\:underline:before:focus { text-decoration-line: underline; } - } - " + }" `) }) test('nested multiple selectors with @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:underline'], - css` - @custom-variant hocus { - &:hover { - @media (hover: hover) { - @slot; - } - } - - &:focus { - @slot; - } + let { build } = await compile(css` + @custom-variant hocus { + &:hover { + @media (hover: hover) { + @slot; } + } + + &:focus { + @slot; + } + } - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['hocus:underline', 'group-hocus:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (hover: hover) { - :is(:where(.group):hover .group-hocus\\:underline) { + .group-hocus\\:underline:is(:where(.group):hover *) { text-decoration-line: underline; } } - :is(:where(.group):focus .group-hocus\\:underline) { + .group-hocus\\:underline:is(:where(.group):focus *) { text-decoration-line: underline; } @@ -4476,94 +4166,82 @@ describe('@custom-variant', () => { .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('selector nested under at-rule with @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:underline'], - css` - @custom-variant hocus { - @media (hover: hover) { - &:hover { - @slot; - } - } - } - - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + let { build } = await compile(css` + @custom-variant hocus { @media (hover: hover) { - :is(:where(.group):hover .group-hocus\\:underline), .hocus\\:underline:hover { - text-decoration-line: underline; + &:hover { + @slot; } } } - " + + @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['hocus:underline', 'group-hocus:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + @media (hover: hover) { + .group-hocus\\:underline:is(:where(.group):hover *), .hocus\\:underline:hover { + text-decoration-line: underline; + } + } + }" `) }) test('at-rule with @slot', async () => { - expect( - await run( - ['any-hover:underline'], - css` - @custom-variant any-hover { - @media (any-hover: hover) { - @slot; - } - } + let { build } = await compile(css` + @custom-variant any-hover { + @media (any-hover: hover) { + @slot; + } + } - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['any-hover:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (any-hover: hover) { .any-hover\\:underline { text-decoration-line: underline; } } - } - " + }" `) }) test('multiple at-rules with @slot', async () => { - expect( - await run( - ['desktop:underline'], - css` - @custom-variant desktop { - @media (any-hover: hover) { - @slot; - } + let { build } = await compile(css` + @custom-variant desktop { + @media (any-hover: hover) { + @slot; + } - @media (pointer: fine) { - @slot; - } - } + @media (pointer: fine) { + @slot; + } + } - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['desktop:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (any-hover: hover) { .desktop\\:underline { text-decoration-line: underline; @@ -4575,36 +4253,32 @@ describe('@custom-variant', () => { text-decoration-line: underline; } } - } - " + }" `) }) test('nested at-rules with @slot', async () => { - expect( - await run( - ['custom-variant:underline'], - css` - @custom-variant custom-variant { - @media (orientation: landscape) { - @media screen { - @slot; - } - - @media print { - display: none; - } - } + let { build } = await compile(css` + @custom-variant custom-variant { + @media (orientation: landscape) { + @media screen { + @slot; } - @layer utilities { - @tailwind utilities; + @media print { + display: none; } - `, - ), - ).toMatchInlineSnapshot(` - " + } + } + @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['custom-variant:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (orientation: landscape) { @media screen { .custom-variant\\:underline { @@ -4618,33 +4292,29 @@ describe('@custom-variant', () => { } } } - } - " + }" `) }) test('at-rule and selector with @slot', async () => { - expect( - await run( - ['custom-dark:underline'], - css` - @custom-variant custom-dark { - @media (prefers-color-scheme: dark) { - @slot; - } - &:is(.dark *) { - @slot; - } - } + let { build } = await compile(css` + @custom-variant custom-dark { + @media (prefers-color-scheme: dark) { + @slot; + } + &:is(.dark *) { + @slot; + } + } - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['custom-dark:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (prefers-color-scheme: dark) { .custom-dark\\:underline { text-decoration-line: underline; @@ -4654,341 +4324,27 @@ describe('@custom-variant', () => { .custom-dark\\:underline:is(.dark *) { text-decoration-line: underline; } - } - " - `) - }) - }) - - describe('@scope', () => { - test('arbitrary variant', async () => { - expect(await run(['[@scope_(.from)_to_(.to)]:underline'])).toMatchInlineSnapshot(` - " - @scope (.from) to (.to) { - .\\[\\@scope_\\(\\.from\\)_to_\\(\\.to\\)\\]\\:underline { - text-decoration-line: underline; - } - } - " - `) - }) - - test('shorthand syntax', async () => { - expect( - await run( - ['scoped:underline'], - css` - @custom-variant scoped (@scope (.from) to (.to)); - - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope (.from) to (.to) { - .scoped\\:underline { - text-decoration-line: underline; - } - } - } - " - `) - }) - - test('with @slot syntax', async () => { - expect( - await run( - ['scoped:underline'], - css` - @custom-variant scoped { - @scope (.from) to (.to) { - @slot; - } - } - - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope (.from) to (.to) { - .scoped\\:underline { - text-decoration-line: underline; - } - } - } - " - `) - }) - - test('@scope in a custom variant combined with user-authored `@scope` rules', async () => { - expect( - await run( - ['blue:bg-blue-500'], - css` - @layer utilities { - @tailwind utilities; - } - - @theme inline { - --color-blue-500: blue; - } - - /* In a variant */ - @custom-variant blue { - @scope ([data-theme='blue']) to ([data-theme]) { - @slot; - } - } - - /* User CSS */ - .parent { - /* @scope with bare declarations */ - @scope (.from) to (.to) { - --x: 1; - } - - .child { - .a { - /* Variant with @scope used by @apply */ - @apply blue:[--x:2]; - } - .b { - /* Variant with @scope used by @apply combined with another variant */ - @apply blue:hover:[--x:3]; - } - .c { - /* Variant with @scoped used by @variant */ - @variant blue { - --x: 4; - } - } - } - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope ([data-theme="blue"]) to ([data-theme]) { - .blue\\:bg-blue-500 { - background-color: #00f; - } - } - } - - @scope (.parent .from) to (.to) { - :where(:scope) { - --x: 1; - } - } - - @scope ([data-theme="blue"]) to ([data-theme]) { - .parent .child .a { - --x: 2; - } - - @media (hover: hover) { - .parent .child .b:hover { - --x: 3; - } - } - - .parent .child .c { - --x: 4; - } - } - " - `) - }) - - test('`&` in the `@scope` prelude of a custom variant controls the composition', async () => { - expect( - await run( - ['in-blue:flex', 'blue-self:flex', 'scoped-panel:flex', 'in-blue:hover:flex'], - css` - /* Default: the utility applies to elements inside the scope */ - @custom-variant in-blue { - @scope ([data-theme='blue']) to ([data-theme]) { - @slot; - } - } - - /* Trailing &: the utility itself becomes the scoping root */ - @custom-variant blue-self { - @scope ([data-theme='blue'] &) to ([data-theme]) { - @slot; - } - } - - /* Leading &: the scoping roots are found inside the utility */ - @custom-variant scoped-panel { - @scope (& .panel) { - @slot; - } - } - - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope ([data-theme="blue"]) to ([data-theme]) { - .in-blue\\:flex { - display: flex; - } - - @media (hover: hover) { - .in-blue\\:hover\\:flex:hover { - display: flex; - } - } - } - - @scope ([data-theme="blue"] .blue-self\\:flex) to ([data-theme]) { - :where(:scope) { - display: flex; - } - } - - @scope (.scoped-panel\\:flex .panel) { - :where(:scope) { - display: flex; - } - } - } - " - `) - }) - - test('user-authored `@scope` inside `@variant` keeps its native semantics', async () => { - // The `hocus` variant does not use `@scope` itself, so the inner `@scope` - // is user CSS and compiles with the native CSS nesting semantics: the - // parent selector moves into the prelude, and bare declarations apply to - // the scoping root. - expect( - await compileCss(css` - @custom-variant hocus (&:hover, &:focus); - - .btn { - @variant hocus { - @scope (.from) to (.to) { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (:is(.btn:hover, .btn:focus) .from) to (.to) { - :where(:scope) { - --x: 1; - } - } - " - `) - }) - - test('user-authored `@scope` inside a `@variant` that uses `@scope` keeps its native semantics', async () => { - // The `blue` variant uses `@scope` itself, so its own `@scope` takes the - // variant composition. But the user CSS slotted into its body is marked - // as `user` content, so the inner `@scope` keeps its native CSS nesting - // semantics: the parent selector moves into the prelude, and bare - // declarations apply to the scoping root. - expect( - await compileCss(css` - @custom-variant blue { - @scope ([data-theme='blue']) to ([data-theme]) { - @slot; - } - } - - .btn { - @variant blue { - @scope (.from) to (.to) { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope ([data-theme="blue"]) to ([data-theme]) { - @scope (.btn .from) to (.to) { - :where(:scope) { - --x: 1; - } - } - } - " - `) - }) - - test('@scope nested in a rule, nested in another @scope, with @slot', async () => { - // Both `@scope` rules take the variant composition: neither prelude is - // made relative to a parent, the outer/inner nesting order is preserved, - // and the rule between them composes into the body with the utility's - // selector. - expect( - await run( - ['foo:underline'], - css` - @custom-variant foo { - @scope (.outer) to (.outer-limit) { - .middle { - @scope (.inner) to (.inner-limit) { - @slot; - } - } - } - } - - @layer utilities { - @tailwind utilities; - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope (.outer) to (.outer-limit) { - @scope (.inner) to (.inner-limit) { - .foo\\:underline .middle { - text-decoration-line: underline; - } - } - } - } - " + }" `) }) }) test('built-in variants can be overridden while keeping their order', async () => { expect( - await run( - // Make sure the order does not change by including the variants - // immediately before and after `dark` - ['rtl:flex', 'dark:flex', 'starting:flex'], + await compileCss( css` @custom-variant dark (&:is([data-theme='dark'] *)); @layer utilities { @tailwind utilities; } `, + + // Make sure the order does not change by including the variants + // immediately before and after `dark` + ['rtl:flex', 'dark:flex', 'starting:flex'], ), ).toMatchInlineSnapshot(` - " - @layer utilities { + "@layer utilities { .rtl\\:flex:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *), .dark\\:flex:is([data-theme="dark"] *) { display: flex; } @@ -4998,15 +4354,13 @@ describe('@custom-variant', () => { display: flex; } } - } - " + }" `) }) test('at-rule-only variants cannot be used with compound variants', async () => { expect( - await run( - ['foo:flex', 'group-foo:flex', 'peer-foo:flex', 'has-foo:flex', 'not-foo:flex'], + await compileCss( css` @custom-variant foo (@media foo); @@ -5014,10 +4368,11 @@ describe('@custom-variant', () => { @tailwind utilities; } `, + + ['foo:flex', 'group-foo:flex', 'peer-foo:flex', 'has-foo:flex', 'not-foo:flex'], ), ).toMatchInlineSnapshot(` - " - @layer utilities { + "@layer utilities { @media not foo { .not-foo\\:flex { display: flex; @@ -5029,15 +4384,13 @@ describe('@custom-variant', () => { display: flex; } } - } - " + }" `) }) test('@custom-variant can reuse existing @variant in the definition', async () => { expect( - await run( - ['hocus:flex'], + await compileCss( css` @custom-variant hocus { @variant hover { @@ -5049,22 +4402,20 @@ describe('@custom-variant', () => { @tailwind utilities; `, + ['hocus:flex'], ), ).toMatchInlineSnapshot(` - " - @media (hover: hover) { + "@media (hover: hover) { .hocus\\:flex:hover:focus { display: flex; } - } - " + }" `) }) test('@custom-variant can reuse @custom-variant that is defined later', async () => { expect( - await run( - ['hocus:flex'], + await compileCss( css` @custom-variant hocus { @variant custom-hover { @@ -5078,20 +4429,18 @@ describe('@custom-variant', () => { @tailwind utilities; `, + ['hocus:flex'], ), ).toMatchInlineSnapshot(` - " - .hocus\\:flex:hover:focus { + ".hocus\\:flex:hover:focus { display: flex; - } - " + }" `) }) test('@custom-variant can reuse existing @variant that is overwritten later', async () => { expect( - await run( - ['hocus:flex'], + await compileCss( css` @custom-variant hocus { @variant hover { @@ -5105,20 +4454,18 @@ describe('@custom-variant', () => { @tailwind utilities; `, + ['hocus:flex'], ), ).toMatchInlineSnapshot(` - " - .hocus\\:flex:hover:focus { + ".hocus\\:flex:hover:focus { display: flex; - } - " + }" `) }) test('@custom-variant cannot use @variant that eventually results in a circular dependency', async () => { return expect(() => - run( - ['foo:flex'], + compileCss( css` @custom-variant custom-variant { @variant foo { @@ -5152,6 +4499,7 @@ describe('@custom-variant', () => { @tailwind utilities; `, + ['foo:flex'], ), ).rejects.toThrowErrorMatchingInlineSnapshot(` [Error: Circular dependency detected in custom variants: @@ -5172,107 +4520,9 @@ describe('@custom-variant', () => { `) }) - // https://github.com/tailwindlabs/tailwindcss/issues/19618 - test('@custom-variant can use a @variant that eventually uses another @custom-variant', async () => { - expect( - await run( - ['a:flex', 'b:flex', 'a:b:flex', 'b:a:flex'], - css` - @custom-variant a { - @slot; - } - - @custom-variant b { - @variant a { - @slot; - } - } - - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - .a\\:flex, .b\\:flex, .a\\:b\\:flex, .b\\:a\\:flex { - display: flex; - } - " - `) - }) - - test('@custom-variant can use a @variant that eventually uses another @custom-variant (2)', async () => { - expect( - await run( - ['a:flex', 'b:flex', 'a:b:flex', 'b:a:flex'], - css` - @custom-variant a { - .a { - @slot; - } - } - - @custom-variant b { - .b { - @variant a { - .a-inside-b { - @slot; - } - } - } - } - - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - .a\\:flex .a, .b\\:flex .b .a .a-inside-b, .a\\:b\\:flex .a .b .a .a-inside-b, .b\\:a\\:flex .b .a .a-inside-b .a { - display: flex; - } - " - `) - }) - - // https://github.com/tailwindlabs/tailwindcss/issues/19618#issuecomment-3830775912 - test('@custom-variant can use existing @slot @variants', async () => { - expect( - await run( - ['hocus:flex'], - css` - @custom-variant hocus { - @variant hover { - @variant focus { - @slot; - } - } - } - - @custom-variant hover { - &:hover { - @slot; - } - - &[data-hover] { - @slot; - } - } - - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - .hocus\\:flex:hover:focus, .hocus\\:flex[data-hover]:focus { - display: flex; - } - " - `) - }) - test('@custom-variant setup that results in a circular dependency error can be solved', async () => { expect( - await run( - ['baz:flex'], + await compileCss( css` @custom-variant foo { @variant hover { @@ -5303,13 +4553,12 @@ describe('@custom-variant', () => { @tailwind utilities; `, + ['baz:flex'], ), ).toMatchInlineSnapshot(` - " - [data-broken-circle] .baz\\:flex:active { + "[data-broken-circle] .baz\\:flex:active { display: flex; - } - " + }" `) }) }) @@ -5350,35 +4599,6 @@ describe('@utility', () => { ) }) - test('@utility can handle escape sequences correctly', async () => { - expect( - await run( - ['push-1/2', 'push-50%'], - css` - @layer utilities { - @tailwind utilities; - } - - @utility push-1\/2 { - right: 50%; - } - - @utility push-50\% { - right: 50%; - } - `, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - .push-1\\/2, .push-50\\% { - right: 50%; - } - } - " - `) - }) - test('A functional @utility must end in -*', () => { return expect( compileCss(css` @@ -5402,73 +4622,36 @@ describe('@utility', () => { `[Error: \`@utility my-*-utility\` defines an invalid utility name. The dynamic portion marked by \`-*\` must appear once at the end.]`, ) }) - - // https://github.com/tailwindlabs/tailwindcss/issues/19505 - test('@utility name cannot contain multiple `/` characters', async () => { - expect( - await run( - ['ui/button'], - css` - @utility ui/button { - display: inline-flex; - background: blue; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot( - ` - " - .ui\\/button { - background: #00f; - display: inline-flex; - } - " - `, - ) - - await expect( - compileCss(css` - @utility ui/button/sm { - display: inline-flex; - background: blue; - font-size: 12px; - } - `), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: \`@utility ui/button/sm\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter.]`, - ) - }) }) test('addBase', async () => { - expect( - await run( - ['underline'], - css` - @plugin "my-plugin"; - @layer base, utilities; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: ({ addBase }: PluginAPI) => { - addBase({ - body: { - 'font-feature-settings': '"tnum"', - }, - }) - }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer base, utilities; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: ({ addBase }: PluginAPI) => { + addBase({ + body: { + 'font-feature-settings': '"tnum"', + }, + }) + }, + }), + }, + ) + + let compiled = build(['underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer base { body { font-feature-settings: "tnum"; } @@ -5478,40 +4661,39 @@ test('addBase', async () => { .underline { text-decoration-line: underline; } - } - " + }" `) }) test('JS APIs support @variant', async () => { - expect( - await run( - ['underline', 'foo', 'bar-one'], - css` - @plugin "my-plugin"; - @layer base, utilities; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: ({ addBase, addUtilities, matchUtilities }: PluginAPI) => { - addBase({ body: { '@variant dark': { color: 'red' } } }) - addUtilities({ '.foo': { '@variant dark': { '--foo': 'foo' } } }) - matchUtilities( - { bar: (value) => ({ '@variant dark': { '--bar': value } }) }, - { values: { one: '1' } }, - ) - }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer base, utilities; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async () => ({ + path: '', + base: '/root', + module: ({ addBase, addUtilities, matchUtilities }: PluginAPI) => { + addBase({ body: { '@variant dark': { color: 'red' } } }) + addUtilities({ '.foo': { '@variant dark': { '--foo': 'foo' } } }) + matchUtilities( + { bar: (value) => ({ '@variant dark': { '--bar': value } }) }, + { values: { one: '1' } }, + ) + }, + }), + }, + ) + + let compiled = build(['underline', 'foo', 'bar-one']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer base { @media (prefers-color-scheme: dark) { body { color: red; @@ -5533,8 +4715,7 @@ test('JS APIs support @variant', async () => { --foo: foo; } } - } - " + }" `) }) @@ -5544,6 +4725,7 @@ it("should error when `layer(…)` is used, but it's not the first param", async css` @import './bar.css' supports(display: grid) layer(utilities); `, + [], { async loadStylesheet() { return { @@ -5593,65 +4775,62 @@ describe('`@reference "…" imports`', () => { } } - expect( - await compileCss( - css` + await expect( + compileCss( + ` @reference './foo/bar.css'; .bar { @apply md:hocus:foo; } `, + [], { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @media (min-width: 768px) { + ).resolves.toMatchInlineSnapshot(` + "@media (min-width: 768px) { .bar:hover, .bar:focus { color: red; } - } - " + }" `) }) test('does not generate utilities', async () => { - expect( - await run( - ['text-underline', 'border'], - css` - @reference './foo/bar.css'; + let loadStylesheet = async (id: string, base = '/root/foo') => { + if (id === './foo/baz.css') { + return { + base, + path: '', + content: css` + @layer utilities { + @tailwind utilities; + } + `, + } + } + return { + path: '', + base: '/root/foo', + content: css` + @import './foo/baz.css'; `, - { - loadStylesheet: async (id: string, base = '/root/foo') => { - if (id === './foo/baz.css') { - return { - base, - path: '', - content: css` - @layer utilities { - @tailwind utilities; - } - `, - } - } - return { - path: '', - base: '/root/foo', - content: css` - @import './foo/baz.css'; - `, - } - }, - }, - ), - ).toEqual('') + } + } + + let { build } = await compile( + css` + @reference './foo/bar.css'; + `, + { loadStylesheet }, + ) + + expect(build(['text-underline', 'border']).trim()).toMatchInlineSnapshot(`""`) }) test('removes all @keyframes, even those contributed by JavasScript plugins', async () => { - expect( - await run( - ['animate-spin', 'match-utility-initial', 'match-components-initial'], + await expect( + compileCss( css` @media reference { @layer theme, base, components, utilities; @@ -5682,6 +4861,7 @@ describe('`@reference "…" imports`', () => { @apply animate-spin; } `, + ['animate-spin', 'match-utility-initial', 'match-components-initial'], { loadModule: async () => ({ path: '', @@ -5722,9 +4902,8 @@ describe('`@reference "…" imports`', () => { }), }, ), - ).toMatchInlineSnapshot(` - " - .bar { + ).resolves.toMatchInlineSnapshot(` + ".bar { animation: var(--animate-spin, spin 1s linear infinite); } @@ -5732,8 +4911,7 @@ describe('`@reference "…" imports`', () => { to { transform: rotate(360deg); } - } - " + }" `) }) @@ -5787,19 +4965,19 @@ describe('`@reference "…" imports`', () => { throw new Error('unreachable') } - expect( - await compileCss( - css` + await expect( + compileCss( + ` @reference './one.css'; .bar { @apply text-red animate-wiggle; } `, + [], { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - .bar { + ).resolves.toMatchInlineSnapshot(` + ".bar { animation: var(--animate-wiggle, wiggle 1s ease-in-out infinite); color: var(--color-red, red); } @@ -5812,8 +4990,7 @@ describe('`@reference "…" imports`', () => { 50% { transform: rotate(3deg); } - } - " + }" `) }) @@ -5837,85 +5014,82 @@ describe('`@reference "…" imports`', () => { } } - expect( - await compileCss( - css` - @import './foo/bar.css' reference; + await expect( + compileCss( + ` + @import './foo/bar.css' reference; + + .bar { + @apply md:hocus:foo; + } + `, + [], + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@media (min-width: 768px) { + .bar:hover, .bar:focus { + color: red; + } + }" + `) + }) + + test('removes styles when the import resolver was handled outside of Tailwind CSS', async () => { + await expect( + compileCss( + ` + @media reference { + @layer theme { + @theme { + --breakpoint-md: 48rem; + } + .foo { + color: red; + } + } + @utility foo { + color: red; + } + @custom-variant hocus (&:hover, &:focus); + } .bar { @apply md:hocus:foo; } `, - { loadStylesheet }, + [], ), - ).toMatchInlineSnapshot(` - " - @media (min-width: 768px) { + ).resolves.toMatchInlineSnapshot(` + "@media (min-width: 48rem) { .bar:hover, .bar:focus { color: red; } - } - " - `) - }) - - test('removes styles when the import resolver was handled outside of Tailwind CSS', async () => { - expect( - await compileCss(css` - @media reference { - @layer theme { - @theme { - --breakpoint-md: 48rem; - } - .foo { - color: red; - } - } - @utility foo { - color: red; - } - @custom-variant hocus (&:hover, &:focus); - } - - .bar { - @apply md:hocus:foo; - } - `), - ).toMatchInlineSnapshot(` - " - @media (min-width: 48rem) { - .bar:hover, .bar:focus { - color: red; - } - } - " + }" `) }) }) describe('@variant', () => { it('should convert legacy body-less `@variant` as a `@custom-variant`', async () => { - expect( - await run( - ['hocus:underline'], + await expect( + compileCss( css` @variant hocus (&:hover, &:focus); @tailwind utilities; `, + ['hocus:underline'], ), - ).toMatchInlineSnapshot(` - " - .hocus\\:underline:hover, .hocus\\:underline:focus { + ).resolves.toMatchInlineSnapshot(` + ".hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; - } - " + }" `) }) it('should convert legacy `@variant` with `@slot` as a `@custom-variant`', async () => { - expect( - await run( - ['hocus:underline'], + await expect( + compileCss( css` @variant hocus { &:hover { @@ -5928,62 +5102,63 @@ describe('@variant', () => { } @tailwind utilities; `, + ['hocus:underline'], ), - ).toMatchInlineSnapshot(` - " - .hocus\\:underline:hover, .hocus\\:underline:focus { + ).resolves.toMatchInlineSnapshot(` + ".hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; - } - " + }" `) }) it('should be possible to use `@variant` in your CSS', async () => { - expect( - await compileCss(css` - .btn { - background: black; + await expect( + compileCss( + css` + .btn { + background: black; - @variant dark { - background: white; + @variant dark { + background: white; + } } - } - @variant hover { - @variant landscape { - .btn2 { + @variant hover { + @variant landscape { + .btn2 { + color: red; + } + } + } + + @variant hover { + .foo { color: red; } + @variant landscape { + .bar { + color: blue; + } + } + .baz { + @variant portrait { + color: green; + } + } } - } - @variant hover { - .foo { - color: red; - } - @variant landscape { - .bar { - color: blue; + @media something { + @variant landscape { + @page { + color: red; + } } } - .baz { - @variant portrait { - color: green; - } - } - } - - @media something { - @variant landscape { - @page { - color: red; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - .btn { + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { background: #000; } @@ -6023,41 +5198,40 @@ describe('@variant', () => { color: red; } } - } - " + }" `) }) it('should be possible to use `@variant` in your CSS with a `@custom-variant` that is defined later', async () => { - expect( - await compileCss(css` - .btn { - background: black; + await expect( + compileCss( + css` + .btn { + background: black; - @variant hocus { - background: white; + @variant hocus { + background: white; + } } - } - @custom-variant hocus (&:hover, &:focus); - `), - ).toMatchInlineSnapshot(` - " - .btn { + @custom-variant hocus (&:hover, &:focus); + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { background: #000; } .btn:hover, .btn:focus { background: #fff; - } - " + }" `) }) it('should be possible to use nested `@variant` rules', async () => { - expect( - await run( - ['disabled:focus:underline'], + await expect( + compileCss( css` .btn { background: black; @@ -6070,10 +5244,10 @@ describe('@variant', () => { } @tailwind utilities; `, + ['disabled:focus:underline'], ), - ).toMatchInlineSnapshot(` - " - .btn { + ).resolves.toMatchInlineSnapshot(` + ".btn { background: #000; } @@ -6083,427 +5257,32 @@ describe('@variant', () => { .disabled\\:focus\\:underline:disabled:focus { text-decoration-line: underline; - } - " + }" `) }) - describe('comma-separated `@variant` rules', () => { - it('should be possible to use comma-separated `@variant` rules', async () => { - let input = css` - .btn { - background: black; - - @variant hover, focus { - background: red; - } - } - @tailwind utilities; - ` - - expect(await compileCss(input)).toMatchInlineSnapshot(` - " - .btn { - background: #000; - } - - @media (hover: hover) { - .btn:hover { - background: red; - } - } - - .btn:focus { - background: red; - } - " - `) - - expect(await compileCss(input)).toEqual( - await compileCss(css` - .btn { - background: black; - - @variant hover { - background: red; - } - @variant focus { - background: red; - } - } - @tailwind utilities; - `), - ) - }) - - it.each( - Array.from( - cartesian( - ['', ' ', ' ', '\t', '\t\t'], // Before - ['', ' ', ' ', '\t', '\t\t'], // After - ), - ), - )( - "should handle optional whitespace ('%s', '%s') between `@variant` variants", - async (before, after) => { - expect( - await compileCss(css` - .btn { - background: black; - - @variant hover${before},${after}focus { - background: red; - } - } - @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - .btn { - background: #000; - } - - @media (hover: hover) { - .btn:hover { - background: red; - } - } - - .btn:focus { - background: red; - } - " - `) - }, - ) - - it('should handle variants containing a `,` inside', async () => { - expect( - await compileCss(css` - .btn { - background: black; - - @variant [&:is(:hover,:focus)], disabled { - background: red; - } - } - @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - .btn { - background: #000; - } - - .btn:is(:hover, :focus), .btn:disabled { - background: red; - } - " - `) - }) - - it('should handle nested comma-separated variants', async () => { - let input = css` - .btn { - background: black; - - @variant hover, focus { - background: red; - - @variant active, disabled { - background: blue; - } - } - } - @tailwind utilities; - ` - - expect(await compileCss(input)).toMatchInlineSnapshot(` - " - .btn { - background: #000; - } - - @media (hover: hover) { - .btn:hover { - background: red; - } - - .btn:hover:active, .btn:hover:disabled { - background: #00f; - } - } - - .btn:focus { - background: red; - } - - .btn:focus:active, .btn:focus:disabled { - background: #00f; - } - " - `) - - expect(await compileCss(input)).toEqual( - await compileCss(css` - .btn { - background: black; - - @variant hover { - background: red; - - @variant active { - background: blue; - } - - @variant disabled { - background: blue; - } - } - - @variant focus { - background: red; - - @variant active { - background: blue; - } - - @variant disabled { - background: blue; - } - } - } - @tailwind utilities; - `), - ) - }) - - it('should error on invalid variants (trailing comma)', async () => { - await expect( - compileCss(css` - .btn { - background: black; - - @variant hover,focus, { - background: red; - } - } - @tailwind utilities; - `), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Cannot use \`@variant\` with empty variant]`, - ) - }) - - it('should error on invalid variants (double comma)', async () => { - await expect( - compileCss(css` - .btn { - background: black; - - @variant hover,,focus { - background: red; - } - } - @tailwind utilities; - `), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Cannot use \`@variant\` with empty variant]`, - ) - }) - }) - - describe('stacked `@variant` rules', () => { - it('should handle stacked variants', async () => { - expect( - await compileCss(css` - .btn { - background: black; - - @variant hover:focus { - background: red; - } - } - @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - .btn { - background: #000; - } - - @media (hover: hover) { - .btn:hover:focus { - background: red; - } - } - " - `) - }) - - it('should handle stacked variants & comma-separated variants', async () => { - expect( - await compileCss(css` - .btn { - background: black; - - @variant hover:focus, disabled { - background: red; - } - } - @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - .btn { - background: #000; - } - - @media (hover: hover) { - .btn:hover:focus { - background: red; - } - } - - .btn:disabled { - background: red; - } - " - `) - }) - - it('should handle variants containing a `:` inside', async () => { - expect( - await compileCss(css` - .btn { - background: black; - - @variant [&:is(:hover,:focus)]:disabled, aria-disabled:hover { - background: red; - } - } - @tailwind utilities; - `), - ).toMatchInlineSnapshot(` - " - .btn { - background: #000; - } - - .btn:is(:hover, :focus):disabled { - background: red; - } - - @media (hover: hover) { - .btn[aria-disabled="true"]:hover { - background: red; - } - } - " - `) - }) - }) - - it('should be possible to use compound and stacked variants in `@variant`', async () => { - let input = css` - .btn { - background: black; - - @variant data-a, data-b:data-c { - background: red; - - @variant data-d, data-e:data-f { - background: blue; - } - } - } - @tailwind utilities; - ` - - expect(await compileCss(input)).toMatchInlineSnapshot(` - " - .btn { - background: #000; - } - - .btn[data-a] { - background: red; - } - - .btn[data-a][data-d], .btn[data-a][data-e][data-f] { - background: #00f; - } - - .btn[data-b][data-c] { - background: red; - } - - .btn[data-b][data-c][data-d], .btn[data-b][data-c][data-e][data-f] { - background: #00f; - } - " - `) - - expect(await compileCss(input)).toEqual( - await compileCss(css` - .btn { - background: black; - - @variant data-a { - background: red; - - @variant data-d { - background: blue; - } - - @variant data-e { - @variant data-f { - background: blue; - } - } - } - - @variant data-b { - @variant data-c { - background: red; - - @variant data-d { - background: blue; - } - - @variant data-e { - @variant data-f { - background: blue; - } - } - } - } - } - @tailwind utilities; - `), - ) - }) - it('should be possible to use `@variant` with a funky looking variants', async () => { - expect( - await compileCss(css` - @theme inline reference { - --container-md: 768px; - } + await expect( + compileCss( + css` + @theme inline reference { + --container-md: 768px; + } - .btn { - background: black; + .btn { + background: black; - @variant @md { - @variant [&.foo] { - background: white; + @variant @md { + @variant [&.foo] { + background: white; + } } } - } - `), - ).toMatchInlineSnapshot(` - " - .btn { + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { background: #000; } @@ -6511,27 +5290,25 @@ describe('@variant', () => { .btn.foo { background: #fff; } - } - " + }" `) }) }) describe('`color-mix(…)` polyfill', () => { it('creates an inlined variable version of the color-mix(…) usages', async () => { - expect( - await run( - ['text-red-500/50'], + await expect( + compileCss( css` @theme { --color-red-500: oklch(63.7% 0.237 25.331); } @tailwind utilities; `, + ['text-red-500/50'], ), - ).toMatchInlineSnapshot(` - " - :root, :host { + ).resolves.toMatchInlineSnapshot(` + ":root, :host { --color-red-500: oklch(63.7% .237 25.331); } @@ -6543,15 +5320,13 @@ describe('`color-mix(…)` polyfill', () => { .text-red-500\\/50 { color: color-mix(in oklab, var(--color-red-500) 50%, transparent); } - } - " + }" `) }) it('creates an inlined variable version of the color-mix(…) usages when it resolves to a var(…) containing another theme variable', async () => { - expect( - await run( - ['text-red/50'], + await expect( + compileCss( css` @theme { --color-red: var(--color-red-500); @@ -6559,10 +5334,10 @@ describe('`color-mix(…)` polyfill', () => { } @tailwind utilities; `, + ['text-red/50'], ), - ).toMatchInlineSnapshot(` - " - :root, :host { + ).resolves.toMatchInlineSnapshot(` + ":root, :host { --color-red: var(--color-red-500); --color-red-500: oklch(63.7% .237 25.331); } @@ -6575,26 +5350,27 @@ describe('`color-mix(…)` polyfill', () => { .text-red\\/50 { color: color-mix(in oklab, var(--color-red) 50%, transparent); } - } - " + }" `) }) it('works for color values in the first and second position', async () => { - expect( - await compileCss(css` - @theme { - --color-red-500: oklch(63.7% 0.237 25.331); - --color-orange-500: oklch(70.5% 0.213 47.604); - } - @tailwind utilities; - .mixed { - color: color-mix(in lch, var(--color-red-500) 50%, var(--color-orange-500)); - } - `), - ).toMatchInlineSnapshot(` - " - :root, :host { + await expect( + compileCss( + css` + @theme { + --color-red-500: oklch(63.7% 0.237 25.331); + --color-orange-500: oklch(70.5% 0.213 47.604); + } + @tailwind utilities; + .mixed { + color: color-mix(in lch, var(--color-red-500) 50%, var(--color-orange-500)); + } + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ":root, :host { --color-red-500: oklch(63.7% .237 25.331); --color-orange-500: oklch(70.5% .213 47.604); } @@ -6607,29 +5383,30 @@ describe('`color-mix(…)` polyfill', () => { .mixed { color: color-mix(in lch, var(--color-red-500) 50%, var(--color-orange-500)); } - } - " + }" `) }) it('works for nested `color-mix(…)` calls', async () => { - expect( - await compileCss(css` - @theme { - --color-red-500: oklch(63.7% 0.237 25.331); - } - @tailwind utilities; - .stacked { - color: color-mix( - in lch, - color-mix(in lch, var(--color-red-500) 50%, transparent) 50%, - transparent - ); - } - `), - ).toMatchInlineSnapshot(` - " - :root, :host { + await expect( + compileCss( + css` + @theme { + --color-red-500: oklch(63.7% 0.237 25.331); + } + @tailwind utilities; + .stacked { + color: color-mix( + in lch, + color-mix(in lch, var(--color-red-500) 50%, transparent) 50%, + transparent + ); + } + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ":root, :host { --color-red-500: oklch(63.7% .237 25.331); } @@ -6641,31 +5418,32 @@ describe('`color-mix(…)` polyfill', () => { .stacked { color: color-mix(in lch, color-mix(in lch, var(--color-red-500) 50%, transparent) 50%, transparent); } - } - " + }" `) }) it('works with multiple `color-mix(…)` functions in one declaration', async () => { - expect( - await compileCss(css` - @theme { - --color-red-500: oklch(63.7% 0.237 25.331); - --color-orange-500: oklch(70.5% 0.213 47.604); - } - @tailwind utilities; - .gradient { - background: linear-gradient( - 90deg, - color-mix(in oklab, var(--color-red-500) 50%, transparent) 0%, - color-mix(in oklab, var(--color-orange-500) 50%, transparent) 0%, - 100% - ); - } - `), - ).toMatchInlineSnapshot(` - " - :root, :host { + await expect( + compileCss( + css` + @theme { + --color-red-500: oklch(63.7% 0.237 25.331); + --color-orange-500: oklch(70.5% 0.213 47.604); + } + @tailwind utilities; + .gradient { + background: linear-gradient( + 90deg, + color-mix(in oklab, var(--color-red-500) 50%, transparent) 0%, + color-mix(in oklab, var(--color-orange-500) 50%, transparent) 0%, + 100% + ); + } + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ":root, :host { --color-red-500: oklch(63.7% .237 25.331); --color-orange-500: oklch(70.5% .213 47.604); } @@ -6678,14 +5456,13 @@ describe('`color-mix(…)` polyfill', () => { .gradient { background: linear-gradient(90deg, color-mix(in oklab, var(--color-red-500) 50%, transparent) 0%, color-mix(in oklab, var(--color-orange-500) 50%, transparent) 0%, 100%); } - } - " + }" `) }) it('works with no spaces after the `var(…)`', async () => { - expect( - await compileCss( + await expect( + compileCss( // prettier-ignore css` @theme { @@ -6696,10 +5473,10 @@ describe('`color-mix(…)` polyfill', () => { color: color-mix(in oklab,var(--color-red-500)50%,transparent); } `, + [], ), - ).toMatchInlineSnapshot(` - " - :root, :host { + ).resolves.toMatchInlineSnapshot(` + ":root, :host { --color-red-500: oklch(63.7% .237 25.331); } @@ -6709,27 +5486,25 @@ describe('`color-mix(…)` polyfill', () => { @supports (color: color-mix(in lab, red, red)) { .text-red-500\\/50 { - color: color-mix(in oklab,var(--color-red-500)50%,transparent); + color: color-mix(in oklab, var(--color-red-500) 50%, transparent); } - } - " + }" `) }) it('uses the first color value as the fallback when the `color-mix(…)` function contains non-theme variables', async () => { - expect( - await run( - ['text-(--my-color)/50', 'text-red-500/(--my-opacity)', 'text-(--my-color)/(--my-opacity)'], + await expect( + compileCss( css` @theme { --color-red-500: oklch(63.7% 0.237 25.331); } @tailwind utilities; `, + ['text-(--my-color)/50', 'text-red-500/(--my-opacity)', 'text-(--my-color)/(--my-opacity)'], ), - ).toMatchInlineSnapshot(` - " - :root, :host { + ).resolves.toMatchInlineSnapshot(` + ":root, :host { --color-red-500: oklch(63.7% .237 25.331); } @@ -6761,15 +5536,20 @@ describe('`color-mix(…)` polyfill', () => { .text-red-500\\/\\(--my-opacity\\) { color: color-mix(in oklab, var(--color-red-500) var(--my-opacity), transparent); } - } - " + }" `) }) it('uses the first color value as the fallback when the `color-mix(…)` function contains currentcolor', async () => { - expect(await run(['text-current/50'])).toMatchInlineSnapshot(` - " - .text-current\\/50 { + await expect( + compileCss( + css` + @tailwind utilities; + `, + ['text-current/50'], + ), + ).resolves.toMatchInlineSnapshot(` + ".text-current\\/50 { color: currentColor; } @@ -6777,25 +5557,23 @@ describe('`color-mix(…)` polyfill', () => { .text-current\\/50 { color: color-mix(in oklab, currentcolor 50%, transparent); } - } - " + }" `) }) it('uses the first color value as the fallback when the `color-mix(…)` function contains theme variables that resolves to other variables', async () => { - expect( - await run( - ['text-red/50'], + await expect( + compileCss( css` @tailwind utilities; @theme { --color-red: var(--my-red); } `, + ['text-red/50'], ), - ).toMatchInlineSnapshot(` - " - .text-red\\/50 { + ).resolves.toMatchInlineSnapshot(` + ".text-red\\/50 { color: var(--color-red); } @@ -6807,27 +5585,28 @@ describe('`color-mix(…)` polyfill', () => { :root, :host { --color-red: var(--my-red); - } - " + }" `) }) it('uses the first color value of the inner most `color-mix(…)` function as the fallback when nested `color-mix(…)` function all contain non-theme variables', async () => { - expect( - await compileCss(css` - @tailwind utilities; - .stacked { - color: color-mix( - in oklab, - color-mix(in oklab, var(--my-color) var(--my-inner-opacity), transparent) - var(--my-outer-opacity), - transparent - ); - } - `), - ).toMatchInlineSnapshot(` - " - .stacked { + await expect( + compileCss( + css` + @tailwind utilities; + .stacked { + color: color-mix( + in oklab, + color-mix(in oklab, var(--my-color) var(--my-inner-opacity), transparent) + var(--my-outer-opacity), + transparent + ); + } + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".stacked { color: var(--my-color); } @@ -6835,35 +5614,31 @@ describe('`color-mix(…)` polyfill', () => { .stacked { color: color-mix(in oklab, color-mix(in oklab, var(--my-color) var(--my-inner-opacity), transparent) var(--my-outer-opacity), transparent); } - } - " + }" `) }) it('does not create a fallback when all color values are statically analyzable (lightningcss will flatten this)', async () => { - expect( - await run( - ['text-red-500/50'], + await expect( + compileCss( css` @theme inline { --color-red-500: oklch(63.7% 0.237 25.331); } @tailwind utilities; `, + ['text-red-500/50'], ), - ).toMatchInlineSnapshot(` - " - .text-red-500\\/50 { + ).resolves.toMatchInlineSnapshot(` + ".text-red-500\\/50 { color: oklab(63.7% .214 .101 / .5); - } - " + }" `) }) it('also replaces eventual variables in opacity values', async () => { - expect( - await run( - ['text-red-500/(--my-half)'], + await expect( + compileCss( css` @theme { --my-half: 50%; @@ -6871,10 +5646,10 @@ describe('`color-mix(…)` polyfill', () => { } @tailwind utilities; `, + ['text-red-500/(--my-half)'], ), - ).toMatchInlineSnapshot(` - " - :root, :host { + ).resolves.toMatchInlineSnapshot(` + ":root, :host { --my-half: 50%; --color-red-500: oklch(63.7% .237 25.331); } @@ -6887,15 +5662,13 @@ describe('`color-mix(…)` polyfill', () => { .text-red-500\\/\\(--my-half\\) { color: color-mix(in oklab, var(--color-red-500) var(--my-half), transparent); } - } - " + }" `) }) it('does not delete theme variables from the output', async () => { - expect( - await run( - ['text-red-500', 'shadow-xl', 'opacity-disabled'], + await expect( + compileCss( css` @layer theme { @theme { @@ -6906,10 +5679,10 @@ describe('`color-mix(…)` polyfill', () => { } @tailwind utilities; `, + ['text-red-500', 'shadow-xl', 'opacity-disabled'], ), - ).toMatchInlineSnapshot(` - " - @layer properties { + ).resolves.toMatchInlineSnapshot(` + "@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-shadow: 0 0 #0000; @@ -7036,64 +5809,41 @@ describe('`color-mix(…)` polyfill', () => { syntax: "*"; inherits: false; initial-value: 0 0 #0000; - } - " - `) - }) - - it('does not apply optimizations when already inside a @supports (color: color-mix... block', async () => { - expect( - await run( - ['mixed'], - css` - @tailwind utilities; - @utility mixed { - @supports (color: color-mix(in lab, red, red)) { - background: color-mix(in oklab, var(--color-1), var(--color-2) 0%); - } - } - `, - ), - ).toMatchInlineSnapshot(` - " - @supports (color: color-mix(in lab, red, red)) { - .mixed { - background: color-mix(in oklab, var(--color-1), var(--color-2) 0%); - } - } - " + }" `) }) }) describe('`@property` polyfill', async () => { it('emits fallbacks', async () => { - expect( - await compileCss(css` - @tailwind utilities; + await expect( + compileCss( + css` + @tailwind utilities; - @property --no-inherit-no-value { - syntax: '*'; - inherits: false; - } - @property --no-inherit-value { - syntax: '*'; - inherits: false; - initial-value: red; - } - @property --inherit-no-value { - syntax: '*'; - inherits: true; - } - @property --inherit-value { - syntax: '*'; - inherits: true; - initial-value: red; - } - `), - ).toMatchInlineSnapshot(` - " - @layer properties { + @property --no-inherit-no-value { + syntax: '*'; + inherits: false; + } + @property --no-inherit-value { + syntax: '*'; + inherits: false; + initial-value: red; + } + @property --inherit-no-value { + syntax: '*'; + inherits: true; + } + @property --inherit-value { + syntax: '*'; + inherits: true; + initial-value: red; + } + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + "@layer properties { @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { :root, :host { --inherit-no-value: initial; @@ -7127,14 +5877,13 @@ describe('`@property` polyfill', async () => { syntax: "*"; inherits: true; initial-value: red; - } - " + }" `) }) it('emitting fallbacks can be disabled (necessary for CSS modules)', async () => { - expect( - await compileCss( + await expect( + compileCss( css` @tailwind utilities; @@ -7157,13 +5906,13 @@ describe('`@property` polyfill', async () => { initial-value: red; } `, + [], { polyfills: Polyfills.None, }, ), - ).toMatchInlineSnapshot(` - " - @property --no-inherit-no-value { + ).resolves.toMatchInlineSnapshot(` + "@property --no-inherit-no-value { syntax: "*"; inherits: false } @@ -7183,8 +5932,7 @@ describe('`@property` polyfill', async () => { syntax: "*"; inherits: true; initial-value: red; - } - " + }" `) }) }) @@ -7222,7 +5970,7 @@ describe('feature detection', () => { test('using `@reference`', async () => { let compiler = await compile( css` - @reference 'tailwindcss/preflight'; + @import 'tailwindcss/preflight'; `, { loadStylesheet: async (_, base) => ({ base, path: '', content: '' }) }, ) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index bc279e624..6cf77bc81 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -34,8 +34,7 @@ import { segment } from './utils/segment' import { topologicalSort } from './utils/topological-sort' import { compoundsForSelectors, IS_VALID_VARIANT_NAME, substituteAtVariant } from './variants' import { walk, WalkAction } from './walk' - -export interface Config extends UserConfig {} +export type Config = UserConfig const IS_VALID_PREFIX = /^[a-z]+$/ @@ -275,7 +274,7 @@ async function parseCss( if (path[0] === 'i' && path.startsWith('inline(')) { inline = true - path = path.slice(7, -1).trim() + path = path.slice(7, -1) } if ( @@ -392,25 +391,21 @@ async function parseCss( } } - let usesAtScope = atRuleParams.some((param) => param.startsWith('@scope')) - customVariants.set(name, (designSystem) => { designSystem.variants.static( name, (r) => { - let slot = usesAtScope ? [contextNode({ source: 'user' }, r.nodes)] : r.nodes - let nodes: AstNode[] = [] if (styleRuleSelectors.length > 0) { - nodes.push(styleRule(styleRuleSelectors.join(', '), slot)) + nodes.push(styleRule(styleRuleSelectors.join(', '), r.nodes)) } for (let selector of atRuleParams) { - nodes.push(rule(selector, slot)) + nodes.push(rule(selector, r.nodes)) } - r.nodes = usesAtScope ? [contextNode({ source: 'variant' }, nodes)] : nodes + r.nodes = nodes }, { compounds: compoundsForSelectors([...styleRuleSelectors, ...atRuleParams]), @@ -600,7 +595,7 @@ async function parseCss( } }) - let designSystem = buildDesignSystem(theme, utilitiesNode?.src) + let designSystem = buildDesignSystem(theme) if (important) { designSystem.important = important @@ -860,7 +855,7 @@ export async function compile( } export async function __unstable__loadDesignSystem(css: string, opts: CompileOptions = {}) { - let result = await parseCss(CSS.parse(css, { from: opts.from }), opts) + let result = await parseCss(CSS.parse(css), opts) return result.designSystem } diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 63f9e7576..ca9830664 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -1,6 +1,5 @@ import { expect, test } from 'vitest' import { __unstable__loadDesignSystem } from '.' -import { decl, rule } from './ast' import plugin from './plugin' import { ThemeOptions } from './theme' @@ -97,9 +96,9 @@ test('getVariants compound', async () => { ] expect(list).toEqual([ - ['@media (hover: hover) { :is(:where(.group):hover &) }'], - ['@media (hover: hover) { :is(:where(.group\\/sidebar):hover &) }'], - ['@media (hover: hover) { :is(:is(:where(.group):hover :where(.group)) &) }'], + ['@media (hover: hover) { &:is(:where(.group):hover *) }'], + ['@media (hover: hover) { &:is(:where(.group\\/sidebar):hover *) }'], + ['@media (hover: hover) { &:is(:where(.group):is(:where(.group):hover *) *) }'], [], [], ]) @@ -166,26 +165,6 @@ test('Can produce CSS per candidate using `candidatesToCss`', async () => { `) }) -test('Can produce AST per candidate using `candidatesToAst`', async () => { - let design = await loadDesignSystem() - design.invalidCandidates = new Set(['bg-[#fff]']) - - expect( - design.candidatesToAst(['underline', 'i-dont-exist', 'bg-[#fff]', 'bg-[#000]', 'text-xs']), - ).toEqual([ - [rule('.underline', [decl('text-decoration-line', 'underline')])], - [], - [], - [rule('.bg-\\[\\#000\\]', [decl('background-color', '#000')])], - [ - rule('.text-xs', [ - decl('font-size', 'var(--text-xs)'), - decl('line-height', 'var(--tw-leading, var(--text-xs--line-height))'), - ]), - ], - ]) -}) - test('Utilities do not show wrapping selector in intellisense', async () => { let input = css` @import 'tailwindcss/utilities'; @@ -213,9 +192,11 @@ test('Utilities do not show wrapping selector in intellisense', async () => { text-decoration-line: underline; } ", - "@media (hover: hover) { - .hover\\:line-through:hover { - text-decoration-line: line-through; + ".hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } } } ", @@ -242,9 +223,11 @@ test('Utilities, when marked as important, show as important in intellisense', a text-decoration-line: underline !important; } ", - "@media (hover: hover) { - .hover\\:line-through:hover { - text-decoration-line: line-through !important; + ".hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through !important; + } } } ", @@ -255,7 +238,7 @@ test('Utilities, when marked as important, show as important in intellisense', a test('Static utilities from plugins are listed in hovers and completions', async () => { let input = css` @import 'tailwindcss/utilities'; - @plugin "./plugin.js"; + @plugin "./plugin.js"l; ` let design = await __unstable__loadDesignSystem(input, { @@ -292,7 +275,7 @@ test('Static utilities from plugins are listed in hovers and completions', async test('Functional utilities from plugins are listed in hovers and completions', async () => { let input = css` @import 'tailwindcss/utilities'; - @plugin "./plugin.js"; + @plugin "./plugin.js"l; ` let design = await __unstable__loadDesignSystem(input, { @@ -357,48 +340,48 @@ test('Functional utilities from plugins are listed in hovers and completions', a expect(design.candidatesToCss(['custom-1-red', 'custom-1-green', 'custom-1-unknown'])) .toMatchInlineSnapshot(` - [ - ".custom-1-red { - color: #ff0000; - } - ", - ".custom-1-green { - color: #ff0000; - } - ", - null, - ] - `) + [ + ".custom-1-red { + color: #ff0000; + } + ", + ".custom-1-green { + color: #ff0000; + } + ", + null, + ] + `) expect(design.candidatesToCss(['custom-2-red', 'custom-2-green', 'custom-2-unknown'])) .toMatchInlineSnapshot(` - [ - ".custom-2-red { - color: #ff0000 / 0%; - } - ", - ".custom-2-green { - color: #ff0000 / 0%; - } - ", - null, - ] - `) + [ + ".custom-2-red { + color: #ff0000 / 0%; + } + ", + ".custom-2-green { + color: #ff0000 / 0%; + } + ", + null, + ] + `) expect(design.candidatesToCss(['custom-2-red/50', 'custom-2-red/75', 'custom-2-red/unknown'])) .toMatchInlineSnapshot(` - [ - ".custom-2-red\\/50 { - color: #ff0000 / 50%; - } - ", - ".custom-2-red\\/75 { - color: #ff0000 / 75%; - } - ", - null, - ] - `) + [ + ".custom-2-red\\/50 { + color: #ff0000 / 50%; + } + ", + ".custom-2-red\\/75 { + color: #ff0000 / 75%; + } + ", + null, + ] + `) let classMap = new Map(design.getClassList()) let classNames = Array.from(classMap.keys()) @@ -493,6 +476,11 @@ test('Custom functional @utility', async () => { @import 'tailwindcss/utilities'; @theme reference { + --tab-size-1: 1; + --tab-size-2: 2; + --tab-size-4: 4; + --tab-size-github: 8; + --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); @@ -501,11 +489,10 @@ test('Custom functional @utility', async () => { --spacing: 0.25rem; --spacing-custom: 123px; + } - --negative-1: 1; - --negative-2: 2; - --negative-4: 4; - --negative-a: 8; + @utility tab-* { + tab-size: --value(--tab-size, 'revert', 'initial'); } @utility example-* { @@ -527,7 +514,7 @@ test('Custom functional @utility', async () => { } @utility -negative-* { - margin: --value(--negative-*); + margin: --value(--tab-size-*); } ` @@ -542,6 +529,18 @@ test('Custom functional @utility', async () => { let classMap = new Map(design.getClassList()) let classNames = Array.from(classMap.keys()) + expect(classNames).toContain('tab-1') + expect(classNames).toContain('tab-2') + expect(classNames).toContain('tab-4') + expect(classNames).toContain('tab-github') + expect(classNames).toContain('tab-revert') + expect(classNames).toContain('tab-initial') + + expect(classNames).not.toContain('-tab-1') + expect(classNames).not.toContain('-tab-2') + expect(classNames).not.toContain('-tab-4') + expect(classNames).not.toContain('-tab-github') + expect(classNames).toContain('with-custom-spacing-custom') expect(classNames).not.toContain('with-custom-spacing-0') expect(classNames).not.toContain('with-custom-spacing-0.5') @@ -563,12 +562,12 @@ test('Custom functional @utility', async () => { expect(classNames).toContain('-negative-1') expect(classNames).toContain('-negative-2') expect(classNames).toContain('-negative-4') - expect(classNames).toContain('-negative-a') + expect(classNames).toContain('-negative-github') expect(classNames).not.toContain('--negative-1') expect(classNames).not.toContain('--negative-2') expect(classNames).not.toContain('--negative-4') - expect(classNames).not.toContain('--negative-a') + expect(classNames).not.toContain('--negative-github') expect(classNames).toContain('example-xs') expect(classMap.get('example-xs')?.modifiers).toEqual(['normal', 'foo', 'bar']) @@ -615,13 +614,13 @@ test('Theme keys with underscores are suggested with underscores', async () => { /* Disable the spacing scale */ --spacing: initial; - /* This will get suggested with a dot because its surrounded by numbers */ + /* This will get suggeted with a dot because its surrounded by numbers */ --spacing-1_5: 1.5rem; - /* This will get suggested with a dot */ + /* This will get suggeted with a dot */ --spacing-2\.5: 1.5rem; - /* This will get suggested with an underscore */ + /* This will get suggeted with an underscore */ --spacing-logo_margin: 0.875rem; } diff --git a/packages/tailwindcss/src/plugin.test.ts b/packages/tailwindcss/src/plugin.test.ts index a7704ae58..485381cde 100644 --- a/packages/tailwindcss/src/plugin.test.ts +++ b/packages/tailwindcss/src/plugin.test.ts @@ -1,32 +1,30 @@ import { expect, test } from 'vitest' +import { compile } from '.' import plugin from './plugin' -import { compileCss } from './test-utils/run' const css = String.raw test('plugin', async () => { - expect( - await compileCss( - css` - @plugin "my-plugin"; - `, - { - loadModule: async () => ({ - module: plugin(function ({ addBase }) { - addBase({ - body: { - margin: '0', - }, - }) - }), - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + let input = css` + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: plugin(function ({ addBase }) { + addBase({ + body: { + margin: '0', + }, + }) + }), + base: '/root', + path: '', + }), + }) + + expect(compiler.build([])).toMatchInlineSnapshot(` + "@layer base { body { margin: 0; } @@ -36,30 +34,28 @@ test('plugin', async () => { }) test('plugin.withOptions', async () => { - expect( - await compileCss( - css` - @plugin "my-plugin"; - `, - { - loadModule: async () => ({ - module: plugin.withOptions(function (opts = { foo: '1px' }) { - return function ({ addBase }) { - addBase({ - body: { - margin: opts.foo, - }, - }) - } - }), - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + let input = css` + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: plugin.withOptions(function (opts = { foo: '1px' }) { + return function ({ addBase }) { + addBase({ + body: { + margin: opts.foo, + }, + }) + } + }), + base: '/root', + path: '', + }), + }) + + expect(compiler.build([])).toMatchInlineSnapshot(` + "@layer base { body { margin: 1px; } diff --git a/packages/tailwindcss/src/plugin.ts b/packages/tailwindcss/src/plugin.ts index 8035633b0..5425f03c1 100644 --- a/packages/tailwindcss/src/plugin.ts +++ b/packages/tailwindcss/src/plugin.ts @@ -41,6 +41,5 @@ export type { PluginFn as PluginCreator, Plugin as PluginsConfig, PluginUtils, - PluginWithConfig, ThemeConfig, } diff --git a/packages/tailwindcss/src/prefix.test.ts b/packages/tailwindcss/src/prefix.test.ts index ded304cad..c60383de4 100644 --- a/packages/tailwindcss/src/prefix.test.ts +++ b/packages/tailwindcss/src/prefix.test.ts @@ -1,7 +1,6 @@ import { expect, test } from 'vitest' import { compile } from '.' import plugin from './plugin' -import { compileCss, run } from './test-utils/run' const css = String.raw @@ -15,57 +14,66 @@ test('utilities must be prefixed', async () => { } ` + let compiler = await compile(input) + // Prefixed utilities are generated expect( - await run( - [ - 'tw:underline', - 'tw:hover:line-through', - 'tw:custom', - 'tw:group-hover:flex', - 'tw:peer-hover:flex', - ], - input, - ), + compiler.build([ + 'tw:underline', + 'tw:hover:line-through', + 'tw:custom', + 'tw:group-hover:flex', + 'tw:peer-hover:flex', + ]), ).toMatchInlineSnapshot(` - " - .tw\\:custom { + ".tw\\:custom { color: red; } - .tw\\:underline { text-decoration-line: underline; } - - @media (hover: hover) { - :is(:where(.tw\\:group):hover .tw\\:group-hover\\:flex), :is(:where(.tw\\:peer):hover ~ .tw\\:peer-hover\\:flex) { - display: flex; + .tw\\:group-hover\\:flex { + &:is(:where(.tw\\:group):hover *) { + @media (hover: hover) { + display: flex; + } } - - .tw\\:hover\\:line-through:hover { - text-decoration-line: line-through; + } + .tw\\:peer-hover\\:flex { + &:is(:where(.tw\\:peer):hover ~ *) { + @media (hover: hover) { + display: flex; + } + } + } + .tw\\:hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } } } " `) // Non-prefixed utilities are ignored - expect(await run(['underline', 'hover:line-through', 'custom'], input)).toEqual('') + compiler = await compile(input) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('') }) test('utilities used in @apply must be prefixed', async () => { - // Prefixed utilities are generated - expect( - await compileCss(css` - @theme reference prefix(tw); + let compiler = await compile(css` + @theme reference prefix(tw); - .my-underline { - @apply tw:underline; - } - `), - ).toMatchInlineSnapshot(` - " .my-underline { + @apply tw:underline; + } + `) + + // Prefixed utilities are generated + expect(compiler.build([])).toMatchInlineSnapshot(` + ".my-underline { text-decoration-line: underline; } " @@ -86,26 +94,21 @@ test('utilities used in @apply must be prefixed', async () => { }) test('CSS variables output by the theme are prefixed', async () => { - // Prefixed utilities are generated - expect( - await run( - ['tw:text-red'], - css` - @theme prefix(tw) { - --color-red: #f00; - --color-green: #0f0; - --breakpoint-sm: 640px; - } - - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --tw-color-red: red; + let compiler = await compile(css` + @theme prefix(tw) { + --color-red: #f00; + --color-green: #0f0; + --breakpoint-sm: 640px; } + @tailwind utilities; + `) + + // Prefixed utilities are generated + expect(compiler.build(['tw:text-red'])).toMatchInlineSnapshot(` + ":root, :host { + --tw-color-red: #f00; + } .tw\\:text-red { color: var(--tw-color-red); } @@ -114,81 +117,78 @@ test('CSS variables output by the theme are prefixed', async () => { }) test('CSS theme functions do not use the prefix', async () => { - expect( - await run( - ['tw:[color:theme(--color-red)]', 'tw:text-[theme(--color-red)]'], - css` - @theme prefix(tw) { - --color-red: #f00; - --color-green: #0f0; - --breakpoint-sm: 640px; - } - - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - .tw\\:\\[color\\:theme\\(--color-red\\)\\], .tw\\:text-\\[theme\\(--color-red\\)\\] { - color: red; + let compiler = await compile(css` + @theme prefix(tw) { + --color-red: #f00; + --color-green: #0f0; + --breakpoint-sm: 640px; } - " + + @tailwind utilities; + `) + + expect(compiler.build(['tw:[color:theme(--color-red)]', 'tw:text-[theme(--color-red)]'])) + .toMatchInlineSnapshot(` + ".tw\\:\\[color\\:theme\\(--color-red\\)\\] { + color: #f00; + } + .tw\\:text-\\[theme\\(--color-red\\)\\] { + color: #f00; + } + " + `) + + compiler = await compile(css` + @theme reference prefix(tw) { + --color-red: #f00; + --color-green: #0f0; + --breakpoint-sm: 640px; + } + + @tailwind utilities; `) expect( - await run( - ['tw:[color:theme(--tw-color-red)]', 'tw:text-[theme(--tw-color-red)]'], - css` - @theme reference prefix(tw) { - --color-red: #f00; - --color-green: #0f0; - --breakpoint-sm: 640px; - } - - @tailwind utilities; - `, - ), + compiler.build(['tw:[color:theme(--tw-color-red)]', 'tw:text-[theme(--tw-color-red)]']), ).toEqual('') }) test('JS theme functions do not use the prefix', async () => { - expect( - await run( - ['tw:my-custom'], - css` - @theme prefix(tw) { - --color-red: #f00; - --color-green: #0f0; - --breakpoint-sm: 640px; + let compiler = await compile( + css` + @theme prefix(tw) { + --color-red: #f00; + --color-green: #0f0; + --breakpoint-sm: 640px; + } + + @plugin "./plugin.js"; + + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: plugin(({ addUtilities, theme }) => { + addUtilities({ + '.my-custom': { + color: theme('--color-red'), + }, + }) + + // The theme function does not use the prefix + expect(theme('--tw-color-red')).toEqual(undefined) + }), } - - @plugin "./plugin.js"; - - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: plugin(({ addUtilities, theme }) => { - addUtilities({ - '.my-custom': { - color: theme('--color-red'), - }, - }) - - // The theme function does not use the prefix - expect(theme('--tw-color-red')).toEqual(undefined) - }), - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - .tw\\:my-custom { - color: red; + }, + ) + + expect(compiler.build(['tw:my-custom'])).toMatchInlineSnapshot(` + ".tw\\:my-custom { + color: #f00; } " `) @@ -204,8 +204,8 @@ test('a prefix can be configured via @import theme(…)', async () => { } ` - let options: Partial[1]> = { - async loadStylesheet(_id, base) { + let compiler = await compile(input, { + async loadStylesheet(id, base) { return { path: '', base, @@ -216,39 +216,54 @@ test('a prefix can be configured via @import theme(…)', async () => { `, } }, - } + }) // Prefixed utilities are generated expect( - await run( - ['tw:underline', 'tw:bg-potato', 'tw:hover:line-through', 'tw:custom', 'flex', 'text-potato'], - input, - options, - ), + compiler.build([ + 'tw:underline', + 'tw:bg-potato', + 'tw:hover:line-through', + 'tw:custom', + 'flex', + 'text-potato', + ]), ).toMatchInlineSnapshot(` - " - .tw\\:bg-potato { + ".tw\\:bg-potato { background-color: var(--tw-color-potato, #7a4724); } - .tw\\:custom { color: red; } - .tw\\:underline { text-decoration-line: underline; } - - @media (hover: hover) { - .tw\\:hover\\:line-through:hover { - text-decoration-line: line-through; + .tw\\:hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } } } " `) // Non-prefixed utilities are ignored - expect(await run(['underline', 'hover:line-through', 'custom'], input, options)).toEqual('') + compiler = await compile(input, { + async loadStylesheet(id, base) { + return { + path: '', + base, + content: css` + @theme { + --color-potato: #7a4724; + } + `, + } + }, + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(`""`) }) test('a prefix can be configured via @import prefix(…)', async () => { @@ -260,8 +275,8 @@ test('a prefix can be configured via @import prefix(…)', async () => { } ` - let options: Partial[1]> = { - async loadStylesheet(_id, base) { + let compiler = await compile(input, { + async loadStylesheet(id, base) { return { path: '', base, @@ -273,42 +288,49 @@ test('a prefix can be configured via @import prefix(…)', async () => { `, } }, - } + }) - expect( - await run( - ['tw:underline', 'tw:bg-potato', 'tw:hover:line-through', 'tw:custom'], - input, - options, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --tw-color-potato: #7a4724; - } - - .tw\\:bg-potato { - background-color: var(--tw-color-potato); - } - - .tw\\:custom { - color: red; - } - - .tw\\:underline { - text-decoration-line: underline; - } - - @media (hover: hover) { - .tw\\:hover\\:line-through:hover { - text-decoration-line: line-through; + expect(compiler.build(['tw:underline', 'tw:bg-potato', 'tw:hover:line-through', 'tw:custom'])) + .toMatchInlineSnapshot(` + ":root, :host { + --tw-color-potato: #7a4724; } - } - " - `) + .tw\\:bg-potato { + background-color: var(--tw-color-potato); + } + .tw\\:custom { + color: red; + } + .tw\\:underline { + text-decoration-line: underline; + } + .tw\\:hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } + } + " + `) // Non-prefixed utilities are ignored - expect(await run(['underline', 'hover:line-through', 'custom'], input, options)).toEqual('') + compiler = await compile(input, { + async loadStylesheet(id, base) { + return { + path: '', + base, + content: css` + @theme { + --color-potato: #7a4724; + } + @tailwind utilities; + `, + } + }, + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(`""`) }) test('a prefix must be letters only', async () => { @@ -322,17 +344,15 @@ test('a prefix must be letters only', async () => { }) test('a candidate matching the prefix does not crash', async () => { - expect( - await run( - ['tomato', 'tomato:flex'], - css` - @theme reference prefix(tomato); - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - .tomato\\:flex { + let input = css` + @theme reference prefix(tomato); + @tailwind utilities; + ` + + let compiler = await compile(input) + + expect(compiler.build(['tomato', 'tomato:flex'])).toMatchInlineSnapshot(` + ".tomato\\:flex { display: flex; } " diff --git a/packages/tailwindcss/src/property-order.ts b/packages/tailwindcss/src/property-order.ts index bf9f0791a..f024f6977 100644 --- a/packages/tailwindcss/src/property-order.ts +++ b/packages/tailwindcss/src/property-order.ts @@ -11,8 +11,6 @@ export default [ 'inset-block', 'inset-inline-start', 'inset-inline-end', - 'inset-block-start', - 'inset-block-end', 'top', 'right', 'bottom', @@ -41,8 +39,6 @@ export default [ 'margin-block', 'margin-inline-start', 'margin-inline-end', - 'margin-block-start', - 'margin-block-end', 'margin-top', 'margin-right', 'margin-bottom', @@ -92,8 +88,6 @@ export default [ '--tw-skew-y', 'transform', - 'zoom', - 'animation', 'cursor', @@ -114,8 +108,6 @@ export default [ 'scroll-margin-block', 'scroll-margin-inline-start', 'scroll-margin-inline-end', - 'scroll-margin-block-start', - 'scroll-margin-block-end', 'scroll-margin-top', 'scroll-margin-right', 'scroll-margin-bottom', @@ -126,17 +118,11 @@ export default [ 'scroll-padding-block', 'scroll-padding-inline-start', 'scroll-padding-inline-end', - 'scroll-padding-block-start', - 'scroll-padding-block-end', 'scroll-padding-top', 'scroll-padding-right', 'scroll-padding-bottom', 'scroll-padding-left', - 'scrollbar-width', - 'scrollbar-color', - 'scrollbar-gutter', - 'list-style-position', 'list-style-type', 'list-style-image', @@ -210,8 +196,6 @@ export default [ 'border-block-width', 'border-inline-start-width', 'border-inline-end-width', - 'border-block-start-width', - 'border-block-end-width', 'border-top-width', 'border-right-width', 'border-bottom-width', @@ -222,8 +206,6 @@ export default [ 'border-block-style', 'border-inline-start-style', 'border-inline-end-style', - 'border-block-start-style', - 'border-block-end-style', 'border-top-style', 'border-right-style', 'border-bottom-style', @@ -234,8 +216,6 @@ export default [ 'border-block-color', 'border-inline-start-color', 'border-inline-end-color', - 'border-block-start-color', - 'border-block-end-color', 'border-top-color', 'border-right-color', 'border-bottom-color', @@ -337,8 +317,6 @@ export default [ 'padding-block', 'padding-inline-start', 'padding-inline-end', - 'padding-block-start', - 'padding-block-end', 'padding-top', 'padding-right', 'padding-bottom', @@ -349,7 +327,6 @@ export default [ 'vertical-align', 'font-family', - 'font-feature-settings', 'font-size', 'line-height', 'font-weight', @@ -360,7 +337,6 @@ export default [ 'text-overflow', 'hyphens', 'white-space', - 'tab-size', 'color', 'text-transform', diff --git a/packages/tailwindcss/src/selector-parser.test.ts b/packages/tailwindcss/src/selector-parser.test.ts index fa38dc578..132359ccc 100644 --- a/packages/tailwindcss/src/selector-parser.test.ts +++ b/packages/tailwindcss/src/selector-parser.test.ts @@ -9,315 +9,54 @@ describe('parse', () => { it('should parse a compound selector', () => { expect(parse('.foo.bar:hover#id')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '.bar' }, - { kind: 'selector', value: ':hover' }, - { kind: 'selector', value: '#id' }, - ], - }, - ]) - }) - - it('should parse a pseudo-element selector with double ::', () => { - expect(parse('.foo::before')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '::before' }, - ], - }, + { kind: 'selector', value: '.foo' }, + { kind: 'selector', value: '.bar' }, + { kind: 'selector', value: ':hover' }, + { kind: 'selector', value: '#id' }, ]) }) it('should parse a selector list', () => { expect(parse('.foo,.bar')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '.bar' }, - ], - }, - ]) - }) - - // We've got 3 options here: - // - // 1. We could throw, but then it becomes more annoying when you don't have - // control over the CSS (for example when it's coming from a package). - // 2. We could parse it and ignore the invalid cases as-if they weren't there. - // Re-printing the AST would turn it into a valid situation. - // 3. We could parse the invalid case and turn it into a `compound` such that - // it stays invalid. When we re-print we would re-introduce the error. - // - // Before this change, we would keep the errors, and pass it along: - // - In a browser environment, these would be ignored - // - In Lightning CSS, these are thrown out - // - // For that reason alone, let's go with option 3 for now, such that the - // behavior is the same as before. This does mean that we see "weird" empty - // compound nodes. - it('should safely parse an invalid selector list', () => { - expect(parse('.foo,')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'compound', nodes: [] }, - ], - }, - ]) - expect(parse(',.foo')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'compound', nodes: [] }, - { kind: 'selector', value: '.foo' }, - ], - }, - ]) - expect(parse(',.foo,')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'compound', nodes: [] }, - { kind: 'selector', value: '.foo' }, - { kind: 'compound', nodes: [] }, - ], - }, - ]) - expect(parse('.foo,,.bar')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'compound', nodes: [] }, - { kind: 'selector', value: '.bar' }, - ], - }, - ]) - }) - - it('should parse selector lists with whitespace around the comma', () => { - expect(parse('.foo, .bar')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '.bar' }, - ], - }, - ]) - - expect(parse('.foo , .bar')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '.bar' }, - ], - }, - ]) - - expect(parse('.foo ,.bar')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '.bar' }, - ], - }, - ]) - }) - - it('should parse a list of attribute selectors', () => { - expect(parse('[foo],[bar]')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'selector', value: '[foo]' }, - { kind: 'selector', value: '[bar]' }, - ], - }, - ]) - }) - - it('should parse a list of selectors with just functions', () => { - expect(parse(':is(.a),:is(.b)')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'function', value: ':is', nodes: [{ kind: 'selector', value: '.a' }] }, - { kind: 'function', value: ':is', nodes: [{ kind: 'selector', value: '.b' }] }, - ], - }, - ]) - }) - - it('should parse selector lists with more than two selectors', () => { - expect(parse('.foo,.bar,.baz')).toEqual([ - { - kind: 'list', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '.bar' }, - { kind: 'selector', value: '.baz' }, - ], - }, - ]) - }) - - it('should group selectors with combinators in a selector list', () => { - expect(parse('.a+.b, .c .d[attr], .e')).toEqual([ - { - kind: 'list', - nodes: [ - { - kind: 'complex', - nodes: [ - { kind: 'selector', value: '.a' }, - { kind: 'combinator', value: '+' }, - { kind: 'selector', value: '.b' }, - ], - }, - { - kind: 'complex', - nodes: [ - { kind: 'selector', value: '.c' }, - { kind: 'combinator', value: ' ' }, - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '.d' }, - { kind: 'selector', value: '[attr]' }, - ], - }, - ], - }, - { kind: 'selector', value: '.e' }, - ], - }, - ]) - }) - - it('should parse complex selectors in a selector list', () => { - expect(parse('#a.b > .c, .d')).toEqual([ - { - kind: 'list', - nodes: [ - { - kind: 'complex', - nodes: [ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '#a' }, - { kind: 'selector', value: '.b' }, - ], - }, - { kind: 'combinator', value: '>' }, - { kind: 'selector', value: '.c' }, - ], - }, - { kind: 'selector', value: '.d' }, - ], - }, + { kind: 'selector', value: '.foo' }, + { kind: 'separator', value: ',' }, + { kind: 'selector', value: '.bar' }, ]) }) it('should combine everything within attribute selectors', () => { expect(parse('.foo[bar="baz"]')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '[bar="baz"]' }, - ], - }, + { kind: 'selector', value: '.foo' }, + { kind: 'selector', value: '[bar="baz"]' }, ]) }) it('should parse functions', () => { expect(parse('.foo:hover:not(.bar:focus)')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: ':hover' }, - { - kind: 'function', - value: ':not', - nodes: [ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '.bar' }, - { kind: 'selector', value: ':focus' }, - ], - }, - ], - }, - ], - }, - ]) - }) - - it('should parse selector lists in functions', () => { - expect(parse(':is(.foo, .bar)')).toEqual([ + { kind: 'selector', value: '.foo' }, + { kind: 'selector', value: ':hover' }, { kind: 'function', - value: ':is', nodes: [ { - kind: 'list', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '.bar' }, - ], + kind: 'selector', + value: '.bar', + }, + { + kind: 'selector', + value: ':focus', }, ], + value: ':not', }, ]) }) it('should handle next-children combinator', () => { expect(parse('.foo + p')).toEqual([ - { - kind: 'complex', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'combinator', value: '+' }, - { kind: 'selector', value: 'p' }, - ], - }, - ]) - }) - - it('should normalize combinators', () => { - expect(parse('.foo + .bar')).toEqual([ - { - kind: 'complex', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'combinator', value: '+' }, - { kind: 'selector', value: '.bar' }, - ], - }, - ]) - - expect(parse('.foo \n\t .bar')).toEqual([ - { - kind: 'complex', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'combinator', value: ' ' }, - { kind: 'selector', value: '.bar' }, - ], - }, + { kind: 'selector', value: '.foo' }, + { kind: 'combinator', value: ' + ' }, + { kind: 'selector', value: 'p' }, ]) }) @@ -326,39 +65,14 @@ describe('parse', () => { }) it('parses :nth-child()', () => { - // The `An+B` part is not a selector, it stays an opaque value expect(parse(':nth-child(n+1)')).toEqual([ - { - kind: 'function', - value: ':nth-child', - nodes: [{ kind: 'value', value: 'n+1' }], - }, - ]) - - // The selector list after `of` is parsed as a selector - expect(parse(':nth-child(2 of &)')).toEqual([ { kind: 'function', value: ':nth-child', nodes: [ - { kind: 'value', value: '2 of ' }, - { kind: 'selector', value: '&' }, - ], - }, - ]) - - expect(parse(':nth-child(2n + 1 of .foo, .bar)')).toEqual([ - { - kind: 'function', - value: ':nth-child', - nodes: [ - { kind: 'value', value: '2n + 1 of ' }, { - kind: 'list', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '.bar' }, - ], + kind: 'value', + value: 'n+1', }, ], }, @@ -368,34 +82,24 @@ describe('parse', () => { it('parses &:has(.child:nth-child(2))', () => { expect(parse('&:has(.child:nth-child(2))')).toEqual([ { - kind: 'compound', + kind: 'selector', + value: '&', + }, + { + kind: 'function', + value: ':has', nodes: [ { kind: 'selector', - value: '&', + value: '.child', }, { kind: 'function', - value: ':has', + value: ':nth-child', nodes: [ { - kind: 'compound', - nodes: [ - { - kind: 'selector', - value: '.child', - }, - { - kind: 'function', - value: ':nth-child', - nodes: [ - { - kind: 'value', - value: '2', - }, - ], - }, - ], + kind: 'value', + value: '2', }, ], }, @@ -407,25 +111,20 @@ describe('parse', () => { it('parses &:has(:nth-child(2))', () => { expect(parse('&:has(:nth-child(2))')).toEqual([ { - kind: 'compound', + kind: 'selector', + value: '&', + }, + { + kind: 'function', + value: ':has', nodes: [ - { - kind: 'selector', - value: '&', - }, { kind: 'function', - value: ':has', + value: ':nth-child', nodes: [ { - kind: 'function', - value: ':nth-child', - nodes: [ - { - kind: 'value', - value: '2', - }, - ], + kind: 'value', + value: '2', }, ], }, @@ -436,117 +135,29 @@ describe('parse', () => { it('parses nesting selector before attribute selector', () => { expect(parse('&[data-foo]')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '&' }, - { kind: 'selector', value: '[data-foo]' }, - ], - }, + { kind: 'selector', value: '&' }, + { kind: 'selector', value: '[data-foo]' }, ]) }) it('parses nesting selector after an attribute selector', () => { expect(parse('[data-foo]&')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '[data-foo]' }, - { kind: 'selector', value: '&' }, - ], - }, + { kind: 'selector', value: '[data-foo]' }, + { kind: 'selector', value: '&' }, ]) }) it('parses universal selector before attribute selector', () => { expect(parse('*[data-foo]')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '*' }, - { kind: 'selector', value: '[data-foo]' }, - ], - }, + { kind: 'selector', value: '*' }, + { kind: 'selector', value: '[data-foo]' }, ]) }) - it('parses the universal selector after an attribute selector', () => { + it('parses universal selector after an attribute selector', () => { expect(parse('[data-foo]*')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '[data-foo]' }, - { kind: 'selector', value: '*' }, - ], - }, - ]) - }) - - it('parses another attribute selector after an attribute selector', () => { - expect(parse('[data-foo][data-bar]')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '[data-foo]' }, - { kind: 'selector', value: '[data-bar]' }, - ], - }, - ]) - }) - - it('parses a type selector before an attribute selector', () => { - expect(parse('div[data-foo]')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: 'div' }, - { kind: 'selector', value: '[data-foo]' }, - ], - }, - ]) - }) - - it('parses a type selector after an attribute selector', () => { - expect(parse('[data-foo]div')).toEqual([ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '[data-foo]' }, - { kind: 'selector', value: 'div' }, - ], - }, - ]) - }) - - it('should parse selector lists as real selectors', () => { - expect(parse('.foo[attr], .bar#id, .baz + .qux')).toEqual([ - { - kind: 'list', - nodes: [ - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '[attr]' }, - ], - }, - { - kind: 'compound', - nodes: [ - { kind: 'selector', value: '.bar' }, - { kind: 'selector', value: '#id' }, - ], - }, - { - kind: 'complex', - nodes: [ - { kind: 'selector', value: '.baz' }, - { kind: 'combinator', value: '+' }, - { kind: 'selector', value: '.qux' }, - ], - }, - ], - }, + { kind: 'selector', value: '[data-foo]' }, + { kind: 'selector', value: '*' }, ]) }) }) @@ -561,20 +172,7 @@ describe('toCss', () => { }) it('should print a selector list', () => { - expect(toCss(parse('.foo, .bar'))).toBe('.foo, .bar') - }) - - it('should print a selector list with normalized commas', () => { - expect(toCss(parse('.foo,.bar'))).toBe('.foo, .bar') - expect(toCss(parse('.foo, .bar'))).toBe('.foo, .bar') - expect(toCss(parse('.foo , .bar'))).toBe('.foo, .bar') - expect(toCss(parse('.foo ,.bar'))).toBe('.foo, .bar') - }) - - it('should print a selector list but minimized', () => { - expect(toCss(parse('.foo, .bar'), true)).toBe('.foo,.bar') - expect(toCss(parse('.foo , .bar'), true)).toBe('.foo,.bar') - expect(toCss(parse('.foo ,.bar'), true)).toBe('.foo,.bar') + expect(toCss(parse('.foo,.bar'))).toBe('.foo,.bar') }) it('should print an attribute selectors', () => { @@ -590,20 +188,7 @@ describe('toCss', () => { }) it('should print :nth-child()', () => { - // The `An+B` part is printed verbatim, whitespace in the selector list - // after `of` is normalized expect(toCss(parse(':nth-child(n+1)'))).toBe(':nth-child(n+1)') - expect(toCss(parse(':nth-child(+2)'))).toBe(':nth-child(+2)') - expect(toCss(parse(':nth-child(2n + 1 of .foo,.bar)'))).toBe(':nth-child(2n + 1 of .foo, .bar)') - }) - - it('should pretty print a complex selector', () => { - expect(toCss(parse('.a+.b, .c .d[attr], .e'))).toBe('.a + .b, .c .d[attr], .e') - }) - - it('should minify a complex selector during printing', () => { - expect(toCss(parse('.a+.b, .c .d[attr], .e'), true)).toBe('.a+.b,.c .d[attr],.e') - // ^ This space is significant to indicate a descendant combinator }) }) diff --git a/packages/tailwindcss/src/selector-parser.ts b/packages/tailwindcss/src/selector-parser.ts index 2a435f687..184eb1286 100644 --- a/packages/tailwindcss/src/selector-parser.ts +++ b/packages/tailwindcss/src/selector-parser.ts @@ -1,31 +1,5 @@ -type Combinator = - | ' ' // Descendant combinator - | '>' // Child combinator - | '+' // Next-sibling combinator - | '~' // Subsequent-sibling combinator - -export type SelectorListNode = { - kind: 'list' - nodes: SelectorAstNode[] -} - export type SelectorCombinatorNode = { kind: 'combinator' - value: Combinator -} - -export type SelectorComplexNode = { - kind: 'complex' - nodes: SelectorAstNode[] -} - -export type SelectorCompoundNode = { - kind: 'compound' - nodes: SelectorAstNode[] -} - -export type SelectorNode = { - kind: 'selector' value: string } @@ -35,63 +9,57 @@ export type SelectorFunctionNode = { nodes: SelectorAstNode[] } +export type SelectorNode = { + kind: 'selector' + value: string +} + export type SelectorValueNode = { kind: 'value' value: string } +export type SelectorSeparatorNode = { + kind: 'separator' + value: string +} + export type SelectorAstNode = - | SelectorFunctionNode | SelectorCombinatorNode - | SelectorComplexNode - | SelectorCompoundNode - | SelectorListNode + | SelectorFunctionNode | SelectorNode + | SelectorSeparatorNode | SelectorValueNode -function combinator(value: Combinator): SelectorCombinatorNode { +function combinator(value: string): SelectorCombinatorNode { return { kind: 'combinator', value, } } -function complex(nodes: SelectorAstNode[]): SelectorComplexNode { - return { - kind: 'complex', - nodes, - } -} - -function compound(nodes: SelectorAstNode[]): SelectorCompoundNode { - return { - kind: 'compound', - nodes, - } -} - -export function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode { +function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode { return { kind: 'function', - value, + value: value, nodes, } } -function list(nodes: SelectorAstNode[]): SelectorListNode { - return { - kind: 'list', - nodes, - } -} - -export function selector(value: string): SelectorNode { +function selector(value: string): SelectorNode { return { kind: 'selector', value, } } +function separator(value: string): SelectorSeparatorNode { + return { + kind: 'separator', + value, + } +} + function value(value: string): SelectorValueNode { return { kind: 'value', @@ -99,110 +67,19 @@ function value(value: string): SelectorValueNode { } } -export function isUniversalSelector(node: SelectorAstNode): boolean { - return node.kind === 'selector' && node.value.charCodeAt(0) === ASTERISK -} - -export function isNestingSelector(node: SelectorAstNode): boolean { - return node.kind === 'selector' && node.value.charCodeAt(0) === AMPERSAND -} - -export function isClassSelector(node: SelectorAstNode): boolean { - return node.kind === 'selector' && node.value.charCodeAt(0) === DOT -} - -export function isIdSelector(node: SelectorAstNode): boolean { - return node.kind === 'selector' && node.value.charCodeAt(0) === HASH -} - -export function isPseudoSelector(node: SelectorAstNode): boolean { - return node.kind === 'selector' && node.value.charCodeAt(0) === COLON -} - -export function isAttributeSelector(node: SelectorAstNode): boolean { - return node.kind === 'selector' && node.value.charCodeAt(0) === OPEN_BRACKET -} - -export function isTypeSelector(node: SelectorAstNode): boolean { - if (node.kind !== 'selector') return false - - switch (node.value.charCodeAt(0)) { - case ASTERISK: // Universal selector - case AMPERSAND: // Nesting selector - case DOT: // Class selector - case HASH: // ID selector - case COLON: // Pseudo selector - case OPEN_BRACKET: // Attribute selector - return false - - // We don't fully verify whether this is actually a proper type selector, - // but we assume it is one if it's not any of the other ones. - default: - return true - } -} - -export function cloneAstNode(node: T): T { - switch (node.kind) { - case 'combinator': - case 'selector': - case 'value': - return { - kind: node.kind, - value: node.value, - } as T - - case 'complex': - case 'compound': - case 'list': - return { - kind: node.kind, - nodes: node.nodes.map(cloneAstNode), - } as T - - case 'function': - return { - kind: node.kind, - value: node.value, - nodes: node.nodes.map(cloneAstNode), - } satisfies SelectorFunctionNode as T - - default: - node satisfies never - throw new Error(`Unknown node kind: ${(node as any).kind}`) - } -} - -export function toCss(ast: SelectorAstNode[], minify = false) { +export function toCss(ast: SelectorAstNode[]) { let css = '' - for (let node of ast) { + for (const node of ast) { switch (node.kind) { + case 'combinator': case 'selector': + case 'separator': case 'value': { css += node.value break } - case 'combinator': { - if (minify || node.value === ' ') { - css += node.value - } else { - css += ` ${node.value} ` - } - break - } - case 'function': { - css += `${node.value}(${toCss(node.nodes, minify)})` - break - } - case 'complex': - case 'compound': { - css += toCss(node.nodes, minify) - break - } - case 'list': { - css += node.nodes.map((node) => toCss([node], minify)).join(minify ? ',' : ', ') - break + css += node.value + '(' + toCss(node.nodes) + ')' } } } @@ -215,10 +92,10 @@ const CLOSE_PAREN = 0x29 const COLON = 0x3a const COMMA = 0x2c const DOUBLE_QUOTE = 0x22 -const DOT = 0x2e +const FULL_STOP = 0x2e const GREATER_THAN = 0x3e const NEWLINE = 0x0a -const HASH = 0x23 +const NUMBER_SIGN = 0x23 const OPEN_BRACKET = 0x5b const OPEN_PAREN = 0x28 const PLUS = 0x2b @@ -234,86 +111,18 @@ export function parse(input: string) { let ast: SelectorAstNode[] = [] - let target = ast + let stack: (SelectorFunctionNode | null)[] = [] - let containsCombinator = false - - let contextStack: { - target: SelectorAstNode[] - currentList: SelectorListNode | null - containsCombinator: boolean - }[] = [] - - let currentList: SelectorListNode | null = null + let parent = null as SelectorFunctionNode | null let buffer = '' let peekChar - function current(nodes = target): SelectorAstNode { - return nodes.length === 1 ? nodes[0] : containsCombinator ? complex(nodes) : compound(nodes) - } - - function append(node: SelectorAstNode) { - let existing = target[target.length - 1] - - if (existing?.kind === 'compound') { - existing.nodes.push(node) - } else if (existing && existing.kind !== 'list' && existing.kind !== 'combinator') { - target[target.length - 1] = compound([existing, node]) - } else { - target.push(node) - } - } - for (let i = 0; i < input.length; i++) { let currentChar = input.charCodeAt(i) switch (currentChar) { - // Handle selector lists - // - // ```css - // .foo, .bar {} - // ^ - // ``` - case COMMA: { - // Flush remaining buffer as a selector - if (buffer.length > 0) { - append(selector(buffer)) - buffer = '' - } - - // Skip whitespace - for (; i + 1 < input.length; i++) { - peekChar = input.charCodeAt(i + 1) - if (peekChar !== NEWLINE && peekChar !== SPACE && peekChar !== TAB) { - break - } - } - - // Add nodes to the current list - if (currentList) { - currentList.nodes.push(current()) - target = [] - containsCombinator = false - } - - // Start a new list - else { - let nodes = target.splice(0) - let item = current(nodes) - let node = list([item]) - target.push(node) - currentList = node - target = [] - containsCombinator = false - } - - break - } - - // Handle combinators - // // E.g.: // // ```css @@ -323,24 +132,31 @@ export function parse(input: string) { // .foo > .bar // ^^^ // ``` + case COMMA: case GREATER_THAN: case NEWLINE: case SPACE: case PLUS: case TAB: case TILDE: { - // Flush remaining buffer as a selector + // 1. Handle everything before the combinator as a selector if (buffer.length > 0) { - append(selector(buffer)) + let node = selector(buffer) + if (parent) { + parent.nodes.push(node) + } else { + ast.push(node) + } buffer = '' } - // Look ahead and find the end of the combinator + // 2. Look ahead and find the end of the combinator let start = i let end = i + 1 for (; end < input.length; end++) { peekChar = input.charCodeAt(end) if ( + peekChar !== COMMA && peekChar !== GREATER_THAN && peekChar !== NEWLINE && peekChar !== SPACE && @@ -353,21 +169,18 @@ export function parse(input: string) { } i = end - 1 - let value = input.slice(start, end).trim() - if ( - value === '' && - (target.length === 0 || end >= input.length || input.charCodeAt(end) === COMMA) - ) { - break + let contents = input.slice(start, end) + let node = contents.trim() === ',' ? separator(contents) : combinator(contents) + if (parent) { + parent.nodes.push(node) + } else { + ast.push(node) } - target.push(combinator((value === '' ? ' ' : value) as Combinator)) - containsCombinator = true - break } - // Start of a function call + // Start of a function call. // // E.g.: // @@ -379,7 +192,7 @@ export function parse(input: string) { let node = fun(buffer, []) buffer = '' - // If the function is not one of the following, we combine all its + // If the function is not one of the following, we combine all it's // contents into a single value node if ( node.value !== ':not' && @@ -391,7 +204,7 @@ export function parse(input: string) { let start = i + 1 let nesting = 0 - // Find the closing bracket + // Find the closing bracket. for (let j = i + 1; j < input.length; j++) { peekChar = input.charCodeAt(j) if (peekChar === OPEN_PAREN) { @@ -408,57 +221,31 @@ export function parse(input: string) { } let end = i - let contents = input.slice(start, end) - - // `:nth-child(…)` and `:nth-last-child(…)` can contain an - // `of ` clause. The selector list must be - // parsed (e.g. to be able to substitute `&`), but the `An+B` part - // is not a selector so it stays an opaque value node. E.g.: - // - // ```css - // :nth-child(2n + 1 of .foo, .bar) - // ^^^^^^^^^^ value - // ^^^^^^^^^^ selector list - // ``` - if (node.value === ':nth-child' || node.value === ':nth-last-child') { - let idx = contents.indexOf('of ') - if (idx !== -1) { - node.nodes.push( - value( - contents.slice(0, idx + 3), // value `2n + 1 of ` - ), - ...parse( - contents.slice(idx + 3), // `.foo, .bar` - ), - ) - buffer = '' - i = end - - append(node) - - break - } - } - - node.nodes.push(value(contents)) + node.nodes.push(value(input.slice(start, end))) buffer = '' i = end - append(node) + if (parent) { + parent.nodes.push(node) + } else { + ast.push(node) + } break } - append(node) - contextStack.push({ target, currentList, containsCombinator }) - target = node.nodes - containsCombinator = false - currentList = null + if (parent) { + parent.nodes.push(node) + } else { + ast.push(node) + } + stack.push(node) + parent = node break } - // End of a function call + // End of a function call. // // E.g.: // @@ -467,27 +254,25 @@ export function parse(input: string) { // ^ // ``` case CLOSE_PAREN: { - // Flush remaining buffer as a selector + let tail = stack.pop() + + // Handle everything before the closing paren a selector if (buffer.length > 0) { - append(selector(buffer)) + let node = selector(buffer) + tail!.nodes.push(node) buffer = '' } - if (currentList) { - currentList.nodes.push(current()) - } else if (containsCombinator) { - target.splice(0, target.length, complex(target.splice(0))) + if (stack.length > 0) { + parent = stack[stack.length - 1] + } else { + parent = null } - let context = contextStack.pop() - target = context?.target ?? ast - currentList = context?.currentList ?? null - containsCombinator = context?.containsCombinator ?? false - break } - // Split compound selectors + // Split compound selectors. // // E.g.: // @@ -495,24 +280,24 @@ export function parse(input: string) { // .foo.bar // ^ // ``` - case DOT: + case FULL_STOP: case COLON: - case HASH: { - if (currentChar === COLON && buffer === ':') { - buffer += input[i] - break - } - - // Handle everything before the combinator as a selector and start a new - // selector + case NUMBER_SIGN: { + // Handle everything before the combinator as a selector and + // start a new selector if (buffer.length > 0) { - append(selector(buffer)) + let node = selector(buffer) + if (parent) { + parent.nodes.push(node) + } else { + ast.push(node) + } } buffer = input[i] break } - // Start of an attribute selector + // Start of an attribute selector. // // NOTE: Right now we don't care about the individual parts of the // attribute selector, we just want to find the matching closing bracket. @@ -521,16 +306,21 @@ export function parse(input: string) { // future, then we can use the `AttributeSelectorParser` here (and even // inline it if needed) case OPEN_BRACKET: { - // Flush remaining buffer as a selector + // Handle everything before the combinator as a selector if (buffer.length > 0) { - append(selector(buffer)) - buffer = '' + let node = selector(buffer) + if (parent) { + parent.nodes.push(node) + } else { + ast.push(node) + } } + buffer = '' let start = i let nesting = 0 - // Find the closing bracket + // Find the closing bracket. for (let j = i + 1; j < input.length; j++) { peekChar = input.charCodeAt(j) if (peekChar === OPEN_BRACKET) { @@ -546,11 +336,12 @@ export function parse(input: string) { } } - append(selector(input.slice(start, i + 1))) + // Adjust `buffer` to include the string. + buffer += input.slice(start, i + 1) break } - // Start of a string + // Start of a string. case SINGLE_QUOTE: case DOUBLE_QUOTE: { let start = i @@ -562,43 +353,52 @@ export function parse(input: string) { // // ```css // "This is a string with a 'quote' in it" - // ^ ^ -> These are not the end of the string + // ^ ^ -> These are not the end of the string. // ``` for (let j = i + 1; j < input.length; j++) { peekChar = input.charCodeAt(j) - // Current character is a `\` therefore the next character is escaped + // Current character is a `\` therefore the next character is escaped. if (peekChar === BACKSLASH) { j += 1 } - // End of the string + // End of the string. else if (peekChar === currentChar) { i = j break } } - // Adjust `buffer` to include the string + // Adjust `buffer` to include the string. buffer += input.slice(start, i + 1) break } - // Nesting `&` is always a new selector - // Universal `*` is always a new selector + // Nesting `&` is always a new selector. + // Universal `*` is always a new selector. case AMPERSAND: case ASTERISK: { - // Flush remaining buffer as a selector + // 1. Handle everything before the combinator as a selector if (buffer.length > 0) { - append(selector(buffer)) + let node = selector(buffer) + if (parent) { + parent.nodes.push(node) + } else { + ast.push(node) + } buffer = '' } - // Handle the `&` or `*` as a selector on its own - append(selector(input[i])) + // 2. Handle the `&` or `*` as a selector on its own + if (parent) { + parent.nodes.push(selector(input[i])) + } else { + ast.push(selector(input[i])) + } break } - // Escaped characters + // Escaped characters. case BACKSLASH: { buffer += input[i] + input[i + 1] i += 1 @@ -612,15 +412,9 @@ export function parse(input: string) { } } - // Collect the remainder as a selector + // Collect the remainder as a word if (buffer.length > 0) { - append(selector(buffer)) - } - - if (currentList) { - currentList.nodes.push(current()) - } else if (containsCombinator) { - target.splice(0, target.length, complex(target.splice(0))) + ast.push(selector(buffer)) } return ast diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts index 2bc7b0962..8433e8aa0 100644 --- a/packages/tailwindcss/src/source-maps/source-map.test.ts +++ b/packages/tailwindcss/src/source-maps/source-map.test.ts @@ -3,13 +3,12 @@ import dedent from 'dedent' import MagicString from 'magic-string' import * as fs from 'node:fs/promises' import * as path from 'node:path' -import { SourceMapGenerator, type RawSourceMap } from 'source-map-js' +import { SourceMapConsumer, SourceMapGenerator, type RawSourceMap } from 'source-map-js' import { test } from 'vitest' import { compile } from '..' import createPlugin from '../plugin' import { DefaultMap } from '../utils/default-map' import type { DecodedSource, DecodedSourceMap } from './source-map' -import { visualizeSourceMap } from './visualize-source-map' const css = dedent interface RunOptions { @@ -43,7 +42,7 @@ async function run({ input, candidates, options }: RunOptions) { let map = JSON.parse(rawMap.toString()) as RawSourceMap let sources = combined.sources - let annotations = visualizeSourceMap(map, css) + let annotations = formattedMappings(map) return { css, map, sources, annotations } } @@ -81,6 +80,72 @@ function toRawSourceMap(map: DecodedSourceMap): string { return generator.toString() } +/** + * An string annotation that represents a source map + * + * It's not meant to be exhaustive just enough to + * verify that the source map is working and that + * lines are mapped back to the original source + * + * Including when using @apply with multiple classes + */ +function formattedMappings(map: RawSourceMap) { + const smc = new SourceMapConsumer(map) + const annotations: Record< + number, + { + original: { start: [number, number]; end: [number, number] } + generated: { start: [number, number]; end: [number, number] } + source: string + } + > = {} + + smc.eachMapping((mapping) => { + let annotation = (annotations[mapping.generatedLine] = annotations[mapping.generatedLine] || { + ...mapping, + + original: { + start: [mapping.originalLine, mapping.originalColumn], + end: [mapping.originalLine, mapping.originalColumn], + }, + + generated: { + start: [mapping.generatedLine, mapping.generatedColumn], + end: [mapping.generatedLine, mapping.generatedColumn], + }, + + source: mapping.source, + }) + + annotation.generated.end[0] = mapping.generatedLine + annotation.generated.end[1] = mapping.generatedColumn + + annotation.original.end[0] = mapping.originalLine! + annotation.original.end[1] = mapping.originalColumn! + }) + + return Object.values(annotations).map((annotation) => { + return `${annotation.source}: ${formatRange(annotation.generated)} <- ${formatRange(annotation.original)}` + }) +} + +function formatRange(range: { start: [number, number]; end: [number, number] }) { + if (range.start[0] === range.end[0]) { + // This range is on the same line + // and the columns are the same + if (range.start[1] === range.end[1]) { + return `${range.start[0]}:${range.start[1]}` + } + + // This range is on the same line + // but the columns are different + return `${range.start[0]}:${range.start[1]}-${range.end[1]}` + } + + // This range spans multiple lines + return `${range.start[0]}:${range.start[1]}-${range.end[0]}:${range.end[1]}` +} + test('source maps trace back to @import location', async ({ expect }) => { let { sources, annotations } = await run({ input: css` @@ -105,484 +170,129 @@ test('source maps trace back to @import location', async ({ expect }) => { // The output CSS should include annotations linking back to: // 1. The class definition `.foo` // 2. The `@apply underline` line inside of it - expect(annotations).toMatchInlineSnapshot(` - " - output.css | original - | - | --- index.css --- - 1 @layer theme, base, components, utilities; | 1 @layer theme, base, components, utilities; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A @ 1:0-41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A @ 1:0-41 - 2 @layer theme { | 3 @import './theme.css' layer(theme); - ^^^^^^^^^^^^^ B @ 2:0-13 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B @ 3:0-34 - | --- theme.css --- - 3 :root, :host { | 1 @theme default { - ^^^^^^^^^^^^^ C @ 3:2-15 | ^^^^^^^^^^^^^^^ C @ 1:0-15 - 4 --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'N... | 2 --font-sans: - ^ D @ 4:4 | ^ D @ 2:2 - 4 --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'N... | 3 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Ar... - ^ E @ 4:4 | ^ E @ 3:0 - 4 --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'N... | 4 sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... F @ 4:4-5:0 | ^ F @ 4:0 - 5 sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; | 4 sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - ... G @ 5:92-6:0 | ... G @ 4:92 - | 5 --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; - 6 --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', '... | 6 --font-mono: - ^ H @ 6:4 | ^ H @ 6:2 - 6 --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', '... | 7 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', - ^ I @ 6:4 | ^ I @ 7:0 - 6 --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', '... | 8 monospace; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... J @ 6:4-7:0 | ^ J @ 8:0 - 7 monospace; | 8 monospace; - ^ K @ 7:13-8:0 | ^ K @ 8:13 - 8 --default-font-family: var(--font-sans); | 494 --default-font-family: --theme(--font-sans, initial); - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ L @ 8:4-43 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ L @ 494:2-54 - | 495 --default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial); - | 496 --default-font-variation-settings: --theme(--font-sans--font-variation-settings, initial); - 9 --default-mono-font-family: var(--font-mono); | 497 --default-mono-font-family: --theme(--font-mono, initial); - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ M @ 9:4-48 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ M @ 497:2-59 - 10 } | - 11 } | - | --- index.css --- - 12 @layer base { | 4 @import './preflight.css' layer(base); - ^^^^^^^^^^^^ N @ 12:0-12 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ N @ 4:0-37 - | --- preflight.css --- - 13 *, ::after, ::before, ::backdrop, ::file-selector-button { | 7 *, - ^ O @ 13:2 | ^ O @ 7:0 - 13 *, ::after, ::before, ::backdrop, ::file-selector-button { | 8 ::after, - ^ P @ 13:2 | ^ P @ 8:0 - 13 *, ::after, ::before, ::backdrop, ::file-selector-button { | 9 ::before, - ^ Q @ 13:2 | ^ Q @ 9:0 - 13 *, ::after, ::before, ::backdrop, ::file-selector-button { | 10 ::backdrop, - ^ R @ 13:2 | ^ R @ 10:0 - 13 *, ::after, ::before, ::backdrop, ::file-selector-button { | 11 ::file-selector-button { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S @ 13:2-59 | ^^^^^^^^^^^^^^^^^^^^^^^ S @ 11:0-23 - 14 box-sizing: border-box; | 12 box-sizing: border-box; /* 1 */ - ^^^^^^^^^^^^^^^^^^^^^^ T @ 14:4-26 | ^^^^^^^^^^^^^^^^^^^^^^ T @ 12:2-24 - 15 margin: 0; | 13 margin: 0; /* 2 */ - ^^^^^^^^^ U @ 15:4-13 | ^^^^^^^^^ U @ 13:2-11 - 16 padding: 0; | 14 padding: 0; /* 2 */ - ^^^^^^^^^^ V @ 16:4-14 | ^^^^^^^^^^ V @ 14:2-12 - 17 border: 0 solid; | 15 border: 0 solid; /* 3 */ - ^^^^^^^^^^^^^^^ W @ 17:4-19 | ^^^^^^^^^^^^^^^ W @ 15:2-17 - | 16 } - 18 } | - 19 html, :host { | 28 html, - ^ X @ 19:2 | ^ X @ 28:0 - 19 html, :host { | 29 :host { - ^^^^^^^^^^^^ Y @ 19:2-14 | ^^^^^^ Y @ 29:0-6 - 20 line-height: 1.5; | 30 line-height: 1.5; /* 1 */ - ^^^^^^^^^^^^^^^^ Z @ 20:4-20 | ^^^^^^^^^^^^^^^^ Z @ 30:2-18 - 21 -webkit-text-size-adjust: 100%; | 31 -webkit-text-size-adjust: 100%; /* 2 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AA @ 21:4-34 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AA @ 31:2-32 - 22 tab-size: 4; | 32 tab-size: 4; /* 3 */ - ^^^^^^^^^^^ AB @ 22:4-15 | ^^^^^^^^^^^ AB @ 32:2-13 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 33 font-family: --theme( - ^ AC @ 23:4 | ^ AC @ 33:2 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 34 --default-font-family, - ^ AD @ 23:4 | ^ AD @ 34:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 35 -apple-system, - ^ AE @ 23:4 | ^ AE @ 35:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 36 BlinkMacSystemFont, - ^ AF @ 23:4 | ^ AF @ 36:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 37 'Segoe UI', - ^ AG @ 23:4 | ^ AG @ 37:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 38 Roboto, - ^ AH @ 23:4 | ^ AH @ 38:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 39 'Helvetica Neue', - ^ AI @ 23:4 | ^ AI @ 39:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 40 'Noto Sans', - ^ AJ @ 23:4 | ^ AJ @ 40:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 41 Arial, - ^ AK @ 23:4 | ^ AK @ 41:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 42 sans-serif, - ^ AL @ 23:4 | ^ AL @ 42:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 43 'Apple Color Emoji', - ^ AM @ 23:4 | ^ AM @ 43:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 44 'Segoe UI Emoji', - ^ AN @ 23:4 | ^ AN @ 44:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 45 'Segoe UI Symbol', - ^ AO @ 23:4 | ^ AO @ 45:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 46 'Noto Color Emoji' - ^ AP @ 23:4 | ^ AP @ 46:0 - 23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 47 ); /* 4 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... AQ @ 23:4-226 | ^^^ AQ @ 47:0-3 - 24 font-feature-settings: var(--default-font-feature-settings, normal); | 48 font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AR @ 24:4-71 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AR @ 48:2-73 - 25 font-variation-settings: var(--default-font-variation-settings, normal); | 49 font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AS @ 25:4-75 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AS @ 49:2-77 - 26 -webkit-tap-highlight-color: transparent; | 50 -webkit-tap-highlight-color: transparent; /* 7 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AT @ 26:4-44 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AT @ 50:2-42 - | 51 } - 27 } | - 28 hr { | 59 hr { - ^^^ AU @ 28:2-5 | ^^^ AU @ 59:0-3 - 29 height: 0; | 60 height: 0; /* 1 */ - ^^^^^^^^^ AV @ 29:4-13 | ^^^^^^^^^ AV @ 60:2-11 - 30 color: inherit; | 61 color: inherit; /* 2 */ - ^^^^^^^^^^^^^^ AW @ 30:4-18 | ^^^^^^^^^^^^^^ AW @ 61:2-16 - 31 border-top-width: 1px; | 62 border-top-width: 1px; /* 3 */ - ^^^^^^^^^^^^^^^^^^^^^ AX @ 31:4-25 | ^^^^^^^^^^^^^^^^^^^^^ AX @ 62:2-23 - | 63 } - 32 } | - 33 abbr:where([title]) { | 69 abbr:where([title]) { - ^^^^^^^^^^^^^^^^^^^^ AY @ 33:2-22 | ^^^^^^^^^^^^^^^^^^^^ AY @ 69:0-20 - 34 -webkit-text-decoration: underline dotted; | 70 -webkit-text-decoration: underline dotted; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AZ @ 34:4-45 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AZ @ 70:2-43 - 35 text-decoration: underline dotted; | 71 text-decoration: underline dotted; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BA @ 35:4-37 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BA @ 71:2-35 - | 72 } - 36 } | - 37 h1, h2, h3, h4, h5, h6 { | 78 h1, - ^ BB @ 37:2 | ^ BB @ 78:0 - 37 h1, h2, h3, h4, h5, h6 { | 79 h2, - ^ BC @ 37:2 | ^ BC @ 79:0 - 37 h1, h2, h3, h4, h5, h6 { | 80 h3, - ^ BD @ 37:2 | ^ BD @ 80:0 - 37 h1, h2, h3, h4, h5, h6 { | 81 h4, - ^ BE @ 37:2 | ^ BE @ 81:0 - 37 h1, h2, h3, h4, h5, h6 { | 82 h5, - ^ BF @ 37:2 | ^ BF @ 82:0 - 37 h1, h2, h3, h4, h5, h6 { | 83 h6 { - ^^^^^^^^^^^^^^^^^^^^^^^ BG @ 37:2-25 | ^^^ BG @ 83:0-3 - 38 font-size: inherit; | 84 font-size: inherit; - ^^^^^^^^^^^^^^^^^^ BH @ 38:4-22 | ^^^^^^^^^^^^^^^^^^ BH @ 84:2-20 - 39 font-weight: inherit; | 85 font-weight: inherit; - ^^^^^^^^^^^^^^^^^^^^ BI @ 39:4-24 | ^^^^^^^^^^^^^^^^^^^^ BI @ 85:2-22 - | 86 } - 40 } | - 41 a { | 92 a { - ^^ BJ @ 41:2-4 | ^^ BJ @ 92:0-2 - 42 color: inherit; | 93 color: inherit; - ^^^^^^^^^^^^^^ BK @ 42:4-18 | ^^^^^^^^^^^^^^ BK @ 93:2-16 - 43 -webkit-text-decoration: inherit; | 94 -webkit-text-decoration: inherit; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BL @ 43:4-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BL @ 94:2-34 - 44 text-decoration: inherit; | 95 text-decoration: inherit; - ^^^^^^^^^^^^^^^^^^^^^^^^ BM @ 44:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ BM @ 95:2-26 - | 96 } - 45 } | - 46 b, strong { | 102 b, - ^ BN @ 46:2 | ^ BN @ 102:0 - 46 b, strong { | 103 strong { - ^^^^^^^^^^ BO @ 46:2-12 | ^^^^^^^ BO @ 103:0-7 - 47 font-weight: bolder; | 104 font-weight: bolder; - ^^^^^^^^^^^^^^^^^^^ BP @ 47:4-23 | ^^^^^^^^^^^^^^^^^^^ BP @ 104:2-21 - | 105 } - 48 } | - 49 code, kbd, samp, pre { | 114 code, - ^ BQ @ 49:2 | ^ BQ @ 114:0 - 49 code, kbd, samp, pre { | 115 kbd, - ^ BR @ 49:2 | ^ BR @ 115:0 - 49 code, kbd, samp, pre { | 116 samp, - ^ BS @ 49:2 | ^ BS @ 116:0 - 49 code, kbd, samp, pre { | 117 pre { - ^^^^^^^^^^^^^^^^^^^^^ BT @ 49:2-23 | ^^^^ BT @ 117:0-4 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 118 font-family: --theme( - ^ BU @ 50:4 | ^ BU @ 118:2 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 119 --default-mono-font-family, - ^ BV @ 50:4 | ^ BV @ 119:0 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 120 ui-monospace, - ^ BW @ 50:4 | ^ BW @ 120:0 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 121 SFMono-Regular, - ^ BX @ 50:4 | ^ BX @ 121:0 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 122 Menlo, - ^ BY @ 50:4 | ^ BY @ 122:0 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 123 Monaco, - ^ BZ @ 50:4 | ^ BZ @ 123:0 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 124 Consolas, - ^ CA @ 50:4 | ^ CA @ 124:0 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 125 'Liberation Mono', - ^ CB @ 50:4 | ^ CB @ 125:0 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 126 'Courier New', - ^ CC @ 50:4 | ^ CC @ 126:0 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 127 monospace - ^ CD @ 50:4 | ^ CD @ 127:0 - 50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 128 ); /* 1 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... CE @ 50:4-148 | ^^^ CE @ 128:0-3 - 51 font-feature-settings: var(--default-mono-font-feature-settings, normal); | 129 font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CF @ 51:4-76 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CF @ 129:2-78 - 52 font-variation-settings: var(--default-mono-font-variation-settings, normal); | 130 font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CG @ 52:4-80 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... CG @ 130:2-82 - 53 font-size: 1em; | 131 font-size: 1em; /* 4 */ - ^^^^^^^^^^^^^^ CH @ 53:4-18 | ^^^^^^^^^^^^^^ CH @ 131:2-16 - | 132 } - 54 } | - 55 small { | 138 small { - ^^^^^^ CI @ 55:2-8 | ^^^^^^ CI @ 138:0-6 - 56 font-size: 80%; | 139 font-size: 80%; - ^^^^^^^^^^^^^^ CJ @ 56:4-18 | ^^^^^^^^^^^^^^ CJ @ 139:2-16 - | 140 } - 57 } | - 58 sub, sup { | 146 sub, - ^ CK @ 58:2 | ^ CK @ 146:0 - 58 sub, sup { | 147 sup { - ^^^^^^^^^ CL @ 58:2-11 | ^^^^ CL @ 147:0-4 - 59 font-size: 75%; | 148 font-size: 75%; - ^^^^^^^^^^^^^^ CM @ 59:4-18 | ^^^^^^^^^^^^^^ CM @ 148:2-16 - 60 line-height: 0; | 149 line-height: 0; - ^^^^^^^^^^^^^^ CN @ 60:4-18 | ^^^^^^^^^^^^^^ CN @ 149:2-16 - 61 position: relative; | 150 position: relative; - ^^^^^^^^^^^^^^^^^^ CO @ 61:4-22 | ^^^^^^^^^^^^^^^^^^ CO @ 150:2-20 - 62 vertical-align: baseline; | 151 vertical-align: baseline; - ^^^^^^^^^^^^^^^^^^^^^^^^ CP @ 62:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ CP @ 151:2-26 - | 152 } - 63 } | - 64 sub { | 154 sub { - ^^^^ CQ @ 64:2-6 | ^^^^ CQ @ 154:0-4 - 65 bottom: -0.25em; | 155 bottom: -0.25em; - ^^^^^^^^^^^^^^^ CR @ 65:4-19 | ^^^^^^^^^^^^^^^ CR @ 155:2-17 - | 156 } - 66 } | - 67 sup { | 158 sup { - ^^^^ CS @ 67:2-6 | ^^^^ CS @ 158:0-4 - 68 top: -0.5em; | 159 top: -0.5em; - ^^^^^^^^^^^ CT @ 68:4-15 | ^^^^^^^^^^^ CT @ 159:2-13 - | 160 } - 69 } | - 70 table { | 168 table { - ^^^^^^ CU @ 70:2-8 | ^^^^^^ CU @ 168:0-6 - 71 text-indent: 0; | 169 text-indent: 0; /* 1 */ - ^^^^^^^^^^^^^^ CV @ 71:4-18 | ^^^^^^^^^^^^^^ CV @ 169:2-16 - 72 border-color: inherit; | 170 border-color: inherit; /* 2 */ - ^^^^^^^^^^^^^^^^^^^^^ CW @ 72:4-25 | ^^^^^^^^^^^^^^^^^^^^^ CW @ 170:2-23 - 73 border-collapse: collapse; | 171 border-collapse: collapse; /* 3 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^ CX @ 73:4-29 | ^^^^^^^^^^^^^^^^^^^^^^^^^ CX @ 171:2-27 - | 172 } - 74 } | - 75 :-moz-focusring:where(:not(iframe)) { | 178 :-moz-focusring:where(:not(iframe)) { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CY @ 75:2-38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CY @ 178:0-36 - 76 outline: auto; | 179 outline: auto; - ^^^^^^^^^^^^^ CZ @ 76:4-17 | ^^^^^^^^^^^^^ CZ @ 179:2-15 - | 180 } - 77 } | - 78 progress { | 186 progress { - ^^^^^^^^^ DA @ 78:2-11 | ^^^^^^^^^ DA @ 186:0-9 - 79 vertical-align: baseline; | 187 vertical-align: baseline; - ^^^^^^^^^^^^^^^^^^^^^^^^ DB @ 79:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ DB @ 187:2-26 - | 188 } - 80 } | - 81 summary { | 194 summary { - ^^^^^^^^ DC @ 81:2-10 | ^^^^^^^^ DC @ 194:0-8 - 82 display: list-item; | 195 display: list-item; - ^^^^^^^^^^^^^^^^^^ DD @ 82:4-22 | ^^^^^^^^^^^^^^^^^^ DD @ 195:2-20 - | 196 } - 83 } | - 84 ol, ul, menu { | 202 ol, - ^ DE @ 84:2 | ^ DE @ 202:0 - 84 ol, ul, menu { | 203 ul, - ^ DF @ 84:2 | ^ DF @ 203:0 - 84 ol, ul, menu { | 204 menu { - ^^^^^^^^^^^^^ DG @ 84:2-15 | ^^^^^ DG @ 204:0-5 - 85 list-style: none; | 205 list-style: none; - ^^^^^^^^^^^^^^^^ DH @ 85:4-20 | ^^^^^^^^^^^^^^^^ DH @ 205:2-18 - | 206 } - 86 } | - 87 img, svg, video, canvas, audio, iframe, embed, object { | 214 img, - ^ DI @ 87:2 | ^ DI @ 214:0 - 87 img, svg, video, canvas, audio, iframe, embed, object { | 215 svg, - ^ DJ @ 87:2 | ^ DJ @ 215:0 - 87 img, svg, video, canvas, audio, iframe, embed, object { | 216 video, - ^ DK @ 87:2 | ^ DK @ 216:0 - 87 img, svg, video, canvas, audio, iframe, embed, object { | 217 canvas, - ^ DL @ 87:2 | ^ DL @ 217:0 - 87 img, svg, video, canvas, audio, iframe, embed, object { | 218 audio, - ^ DM @ 87:2 | ^ DM @ 218:0 - 87 img, svg, video, canvas, audio, iframe, embed, object { | 219 iframe, - ^ DN @ 87:2 | ^ DN @ 219:0 - 87 img, svg, video, canvas, audio, iframe, embed, object { | 220 embed, - ^ DO @ 87:2 | ^ DO @ 220:0 - 87 img, svg, video, canvas, audio, iframe, embed, object { | 221 object { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DP @ 87:2-56 | ^^^^^^^ DP @ 221:0-7 - 88 display: block; | 222 display: block; /* 1 */ - ^^^^^^^^^^^^^^ DQ @ 88:4-18 | ^^^^^^^^^^^^^^ DQ @ 222:2-16 - 89 vertical-align: middle; | 223 vertical-align: middle; /* 2 */ - ^^^^^^^^^^^^^^^^^^^^^^ DR @ 89:4-26 | ^^^^^^^^^^^^^^^^^^^^^^ DR @ 223:2-24 - | 224 } - 90 } | - 91 img, video { | 230 img, - ^ DS @ 91:2 | ^ DS @ 230:0 - 91 img, video { | 231 video { - ^^^^^^^^^^^ DT @ 91:2-13 | ^^^^^^ DT @ 231:0-6 - 92 max-width: 100%; | 232 max-width: 100%; - ^^^^^^^^^^^^^^^ DU @ 92:4-19 | ^^^^^^^^^^^^^^^ DU @ 232:2-17 - 93 height: auto; | 233 height: auto; - ^^^^^^^^^^^^ DV @ 93:4-16 | ^^^^^^^^^^^^ DV @ 233:2-14 - | 234 } - 94 } | - 95 button, input, select, optgroup, textarea, ::file-selector-button { | 243 button, - ^ DW @ 95:2 | ^ DW @ 243:0 - 95 button, input, select, optgroup, textarea, ::file-selector-button { | 244 input, - ^ DX @ 95:2 | ^ DX @ 244:0 - 95 button, input, select, optgroup, textarea, ::file-selector-button { | 245 select, - ^ DY @ 95:2 | ^ DY @ 245:0 - 95 button, input, select, optgroup, textarea, ::file-selector-button { | 246 optgroup, - ^ DZ @ 95:2 | ^ DZ @ 246:0 - 95 button, input, select, optgroup, textarea, ::file-selector-button { | 247 textarea, - ^ EA @ 95:2 | ^ EA @ 247:0 - 95 button, input, select, optgroup, textarea, ::file-selector-button { | 248 ::file-selector-button { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EB @ 95:2-68 | ^^^^^^^^^^^^^^^^^^^^^^^ EB @ 248:0-23 - 96 font: inherit; | 249 font: inherit; /* 1 */ - ^^^^^^^^^^^^^ EC @ 96:4-17 | ^^^^^^^^^^^^^ EC @ 249:2-15 - 97 font-feature-settings: inherit; | 250 font-feature-settings: inherit; /* 1 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ED @ 97:4-34 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ED @ 250:2-32 - 98 font-variation-settings: inherit; | 251 font-variation-settings: inherit; /* 1 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EE @ 98:4-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EE @ 251:2-34 - 99 letter-spacing: inherit; | 252 letter-spacing: inherit; /* 1 */ - ^^^^^^^^^^^^^^^^^^^^^^^ EF @ 99:4-27 | ^^^^^^^^^^^^^^^^^^^^^^^ EF @ 252:2-25 - 100 color: inherit; | 253 color: inherit; /* 1 */ - ^^^^^^^^^^^^^^ EG @ 100:4-18 | ^^^^^^^^^^^^^^ EG @ 253:2-16 - 101 border-radius: 0; | 254 border-radius: 0; /* 2 */ - ^^^^^^^^^^^^^^^^ EH @ 101:4-20 | ^^^^^^^^^^^^^^^^ EH @ 254:2-18 - 102 background-color: transparent; | 255 background-color: transparent; /* 3 */ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EI @ 102:4-33 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EI @ 255:2-31 - 103 opacity: 1; | 256 opacity: 1; /* 4 */ - ^^^^^^^^^^ EJ @ 103:4-14 | ^^^^^^^^^^ EJ @ 256:2-12 - | 257 } - 104 } | - 105 :where(select:is([multiple], [size])) optgroup { | 263 :where(select:is([multiple], [size])) optgroup { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EK @ 105:2-49 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EK @ 263:0-47 - 106 font-weight: bolder; | 264 font-weight: bolder; - ^^^^^^^^^^^^^^^^^^^ EL @ 106:4-23 | ^^^^^^^^^^^^^^^^^^^ EL @ 264:2-21 - | 265 } - 107 } | - 108 :where(select:is([multiple], [size])) optgroup option { | 271 :where(select:is([multiple], [size])) optgroup option { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM @ 108:2-56 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM @ 271:0-54 - 109 padding-inline-start: 20px; | 272 padding-inline-start: 20px; - ^^^^^^^^^^^^^^^^^^^^^^^^^^ EN @ 109:4-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ EN @ 272:2-28 - | 273 } - 110 } | - 111 ::file-selector-button { | 279 ::file-selector-button { - ^^^^^^^^^^^^^^^^^^^^^^^ EO @ 111:2-25 | ^^^^^^^^^^^^^^^^^^^^^^^ EO @ 279:0-23 - 112 margin-inline-end: 4px; | 280 margin-inline-end: 4px; - ^^^^^^^^^^^^^^^^^^^^^^ EP @ 112:4-26 | ^^^^^^^^^^^^^^^^^^^^^^ EP @ 280:2-24 - | 281 } - 113 } | - 114 ::placeholder { | 287 ::placeholder { - ^^^^^^^^^^^^^^ EQ @ 114:2-16 | ^^^^^^^^^^^^^^ EQ @ 287:0-14 - 115 opacity: 1; | 288 opacity: 1; - ^^^^^^^^^^ ER @ 115:4-14 | ^^^^^^^^^^ ER @ 288:2-12 - | 289 } - 116 } | - 117 @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { | 296 @supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or - ^ ES @ 117:2 | ^ ES @ 296:0 - 117 @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { | 297 (contain-intrinsic-size: 1px) /* Safari 17+ */ { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... ET @ 117:2-92 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 297:0-49 - 118 ::placeholder { | 298 ::placeholder { - ^^^^^^^^^^^^^^ EU @ 118:4-18 | ^^^^^^^^^^^^^^ EU @ 298:2-16 - 119 color: currentcolor; | 299 color: color-mix(in oklab, currentcolor 50%, transparent); - ^^^^^^^^^^^^^^^^^^^ EV @ 119:6-25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 299:4-61 - | 300 } - | 301 } - 120 @supports (color: color-mix(in lab, red, red)) { | - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 120:6-53 | - 121 color: color-mix(in oklab, currentcolor 50%, transparent); | - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 121:8-65 | - 122 } | - 123 } | - 124 } | - 125 textarea { | 307 textarea { - ^^^^^^^^^ EW @ 125:2-11 | ^^^^^^^^^ EW @ 307:0-9 - 126 resize: vertical; | 308 resize: vertical; - ^^^^^^^^^^^^^^^^ EX @ 126:4-20 | ^^^^^^^^^^^^^^^^ EX @ 308:2-18 - | 309 } - 127 } | - 128 ::-webkit-search-decoration { | 315 ::-webkit-search-decoration { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 128:2-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 315:0-28 - 129 -webkit-appearance: none; | 316 -webkit-appearance: none; - ^^^^^^^^^^^^^^^^^^^^^^^^ EZ @ 129:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ EZ @ 316:2-26 - | 317 } - 130 } | - 131 ::-webkit-date-and-time-value { | 324 ::-webkit-date-and-time-value { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 131:2-32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 324:0-30 - 132 min-height: 1lh; | 325 min-height: 1lh; /* 1 */ - ^^^^^^^^^^^^^^^ FB @ 132:4-19 | ^^^^^^^^^^^^^^^ FB @ 325:2-17 - 133 text-align: inherit; | 326 text-align: inherit; /* 2 */ - ^^^^^^^^^^^^^^^^^^^ FC @ 133:4-23 | ^^^^^^^^^^^^^^^^^^^ FC @ 326:2-21 - | 327 } - 134 } | - 135 ::-webkit-datetime-edit { | 333 ::-webkit-datetime-edit { - ^^^^^^^^^^^^^^^^^^^^^^^^ FD @ 135:2-26 | ^^^^^^^^^^^^^^^^^^^^^^^^ FD @ 333:0-24 - 136 display: inline-flex; | 334 display: inline-flex; - ^^^^^^^^^^^^^^^^^^^^ FE @ 136:4-24 | ^^^^^^^^^^^^^^^^^^^^ FE @ 334:2-22 - | 335 } - 137 } | - 138 ::-webkit-datetime-edit-fields-wrapper { | 341 ::-webkit-datetime-edit-fields-wrapper { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FF @ 138:2-41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FF @ 341:0-39 - 139 padding: 0; | 342 padding: 0; - ^^^^^^^^^^ FG @ 139:4-14 | ^^^^^^^^^^ FG @ 342:2-12 - | 343 } - 140 } | - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 345 ::-webkit-datetime-edit, - ^ FH @ 141:2 | ^ FH @ 345:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 346 ::-webkit-datetime-edit-year-field, - ^ FI @ 141:2 | ^ FI @ 346:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 347 ::-webkit-datetime-edit-month-field, - ^ FJ @ 141:2 | ^ FJ @ 347:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 348 ::-webkit-datetime-edit-day-field, - ^ FK @ 141:2 | ^ FK @ 348:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 349 ::-webkit-datetime-edit-hour-field, - ^ FL @ 141:2 | ^ FL @ 349:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 350 ::-webkit-datetime-edit-minute-field, - ^ FM @ 141:2 | ^ FM @ 350:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 351 ::-webkit-datetime-edit-second-field, - ^ FN @ 141:2 | ^ FN @ 351:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 352 ::-webkit-datetime-edit-millisecond-field, - ^ FO @ 141:2 | ^ FO @ 352:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 353 ::-webkit-datetime-edit-meridiem-field { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FP @ 141:2-329 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FP @ 353:0-39 - 142 padding-block: 0; | 354 padding-block: 0; - ^^^^^^^^^^^^^^^^ FQ @ 142:4-20 | ^^^^^^^^^^^^^^^^ FQ @ 354:2-18 - | 355 } - 143 } | - 144 ::-webkit-calendar-picker-indicator { | 361 ::-webkit-calendar-picker-indicator { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FR @ 144:2-38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FR @ 361:0-36 - 145 line-height: 1; | 362 line-height: 1; - ^^^^^^^^^^^^^^ FS @ 145:4-18 | ^^^^^^^^^^^^^^ FS @ 362:2-16 - | 363 } - 146 } | - 147 :-moz-ui-invalid { | 369 :-moz-ui-invalid { - ^^^^^^^^^^^^^^^^^ FT @ 147:2-19 | ^^^^^^^^^^^^^^^^^ FT @ 369:0-17 - 148 box-shadow: none; | 370 box-shadow: none; - ^^^^^^^^^^^^^^^^ FU @ 148:4-20 | ^^^^^^^^^^^^^^^^ FU @ 370:2-18 - | 371 } - 149 } | - 150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 377 button, - ^ FV @ 150:2 | ^ FV @ 377:0 - 150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 378 input:where([type='button'], [type='reset'], [type='submit']), - ^ FW @ 150:2 | ^ FW @ 378:0 - 150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 379 ::file-selector-button { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FX @ 150:2-96 | ^^^^^^^^^^^^^^^^^^^^^^^ FX @ 379:0-23 - 151 appearance: button; | 380 appearance: button; - ^^^^^^^^^^^^^^^^^^ FY @ 151:4-22 | ^^^^^^^^^^^^^^^^^^ FY @ 380:2-20 - | 381 } - 152 } | - 153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 387 ::-webkit-inner-spin-button, - ^ FZ @ 153:2 | ^ FZ @ 387:0 - 153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 388 ::-webkit-outer-spin-button { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GA @ 153:2-59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GA @ 388:0-28 - 154 height: auto; | 389 height: auto; - ^^^^^^^^^^^^ GB @ 154:4-16 | ^^^^^^^^^^^^ GB @ 389:2-14 - | 390 } - 155 } | - 156 [hidden]:where(:not([hidden='until-found'])) { | 396 [hidden]:where(:not([hidden='until-found'])) { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GC @ 156:2-47 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GC @ 396:0-45 - 157 display: none !important; | 397 display: none !important; - ^^^^^^^^^^^^^^^^^^^^^^^^ GD @ 157:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ GD @ 397:2-26 - | 398 } - 158 } | - 159 } | - | --- index.css --- - 160 @layer utilities; | 5 @import './utilities.css' layer(utilities); - ^^^^^^^^^^^^^^^^ GE @ 160:0-16 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GE @ 5:0-42 - | --- input.css --- - 161 .foo { | 3 .foo { - ^^^^^ GF @ 161:0-5 | ^^^^^ GF @ 3:0-5 - 162 text-decoration-line: underline; | 4 @apply underline; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GG @ 162:2-33 | ^^^^^^^^^ GG @ 4:9-18 - | 5 } - 163 } | - 164 | - " - `) + expect(annotations).toEqual([ + 'index.css: 1:0-41 <- 1:0-41', + 'index.css: 2:0-13 <- 3:0-34', + 'theme.css: 3:2-15 <- 1:0-15', + 'theme.css: 4:4 <- 2:2-4:0', + 'theme.css: 5:22 <- 4:22', + 'theme.css: 6:4 <- 6:2-8:0', + 'theme.css: 7:13 <- 8:13', + 'theme.css: 8:4-43 <- 446:2-54', + 'theme.css: 9:4-48 <- 449:2-59', + 'index.css: 12:0-12 <- 4:0-37', + 'preflight.css: 13:2-59 <- 7:0-11:23', + 'preflight.css: 14:4-26 <- 12:2-24', + 'preflight.css: 15:4-13 <- 13:2-11', + 'preflight.css: 16:4-14 <- 14:2-12', + 'preflight.css: 17:4-19 <- 15:2-17', + 'preflight.css: 19:2-14 <- 28:0-29:6', + 'preflight.css: 20:4-20 <- 30:2-18', + 'preflight.css: 21:4-34 <- 31:2-32', + 'preflight.css: 22:4-15 <- 32:2-13', + 'preflight.css: 23:4-159 <- 33:2-42:3', + 'preflight.css: 24:4-71 <- 43:2-73', + 'preflight.css: 25:4-75 <- 44:2-77', + 'preflight.css: 26:4-44 <- 45:2-42', + 'preflight.css: 28:2-5 <- 54:0-3', + 'preflight.css: 29:4-13 <- 55:2-11', + 'preflight.css: 30:4-18 <- 56:2-16', + 'preflight.css: 31:4-25 <- 57:2-23', + 'preflight.css: 33:2-22 <- 64:0-20', + 'preflight.css: 34:4-45 <- 65:2-43', + 'preflight.css: 35:4-37 <- 66:2-35', + 'preflight.css: 37:2-25 <- 73:0-78:3', + 'preflight.css: 38:4-22 <- 79:2-20', + 'preflight.css: 39:4-24 <- 80:2-22', + 'preflight.css: 41:2-4 <- 87:0-2', + 'preflight.css: 42:4-18 <- 88:2-16', + 'preflight.css: 43:4-36 <- 89:2-34', + 'preflight.css: 44:4-28 <- 90:2-26', + 'preflight.css: 46:2-12 <- 97:0-98:7', + 'preflight.css: 47:4-23 <- 99:2-21', + 'preflight.css: 49:2-23 <- 109:0-112:4', + 'preflight.css: 50:4-148 <- 113:2-123:3', + 'preflight.css: 51:4-76 <- 124:2-78', + 'preflight.css: 52:4-80 <- 125:2-82', + 'preflight.css: 53:4-18 <- 126:2-16', + 'preflight.css: 55:2-8 <- 133:0-6', + 'preflight.css: 56:4-18 <- 134:2-16', + 'preflight.css: 58:2-11 <- 141:0-142:4', + 'preflight.css: 59:4-18 <- 143:2-16', + 'preflight.css: 60:4-18 <- 144:2-16', + 'preflight.css: 61:4-22 <- 145:2-20', + 'preflight.css: 62:4-28 <- 146:2-26', + 'preflight.css: 64:2-6 <- 149:0-4', + 'preflight.css: 65:4-19 <- 150:2-17', + 'preflight.css: 67:2-6 <- 153:0-4', + 'preflight.css: 68:4-15 <- 154:2-13', + 'preflight.css: 70:2-8 <- 163:0-6', + 'preflight.css: 71:4-18 <- 164:2-16', + 'preflight.css: 72:4-25 <- 165:2-23', + 'preflight.css: 73:4-29 <- 166:2-27', + 'preflight.css: 75:2-18 <- 173:0-16', + 'preflight.css: 76:4-17 <- 174:2-15', + 'preflight.css: 78:2-11 <- 181:0-9', + 'preflight.css: 79:4-28 <- 182:2-26', + 'preflight.css: 81:2-10 <- 189:0-8', + 'preflight.css: 82:4-22 <- 190:2-20', + 'preflight.css: 84:2-15 <- 197:0-199:5', + 'preflight.css: 85:4-20 <- 200:2-18', + 'preflight.css: 87:2-56 <- 209:0-216:7', + 'preflight.css: 88:4-18 <- 217:2-16', + 'preflight.css: 89:4-26 <- 218:2-24', + 'preflight.css: 91:2-13 <- 225:0-226:6', + 'preflight.css: 92:4-19 <- 227:2-17', + 'preflight.css: 93:4-16 <- 228:2-14', + 'preflight.css: 95:2-68 <- 238:0-243:23', + 'preflight.css: 96:4-17 <- 244:2-15', + 'preflight.css: 97:4-34 <- 245:2-32', + 'preflight.css: 98:4-36 <- 246:2-34', + 'preflight.css: 99:4-27 <- 247:2-25', + 'preflight.css: 100:4-18 <- 248:2-16', + 'preflight.css: 101:4-20 <- 249:2-18', + 'preflight.css: 102:4-33 <- 250:2-31', + 'preflight.css: 103:4-14 <- 251:2-12', + 'preflight.css: 105:2-49 <- 258:0-47', + 'preflight.css: 106:4-23 <- 259:2-21', + 'preflight.css: 108:2-56 <- 266:0-54', + 'preflight.css: 109:4-30 <- 267:2-28', + 'preflight.css: 111:2-25 <- 274:0-23', + 'preflight.css: 112:4-26 <- 275:2-24', + 'preflight.css: 114:2-16 <- 282:0-14', + 'preflight.css: 115:4-14 <- 283:2-12', + 'preflight.css: 117:2-92 <- 291:0-292:49', + 'preflight.css: 118:4-18 <- 293:2-16', + 'preflight.css: 119:6-25 <- 294:4-61', + 'preflight.css: 120:6-53 <- 294:4-61', + 'preflight.css: 121:8-65 <- 294:4-61', + 'preflight.css: 125:2-11 <- 302:0-9', + 'preflight.css: 126:4-20 <- 303:2-18', + 'preflight.css: 128:2-30 <- 310:0-28', + 'preflight.css: 129:4-28 <- 311:2-26', + 'preflight.css: 131:2-32 <- 319:0-30', + 'preflight.css: 132:4-19 <- 320:2-17', + 'preflight.css: 133:4-23 <- 321:2-21', + 'preflight.css: 135:2-26 <- 328:0-24', + 'preflight.css: 136:4-24 <- 329:2-22', + 'preflight.css: 138:2-41 <- 336:0-39', + 'preflight.css: 139:4-14 <- 337:2-12', + 'preflight.css: 141:2-329 <- 340:0-348:39', + 'preflight.css: 142:4-20 <- 349:2-18', + 'preflight.css: 144:2-38 <- 356:0-36', + 'preflight.css: 145:4-18 <- 357:2-16', + 'preflight.css: 147:2-19 <- 364:0-17', + 'preflight.css: 148:4-20 <- 365:2-18', + 'preflight.css: 150:2-96 <- 372:0-374:23', + 'preflight.css: 151:4-22 <- 375:2-20', + 'preflight.css: 153:2-59 <- 382:0-383:28', + 'preflight.css: 154:4-16 <- 384:2-14', + 'preflight.css: 156:2-47 <- 391:0-45', + 'preflight.css: 157:4-28 <- 392:2-26', + 'index.css: 160:0-16 <- 5:0-42', + 'input.css: 161:0-5 <- 3:0-5', + 'input.css: 162:2-33 <- 4:9-18', + ]) }) test('source maps are generated for utilities', async ({ expect }) => { @@ -615,31 +325,17 @@ test('source maps are generated for utilities', async ({ expect }) => { expect(sources.length).toBe(2) // The output CSS should include annotations linking back to: - expect(annotations).toMatchInlineSnapshot(` - " - output.css | original - | - | --- utilities.css --- - 1 .flex { | 1 @tailwind utilities; - ^^^^^^ A @ 1:0-6 | ^^^^^^^^^^^^^^^^^^^ A @ 1:0-19 - 2 display: flex; | - ^^^^^^^^^^^^^ A @ 2:2-15 | - 3 } | - 4 .custom { | - ^^^^^^^^ A @ 4:0-8 | - | --- input.css --- - 5 color: orange; | 4 color: orange; - ^^^^^^^^^^^^^ B @ 5:2-15 | ^^^^^^^^^^^^^ B @ 4:2-15 - | 5 } - 6 } | - 7 .custom-js { | - ^^^^^^^^^^^ A @ 7:0-11 | - 8 color: blue; | - ^^^^^^^^^^^ A @ 8:2-13 | - 9 } | - 10 | - " - `) + expect(annotations).toEqual([ + // @tailwind utilities + 'utilities.css: 1:0-6 <- 1:0-19', + 'utilities.css: 2:2-15 <- 1:0-19', + 'utilities.css: 4:0-8 <- 1:0-19', + // color: orange + 'input.css: 5:2-15 <- 4:2-15', + // @tailwind utilities + 'utilities.css: 7:0-11 <- 1:0-19', + 'utilities.css: 8:2-13 <- 1:0-19', + ]) expect(output).toMatchInlineSnapshot(` ".flex { @@ -666,18 +362,11 @@ test('utilities have source maps pointing to the utilities node', async ({ expec expect(sources).toEqual(['input.css']) - expect(annotations).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .underline { | 1 @tailwind utilities; - ^^^^^^^^^^^ A @ 1:0-11 | ^^^^^^^^^^^^^^^^^^^ A @ 1:0-19 - 2 text-decoration-line: underline; | - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A @ 2:2-33 | - 3 } | - 4 | - " - `) + expect(annotations).toEqual([ + // + 'input.css: 1:0-11 <- 1:0-19', + 'input.css: 2:2-33 <- 1:0-19', + ]) }) test('@apply generates source maps', async ({ expect }) => { @@ -687,7 +376,6 @@ test('@apply generates source maps', async ({ expect }) => { color: blue; @apply text-[#000] hover:text-[#f00]; @apply underline; - @apply --my-mixin-1 --my-mixin-2(); color: red; } `, @@ -695,103 +383,16 @@ test('@apply generates source maps', async ({ expect }) => { expect(sources).toEqual(['input.css']) - expect(annotations).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .foo { | 1 .foo { - ^^^^^ A @ 1:0-5 | ^^^^^ A @ 1:0-5 - 2 color: blue; | 2 color: blue; - ^^^^^^^^^^^ B @ 2:2-13 | ^^^^^^^^^^^ B @ 2:2-13 - 3 color: #000; | 3 @apply text-[#000] hover:text-[#f00]; - ^^^^^^^^^^^ C @ 3:2-13 | ^^^^^^^^^^^ C @ 3:9-20 - 4 &:hover { | 3 @apply text-[#000] hover:text-[#f00]; - ^^^^^^^^ D @ 4:2-10 | ^^^^^^^^^^^^^^^^^ D @ 3:21-38 - 5 @media (hover: hover) { | - ^^^^^^^^^^^^^^^^^^^^^^ D @ 5:4-26 | - 6 color: #f00; | - ^^^^^^^^^^^ D @ 6:6-17 | - 7 } | - 8 } | - 9 text-decoration-line: underline; | 4 @apply underline; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E @ 9:2-33 | ^^^^^^^^^ E @ 4:9-18 - 10 @apply --my-mixin-1 --my-mixin-2(); | 5 @apply --my-mixin-1 --my-mixin-2(); - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 10:2-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 5:2-36 - 11 color: red; | 6 color: red; - ^^^^^^^^^^ G @ 11:2-12 | ^^^^^^^^^^ G @ 6:2-12 - | 7 } - 12 } | - 13 | - " - `) -}) - -test('@variant generates source maps', async ({ expect }) => { - let { sources, annotations } = await run({ - input: css` - .foo { - color: red; - - @variant data-a, data-b:data-c { - color: green; - - @variant data-d, data-e:data-f { - color: blue; - } - } - } - `, - }) - - expect(sources).toEqual(['input.css']) - - expect(annotations).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .foo { | 1 .foo { - ^^^^^ A @ 1:0-5 | ^^^^^ A @ 1:0-5 - 2 color: red; | 2 color: red; - ^^^^^^^^^^ B @ 2:2-12 | ^^^^^^^^^^ B @ 2:2-12 - 3 &[data-a] { | - | 4 @variant data-a, data-b:data-c { - 4 color: green; | 5 color: green; - ^^^^^^^^^^^^ C @ 4:4-16 | ^^^^^^^^^^^^ C @ 5:4-16 - 5 &[data-d] { | - | 7 @variant data-d, data-e:data-f { - 6 color: blue; | 8 color: blue; - ^^^^^^^^^^^ D @ 6:6-17 | ^^^^^^^^^^^ D @ 8:6-17 - | 9 } - | 10 } - | 11 } - 7 } | - 8 &[data-e] { | - 9 &[data-f] { | - 10 color: blue; | - ^^^^^^^^^^^ D @ 10:8-19 | - 11 } | - 12 } | - 13 } | - 14 &[data-b] { | - 15 &[data-c] { | - 16 color: green; | - ^^^^^^^^^^^^ C @ 16:6-18 | - 17 &[data-d] { | - 18 color: blue; | - ^^^^^^^^^^^ D @ 18:8-19 | - 19 } | - 20 &[data-e] { | - 21 &[data-f] { | - 22 color: blue; | - ^^^^^^^^^^^ D @ 22:10-21 | - 23 } | - 24 } | - 25 } | - 26 } | - 27 } | - 28 | - " - `) + expect(annotations).toEqual([ + 'input.css: 1:0-5 <- 1:0-5', + 'input.css: 2:2-13 <- 2:2-13', + 'input.css: 3:2-13 <- 3:9-20', + 'input.css: 4:2-10 <- 3:21-38', + 'input.css: 5:4-26 <- 3:21-38', + 'input.css: 6:6-17 <- 3:21-38', + 'input.css: 9:2-33 <- 4:9-18', + 'input.css: 10:2-12 <- 5:2-12', + ]) }) test('license comments preserve source locations', async ({ expect }) => { @@ -801,14 +402,10 @@ test('license comments preserve source locations', async ({ expect }) => { expect(sources).toEqual(['input.css']) - expect(annotations).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 /*! some comment */ | 1 /*! some comment */ - ^^^^^^^^^^^^^^^^^^^ A @ 1:0-19 | ^^^^^^^^^^^^^^^^^^^ A @ 1:0-19 - " - `) + expect(annotations).toEqual([ + // + 'input.css: 1:0-19 <- 1:0-19', + ]) }) test('license comments with new lines preserve source locations', async ({ expect }) => { @@ -818,25 +415,18 @@ test('license comments with new lines preserve source locations', async ({ expec expect(sources).toEqual(['input.css']) - expect(annotations).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 /*! some | 1 /*! some - ^ A @ 1:0 | ^ A @ 1:0 - 1 /*! some | 2 comment */ - ^^^^^^^^^ B @ 1:0-2:0 | ^ B @ 2:0 - 2 comment */ | 2 comment */ - ^ C @ 2:11-3:0 | ^ C @ 2:11 - " - `) + expect(annotations).toEqual([ + // + 'input.css: 1:0 <- 1:0-2:0', + 'input.css: 2:11 <- 2:11', + ]) }) test('Source locations for `addBase` point to the `@plugin` that generated them', async ({ expect, }) => { let { sources, annotations } = await run({ - input: css` + input: dedent` @plugin "./plugin.js"; @config "./config.js"; `, @@ -873,21 +463,13 @@ test('Source locations for `addBase` point to the `@plugin` that generated them' expect(sources).toEqual(['input.css']) - expect(annotations).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 @layer base { | 1 @plugin "./plugin.js"; - ^^^^^^^^^^^^ A @ 1:0-12 | ^^^^^^^^^^^^^^^^^^^^^ A @ 1:0-21 - 2 body { | - ^^^^^ A @ 2:2-7 | - 3 color: red; | - ^^^^^^^^^^ A @ 3:4-14 | - 4 color: green; | 2 @config "./config.js"; - ^^^^^^^^^^^^ B @ 4:4-16 | ^^^^^^^^^^^^^^^^^^^^^ B @ 2:0-21 - 5 } | - 6 } | - 7 | - " - `) + expect(annotations).toEqual([ + // + 'input.css: 1:0-12 <- 1:0-21', + 'input.css: 2:2-7 <- 1:0-21', + 'input.css: 3:4-14 <- 1:0-21', + 'input.css: 6:0-12 <- 2:0-21', + 'input.css: 7:2-7 <- 2:0-21', + 'input.css: 8:4-16 <- 2:0-21', + ]) }) diff --git a/packages/tailwindcss/src/source-maps/translation-map.test.ts b/packages/tailwindcss/src/source-maps/translation-map.test.ts index 0c367ebbd..ad0a9f435 100644 --- a/packages/tailwindcss/src/source-maps/translation-map.test.ts +++ b/packages/tailwindcss/src/source-maps/translation-map.test.ts @@ -3,7 +3,6 @@ import { assert, expect, test } from 'vitest' import { toCss, type AstNode } from '../ast' import * as CSS from '../css-parser' import { createTranslationMap } from './source-map' -import { visualizeSourceMapRanges, type SourceMapVisualizationRange } from './visualize-source-map' async function analyze(input: string) { let ast = CSS.parse(input, { from: 'input.css' }) @@ -14,26 +13,21 @@ async function analyze(input: string) { }) function format(node: AstNode) { - let ranges: SourceMapVisualizationRange[] = [] + let lines: string[] = [] for (let [oStart, oEnd, gStart, gEnd] of translate(node)) { - ranges.push({ - original: { - source: 'input.css', - start: [oStart.line, oStart.column], - end: [oEnd.line, oEnd.column], - }, - generated: - gStart && gEnd - ? { - start: [gStart.line, gStart.column], - end: [gEnd.line, gEnd.column], - } - : null, - }) + let src = `${oStart.line}:${oStart.column}-${oEnd.line}:${oEnd.column}` + + let dst = '(none)' + + if (gStart && gEnd) { + dst = `${gStart.line}:${gStart.column}-${gEnd.line}:${gEnd.column}` + } + + lines.push(`${dst} <- ${src}`) } - return visualizeSourceMapRanges({ 'input.css': input }, css, ranges) + return lines } return { ast, css, format } @@ -44,13 +38,9 @@ test('comment, single line', async () => { assert(ast[0].kind === 'comment') expect(format(ast[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 /*! foo */ | 1 /*! foo */ - ^^^^^^^^^^ A @ 1:0-10 | ^^^^^^^^^^ A @ 1:0-10 - 2 | - " + [ + "1:0-1:10 <- 1:0-1:10", + ] `) expect(css).toMatchInlineSnapshot(` @@ -64,15 +54,9 @@ test('comment, multi line', async () => { assert(ast[0].kind === 'comment') expect(format(ast[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 /*! foo | 1 /*! foo - ^^^^^^^^ A @ 1:0-2:7 | ^^^^^^^^ A @ 1:0-2:7 - 2 bar */ | 2 bar */ - ^^^^^^^ A | ^^^^^^^ A - 3 | - " + [ + "1:0-2:7 <- 1:0-2:7", + ] `) expect(css).toMatchInlineSnapshot(` @@ -88,15 +72,9 @@ test('declaration, normal property, single line', async () => { assert(ast[0].kind === 'rule') assert(ast[0].nodes[0].kind === 'declaration') expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .foo { | - 2 color: red; | 1 .foo { color: red; } - ^^^^^^^^^^ A @ 2:2-12 | ^^^^^^^^^^ A @ 1:7-17 - 3 } | - 4 | - " + [ + "2:2-2:12 <- 1:7-1:17", + ] `) expect(css).toMatchInlineSnapshot(` @@ -121,22 +99,9 @@ test('declaration, normal property, multi line', async () => { assert(ast[0].kind === 'rule') assert(ast[0].nodes[0].kind === 'declaration') expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .foo { | - 2 grid-template-areas: "a b c" "d e f" "g h i"; | 2 grid-template-areas: - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A @ 2:2-46 | ^^^^^^^^^^^^^^^^^^^^ A @ 2:2-5:11 - | 3 "a b c" - | ^^^^^^^^^^^ A - | 4 "d e f" - | ^^^^^^^^^^^ A - | 5 "g h i"; - | ^^^^^^^^^^^ A - | 6 } - 3 } | - 4 | - " + [ + "2:2-2:46 <- 2:2-5:11", + ] `) expect(css).toMatchInlineSnapshot(` @@ -153,15 +118,9 @@ test('declaration, custom property, single line', async () => { assert(ast[0].kind === 'rule') assert(ast[0].nodes[0].kind === 'declaration') expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .foo { | - 2 --foo: bar; | 1 .foo { --foo: bar; } - ^^^^^^^^^^ A @ 2:2-12 | ^^^^^^^^^^ A @ 1:7-17 - 3 } | - 4 | - " + [ + "2:2-2:12 <- 1:7-1:17", + ] `) expect(css).toMatchInlineSnapshot(` @@ -182,18 +141,9 @@ test('declaration, custom property, multi line', async () => { assert(ast[0].kind === 'rule') assert(ast[0].nodes[0].kind === 'declaration') expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .foo { | - 2 --foo: bar | 2 --foo: bar - ^^^^^^^^^^ A @ 2:2-3:3 | ^^^^^^^^^^ A @ 2:2-3:3 - 3 baz; | 3 baz; - ^^^ A | ^^^ A - | 4 } - 4 } | - 5 | - " + [ + "2:2-3:3 <- 2:2-3:3", + ] `) expect(css).toMatchInlineSnapshot(` @@ -211,13 +161,9 @@ test('at rules, bodyless, single line', async () => { assert(ast[0].kind === 'at-rule') expect(format(ast[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 @layer foo, bar; | 1 @layer foo, bar; - ^^^^^^^^^^^^^^^ A @ 1:0-15 | ^^^^^^^^^^^^^^^^^^^ A @ 1:0-19 - 2 | - " + [ + "1:0-1:15 <- 1:0-1:19", + ] `) expect(css).toMatchInlineSnapshot(` @@ -236,17 +182,9 @@ test('at rules, bodyless, multi line', async () => { assert(ast[0].kind === 'at-rule') expect(format(ast[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 @layer foo, bar; | 1 @layer - ^^^^^^^^^^^^^^^ A @ 1:0-15 | ^^^^^^ A @ 1:0-4:0 - | 2 foo, - | ^^^^^^ A - | 3 bar - | ^^^^^ A - 2 | - " + [ + "1:0-1:15 <- 1:0-4:0", + ] `) expect(css).toMatchInlineSnapshot(` @@ -260,15 +198,9 @@ test('at rules, body, single line', async () => { assert(ast[0].kind === 'at-rule') expect(format(ast[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 @layer foo { | 1 @layer foo { color: red; } - ^^^^^^^^^^^ A @ 1:0-11 | ^^^^^^^^^^^ A @ 1:0-11 - 2 color: red; | - 3 } | - 4 | - " + [ + "1:0-1:11 <- 1:0-1:11", + ] `) expect(css).toMatchInlineSnapshot(` @@ -290,17 +222,9 @@ test('at rules, body, multi line', async () => { assert(ast[0].kind === 'at-rule') expect(format(ast[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 @layer foo { | 1 @layer - ^^^^^^^^^^^ A @ 1:0-11 | ^^^^^^ A @ 1:0-3:0 - | 2 foo - | ^^^^^ A - 2 color: baz; | - 3 } | - 4 | - " + [ + "1:0-1:11 <- 1:0-3:0", + ] `) expect(css).toMatchInlineSnapshot(` @@ -316,15 +240,9 @@ test('style rules, body, single line', async () => { assert(ast[0].kind === 'rule') expect(format(ast[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .foo:is(.bar) { | 1 .foo:is(.bar) { color: red; } - ^^^^^^^^^^^^^^ A @ 1:0-14 | ^^^^^^^^^^^^^^ A @ 1:0-14 - 2 color: red; | - 3 } | - 4 | - " + [ + "1:0-1:14 <- 1:0-1:14", + ] `) expect(css).toMatchInlineSnapshot(` @@ -347,19 +265,9 @@ test('style rules, body, multi line', async () => { assert(ast[0].kind === 'rule') expect(format(ast[0])).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .foo:is( .bar ) { | 1 .foo:is( - ^^^^^^^^^^^^^^^^ A @ 1:0-16 | ^^^^^^^^ A @ 1:0-3:2 - | 2 .bar - | ^^^^^^ A - | 3 ) { - | ^^ A - 2 color: red; | - 3 } | - 4 | - " + [ + "1:0-1:16 <- 1:0-3:2", + ] `) expect(css).toMatchInlineSnapshot(` diff --git a/packages/tailwindcss/src/source-maps/visualize-source-map.test.ts b/packages/tailwindcss/src/source-maps/visualize-source-map.test.ts deleted file mode 100644 index f69bfac31..000000000 --- a/packages/tailwindcss/src/source-maps/visualize-source-map.test.ts +++ /dev/null @@ -1,318 +0,0 @@ -import dedent from 'dedent' -import { SourceMapGenerator, type RawSourceMap } from 'source-map-js' -import { expect, test } from 'vitest' -import { visualizeSourceMap, visualizeSourceMapRanges } from './visualize-source-map' - -const css = dedent - -function sourceMap({ - sources, - mappings, -}: { - sources: Record - mappings: { - source: string - original: [line: number, column: number] - generated: [line: number, column: number] - }[] -}) { - let generator = new SourceMapGenerator() - - for (let mapping of mappings) { - generator.addMapping({ - source: mapping.source, - original: { - line: mapping.original[0], - column: mapping.original[1], - }, - generated: { - line: mapping.generated[0], - column: mapping.generated[1], - }, - }) - } - - for (let [source, content] of Object.entries(sources)) { - generator.setSourceContent(source, content) - } - - return JSON.parse(generator.toString()) as RawSourceMap -} - -test('visualizes single-line ranges', () => { - expect( - visualizeSourceMapRanges( - { - // prettier-ignore - 'input.css': css`.foo { color: red; }`, - }, - css` - .foo { - color: red; - } - `, - [ - { - original: { - source: 'input.css', - start: [1, 7], - end: [1, 17], - }, - generated: { - start: [2, 2], - end: [2, 12], - }, - }, - ], - ), - ).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 .foo { | - 2 color: red; | 1 .foo { color: red; } - ^^^^^^^^^^ A @ 2:2-12 | ^^^^^^^^^^ A @ 1:7-17 - 3 } | - " - `) -}) - -test('visualizes multi-line ranges', () => { - expect( - visualizeSourceMapRanges( - { - 'input.css': css` - /*! foo - bar */ - `, - }, - css` - /*! foo - bar */ - `, - [ - { - original: { - source: 'input.css', - start: [1, 0], - end: [2, 7], - }, - generated: { - start: [1, 0], - end: [2, 7], - }, - }, - ], - ), - ).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 /*! foo | 1 /*! foo - ^^^^^^^ A @ 1:0-2:7 | ^^^^^^^ A @ 1:0-2:7 - 2 bar */ | 2 bar */ - ^^^^^^^ A | ^^^^^^^ A - " - `) -}) - -test('visualizes multiple sources', () => { - expect( - visualizeSourceMapRanges( - { - 'utilities.css': css` - @tailwind utilities; - `, - 'input.css': css` - @utility custom { - color: red; - } - `, - }, - css` - .flex { - display: flex; - } - .custom { - color: red; - } - `, - [ - { - original: { - source: 'utilities.css', - start: [1, 0], - end: [1, 19], - }, - generated: { - start: [1, 0], - end: [1, 6], - }, - }, - { - original: { - source: 'utilities.css', - start: [1, 0], - end: [1, 19], - }, - generated: { - start: [2, 2], - end: [2, 15], - }, - }, - { - original: { - source: 'input.css', - start: [2, 2], - end: [2, 12], - }, - generated: { - start: [5, 2], - end: [5, 12], - }, - }, - ], - ), - ).toMatchInlineSnapshot(` - " - output.css | original - | - | --- utilities.css --- - 1 .flex { | 1 @tailwind utilities; - ^^^^^^ A @ 1:0-6 | ^^^^^^^^^^^^^^^^^^^ A @ 1:0-19 - 2 display: flex; | - ^^^^^^^^^^^^^ A @ 2:2-15 | - 3 } | - 4 .custom { | - | --- input.css --- - 5 color: red; | 2 color: red; - ^^^^^^^^^^ B @ 5:2-12 | ^^^^^^^^^^ B @ 2:2-12 - | 3 } - 6 } | - " - `) -}) - -test('visualizes unmapped source ranges', () => { - expect( - visualizeSourceMapRanges( - { - // prettier-ignore - 'input.css': css`.foo { color: red; }`, - }, - css` - .foo { - color: red; - } - `, - [ - { - original: { - source: 'input.css', - start: [1, 7], - end: [1, 17], - }, - generated: null, - }, - ], - ), - ).toMatchInlineSnapshot(` - " - output.css | input.css - | - unmapped A | 1 .foo { color: red; } - | ^^^^^^^^^^ A @ 1:7-17 - 1 .foo { | - 2 color... | - 3 } | - " - `) -}) - -test('visualizes source-map points as generated segments', () => { - let generated = css` - /*! foo - bar */ - ` - - expect( - visualizeSourceMap( - sourceMap({ - sources: { - 'input.css': css` - /*! foo - bar */ - `, - }, - mappings: [ - { source: 'input.css', original: [1, 0], generated: [1, 0] }, - { source: 'input.css', original: [2, 0], generated: [2, 0] }, - { source: 'input.css', original: [2, 7], generated: [2, 7] }, - ], - }), - generated, - ), - ).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 /*! foo | 1 /*! foo - ^^^^^^^ A @ 1:0-2:0 | ^^^^^^^ A @ 1:0-2:0 - 2 bar */ | 2 bar */ - ^^^^^^^ B @ 2:0-7 | ^^^^^^^ B @ 2:0-7 - " - `) -}) - -test('visualizes final source-map point through end of line', () => { - expect( - visualizeSourceMap( - sourceMap({ - sources: { - 'input.css': css`aaaa`, - }, - mappings: [{ source: 'input.css', original: [1, 0], generated: [1, 0] }], - }), - css`aaaa`, - ), - ).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 aaaa | 1 aaaa - ^^^^ A @ 1:0-2:0 | ^ A @ 1:0 - " - `) -}) - -test('visualizes duplicate source-map points without hiding them', () => { - expect( - visualizeSourceMap( - sourceMap({ - sources: { - 'input.css': css` - aaaa - bbbb - `, - }, - mappings: [ - { source: 'input.css', original: [1, 0], generated: [1, 0] }, - { source: 'input.css', original: [2, 0], generated: [1, 0] }, - { source: 'input.css', original: [2, 4], generated: [1, 4] }, - ], - }), - css` - bbbb - `, - ), - ).toMatchInlineSnapshot(` - " - output.css | input.css - | - 1 bbbb | 1 aaaa - ^ A @ 1:0 | ^ A @ 1:0 - 1 bbbb | 2 bbbb - ^^^^ B @ 1:0-4 | ^^^^ B @ 2:0-4 - " - `) -}) diff --git a/packages/tailwindcss/src/source-maps/visualize-source-map.ts b/packages/tailwindcss/src/source-maps/visualize-source-map.ts deleted file mode 100644 index d0da423be..000000000 --- a/packages/tailwindcss/src/source-maps/visualize-source-map.ts +++ /dev/null @@ -1,538 +0,0 @@ -import { SourceMapConsumer, type RawSourceMap } from 'source-map-js' - -import { DefaultMap } from '../utils/default-map' - -const COLUMN_WIDTH = 100 -const CONTEXT_LINES = 3 - -type Range = { start: [number, number]; end: [number, number] } - -interface Annotation { - original: Range - generated: Range | null - source: string -} - -interface MappedAnnotation extends Annotation { - generated: Range -} - -interface SourceMapPoint { - original: [number, number] - generated: [number, number] - source: string -} - -interface SourceVisualization { - before: string[] - line: string - marker: string - after: string[] -} - -export interface SourceMapVisualizationRange { - original: Range & { source: string } - generated: Range | null -} - -/** - * Render source map mappings as a side-by-side view of generated and original code. - * - * For raw source maps this renders the generated segments implied by mapping points, - * so duplicate points and unmapped generated lines are visible in snapshots. - */ -export function visualizeSourceMap(map: RawSourceMap, css: string) { - let smc = new SourceMapConsumer(map) - let sourceContents = buildSourceContents(map.sources, map.sourcesContent ?? []) - let points: SourceMapPoint[] = [] - - smc.eachMapping((mapping) => { - if ( - mapping.source === null || - mapping.originalLine === null || - mapping.originalColumn === null - ) { - return - } - - points.push({ - original: [mapping.originalLine, mapping.originalColumn], - generated: [mapping.generatedLine, mapping.generatedColumn], - source: mapping.source, - }) - }) - - return renderVisualization(sourceContents, buildAnnotationsFromSourceMapPoints(points), css) -} - -export function visualizeSourceMapRanges( - sources: Record, - generated: string, - ranges: SourceMapVisualizationRange[], -) { - let sourceContents = buildSourceContents(Object.keys(sources), Object.values(sources)) - let annotations: Annotation[] = [] - - for (let range of ranges) { - annotations.push({ - generated: range.generated, - original: range.original, - source: range.original.source, - }) - } - - return renderVisualization(sourceContents, annotations, generated) -} - -function buildSourceContents(sources: string[], contents: (string | null)[]) { - let result = new Map() - - for (let i = 0; i < sources.length; i++) { - let content = contents[i] - - if (content === undefined || content === null) continue - - result.set(sources[i], content.split('\n')) - } - - return result -} - -function buildAnnotationsFromSourceMapPoints(points: SourceMapPoint[]) { - let annotations: Annotation[] = [] - let current: MappedAnnotation | null = null - - function finish(next: SourceMapPoint | null) { - if (current === null) return - - if (samePosition(current.generated.start, current.generated.end)) { - if (next === null) { - current.generated.end = [current.generated.start[0] + 1, 0] - } else if (next.generated[0] > current.generated.start[0]) { - current.generated.end = [current.generated.start[0] + 1, 0] - - if ( - next.source === current.source && - next.generated[0] === current.generated.start[0] + 1 && - next.generated[1] === 0 && - next.original[0] === current.original.start[0] + 1 && - next.original[1] === 0 - ) { - current.original.end = next.original - } - } - } - - annotations.push(current) - current = null - } - - for (let point of points) { - if ( - current !== null && - current.source === point.source && - current.generated.start[0] === point.generated[0] && - point.generated[1] > current.generated.end[1] && - comparePosition(current.original.start, point.original) <= 0 - ) { - current.generated.end = point.generated - current.original.end = point.original - continue - } - - finish(point) - - current = { - original: { - start: point.original, - end: point.original, - }, - generated: { - start: point.generated, - end: point.generated, - }, - source: point.source, - } - } - - finish(null) - - return annotations -} - -function renderVisualization( - sourceContents: Map, - annotationsList: Annotation[], - generated: string, -) { - let generatedLines = generated.split('\n') - let nextLabelIdx = 0 - let labelsBySource = new DefaultMap(() => annotationLabel(nextLabelIdx++)) - let labels = annotationsList.map((annotation) => { - return labelsBySource.get(sourceKey(annotation.source, annotation.original)) - }) - let generatedLineWidth = String(generatedLines.length).length - let sourceLineWidth = Math.max( - 1, - ...Array.from(sourceContents.values(), (lines) => String(lines.length).length), - ) - let leftWidth = Math.min( - 100, - Math.max( - withLineNumber(generatedLineWidth, 'output.css').length, - ...annotationsList.map((annotation, idx) => { - let label = labels[idx] - - if (annotation.generated === null) { - return withoutLineNumber(generatedLineWidth, `unmapped ${label}`).length - } - - let line = withLineNumber( - generatedLineWidth, - generatedLines[annotation.generated.start[0] - 1] ?? '', - annotation.generated.start[0], - ) - let marker = withoutLineNumber( - generatedLineWidth, - visualizeRange( - annotation.generated, - label, - generatedLines[annotation.generated.start[0] - 1] ?? '', - ), - ) - let after = visualizeGeneratedRangeContinuation( - generatedLines, - generatedLineWidth, - annotation.generated, - label, - ) - - return Math.max(line.length, marker.length, ...after.map((line) => line.length)) - }), - ), - ) - let sources = new Set(annotationsList.map((annotation) => annotation.source)) - let sourceHeader = sources.size === 1 ? (annotationsList[0]?.source ?? 'original') : 'original' - let result: string[] = [] - - function add(left: string, right: string) { - result.push(sideBySide(left, right, leftWidth)) - } - - add( - withLineNumber(generatedLineWidth, 'output.css'), - withLineNumber(sourceLineWidth, sourceHeader), - ) - add(withoutLineNumber(generatedLineWidth, ''), withoutLineNumber(sourceLineWidth, '')) - - let lastLine = 0 - let lastSourceLine = new Map() - let lastSource: string | null = null - let renderedSources = new Set() - - for (let idx = 0; idx < annotationsList.length; idx++) { - let annotation = annotationsList[idx] - let label = labels[idx] - - if (annotation.generated === null) { - let source = visualizeSource( - sourceContents, - lastSourceLine, - renderedSources, - sourceLineWidth, - annotation.source, - annotation.original, - label, - ) - - if (sources.size > 1 && source.line !== '' && annotation.source !== lastSource) { - add( - withoutLineNumber(generatedLineWidth, ''), - withoutLineNumber(sourceLineWidth, `--- ${annotation.source} ---`), - ) - lastSource = annotation.source - } - - for (let line of source.before) { - add(withoutLineNumber(generatedLineWidth, ''), line) - } - - add(withoutLineNumber(generatedLineWidth, `unmapped ${label}`), source.line) - add(withoutLineNumber(generatedLineWidth, ''), source.marker) - - for (let line of source.after) { - add(withoutLineNumber(generatedLineWidth, ''), line) - } - - continue - } - - let lineNumber = annotation.generated.start[0] - - for (let i = lastLine + 1; i < lineNumber; i++) { - add( - withLineNumber(generatedLineWidth, generatedLines[i - 1] ?? '', i), - withoutLineNumber(sourceLineWidth, ''), - ) - } - - let endLine = coveredEndLine(annotation.generated) - let generatedAfter = visualizeGeneratedRangeContinuation( - generatedLines, - generatedLineWidth, - annotation.generated, - label, - ) - let source = visualizeSource( - sourceContents, - lastSourceLine, - renderedSources, - sourceLineWidth, - annotation.source, - annotation.original, - label, - ) - - if (sources.size > 1 && source.line !== '' && annotation.source !== lastSource) { - add( - withoutLineNumber(generatedLineWidth, ''), - withoutLineNumber(sourceLineWidth, `--- ${annotation.source} ---`), - ) - lastSource = annotation.source - } - - for (let line of source.before) { - add(withoutLineNumber(generatedLineWidth, ''), line) - } - - add( - withLineNumber(generatedLineWidth, generatedLines[lineNumber - 1] ?? '', lineNumber), - source.line, - ) - add( - withoutLineNumber( - generatedLineWidth, - visualizeRange(annotation.generated, label, generatedLines[lineNumber - 1] ?? ''), - ), - source.marker, - ) - - for (let i = 0; i < Math.max(generatedAfter.length, source.after.length); i++) { - add(generatedAfter[i] ?? withoutLineNumber(generatedLineWidth, ''), source.after[i] ?? '') - } - - lastLine = endLine - } - - for (let i = lastLine + 1; i <= generatedLines.length; i++) { - add( - withLineNumber(generatedLineWidth, generatedLines[i - 1] ?? '', i), - withoutLineNumber(sourceLineWidth, ''), - ) - } - - return `\n${result.join('\n')}\n` -} - -function sideBySide(left: string, right: string, leftWidth: number) { - return `${fitColumn(left, leftWidth)} | ${fitColumn(right, COLUMN_WIDTH, false)}` -} - -function withLineNumber(width: number, value: string, line?: number) { - return `${line === undefined ? ' '.repeat(width) : String(line).padStart(width)} ${value}` -} - -function withoutLineNumber(width: number, value: string) { - if (value === '') return '' - - return `${' '.repeat(width)} ${value}` -} - -function fitColumn(value: string, width: number, pad = true) { - if (value.length <= width) { - return pad ? value.padEnd(width) : value - } - - let markerIdx = value.indexOf(' @ ') - - if (markerIdx !== -1) { - let labelIdx = value.lastIndexOf(' ', markerIdx - 1) + 1 - let suffix = value.slice(labelIdx) - let prefixWidth = width - suffix.length - 4 - - if (prefixWidth > 0) { - let result = `${value.slice(0, prefixWidth)}... ${suffix}` - - return pad ? result.padEnd(width) : result - } - } - - return `${value.slice(0, width - 3)}...` -} - -function annotationLabel(idx: number) { - let label = '' - - for (let current = idx; current >= 0; current = Math.floor(current / 26) - 1) { - label = String.fromCharCode(65 + (current % 26)) + label - } - - return label -} - -function visualizeRange(range: Range, label: string, line?: string) { - let start = range.start[1] - let width = (() => { - if (line !== undefined && range.start[0] !== range.end[0]) { - return Math.max(1, line.length - start) - } - - return Math.max(1, range.end[1] - start) - })() - return `${' '.repeat(start)}${'^'.repeat(width)} ${label} @ ${formatRange(range)}` -} - -function visualizeSource( - sourceContents: Map, - lastSourceLine: Map, - renderedSources: Set, - sourceLineWidth: number, - source: string, - range: Range, - label: string, -): SourceVisualization { - let lines = sourceContents.get(source) - - if (lines === undefined) return { before: [], line: '', marker: '', after: [] } - - let startLine = range.start[0] - let line = lines[startLine - 1] - - if (line === undefined) return { before: [], line: '', marker: '', after: [] } - - let before: string[] = [] - let lastLine = lastSourceLine.get(source) ?? 0 - - if (lastLine > 0 && startLine > lastLine && startLine - lastLine <= CONTEXT_LINES + 1) { - for (let i = lastLine + 1; i < startLine; i++) { - let line = lines[i - 1] ?? '' - - if (line.trim() === '') continue - - before.push(withLineNumber(sourceLineWidth, line, i)) - } - } - - let key = sourceKey(source, range) - - if (renderedSources.has(key)) { - lastSourceLine.set(source, Math.max(lastLine, startLine)) - - return { before: [], line: '', marker: '', after: [] } - } - - renderedSources.add(key) - - let endLine = coveredEndLine(range) - let after = visualizeSourceRangeContinuation(lines, sourceLineWidth, range, label) - - if (startLine > lastLine) { - for (let i = endLine + 1; i <= Math.min(endLine + CONTEXT_LINES, lines.length); i++) { - let line = lines[i - 1] - - if (line?.trim() !== '}') break - - after.push(withLineNumber(sourceLineWidth, line, i)) - endLine = i - } - } - - lastSourceLine.set(source, Math.max(lastLine, endLine)) - - return { - before, - line: withLineNumber(sourceLineWidth, line, startLine), - marker: withoutLineNumber(sourceLineWidth, visualizeRange(range, label, line)), - after, - } -} - -function visualizeSourceRangeContinuation( - lines: string[], - sourceLineWidth: number, - range: Range, - label: string, -) { - let result: string[] = [] - - for (let i = range.start[0] + 1; i <= coveredEndLine(range); i++) { - let line = lines[i - 1] ?? '' - - result.push(withLineNumber(sourceLineWidth, line, i)) - result.push( - withoutLineNumber(sourceLineWidth, visualizeContinuationRange(range, label, line, i)), - ) - } - - return result -} - -function visualizeGeneratedRangeContinuation( - lines: string[], - generatedLineWidth: number, - range: Range, - label: string, -) { - let result: string[] = [] - - for (let i = range.start[0] + 1; i <= coveredEndLine(range); i++) { - let line = lines[i - 1] ?? '' - - result.push(withLineNumber(generatedLineWidth, line, i)) - result.push( - withoutLineNumber(generatedLineWidth, visualizeContinuationRange(range, label, line, i)), - ) - } - - return result -} - -function coveredEndLine(range: Range) { - if (range.start[0] !== range.end[0] && range.end[1] === 0) { - return range.end[0] - 1 - } - - return range.end[0] -} - -function visualizeContinuationRange(range: Range, label: string, line: string, lineNumber: number) { - let start = lineNumber === range.start[0] ? range.start[1] : 0 - let end = lineNumber === range.end[0] ? range.end[1] : line.length - let width = Math.max(1, end - start) - - return `${' '.repeat(start)}${'^'.repeat(width)} ${label}` -} - -function sourceKey(source: string, range: Range) { - return `${source}:${range.start[0]}:${range.start[1]}:${range.end[0]}:${range.end[1]}` -} - -function samePosition(a: [number, number], b: [number, number]) { - return a[0] === b[0] && a[1] === b[1] -} - -function comparePosition(a: [number, number], b: [number, number]) { - return a[0] - b[0] || a[1] - b[1] -} - -function formatRange(range: Range): string { - if (range.start[0] === range.end[0]) { - if (range.start[1] === range.end[1]) { - return `${range.start[0]}:${range.start[1]}` - } - - return `${range.start[0]}:${range.start[1]}-${range.end[1]}` - } - - return `${range.start[0]}:${range.start[1]}-${range.end[0]}:${range.end[1]}` -} diff --git a/packages/tailwindcss/src/test-utils/run.ts b/packages/tailwindcss/src/test-utils/run.ts index 7667ca66f..5b4b1ca99 100644 --- a/packages/tailwindcss/src/test-utils/run.ts +++ b/packages/tailwindcss/src/test-utils/run.ts @@ -1,25 +1,20 @@ import { compile } from '..' import { optimize } from '../../../@tailwindcss-node/src/optimize' -const css = String.raw - -export async function run( - candidates: string[], - input = css` - @tailwind utilities; - `, +export async function compileCss( + css: string, + candidates: string[] = [], options: Parameters[1] = {}, ) { - let { build } = await compile(input, options) - return pretty(optimize(build(candidates)).code) + let { build } = await compile(css, options) + return optimize(build(candidates)).code.trim() } -export async function compileCss(css: string, options: Parameters[1] = {}) { - return run([], css, options) +export async function run(candidates: string[]) { + let { build } = await compile('@tailwind utilities;') + return optimize(build(candidates)).code.trim() } -export function pretty(input: string) { - input = input.trim() - if (input === '') return '' - return `\n${input}\n` +export function optimizeCss(input: string) { + return optimize(input).code } diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 66b051d57..233d3fa23 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -1,14 +1,12 @@ import { describe, expect, test, vi } from 'vitest' import { compile } from '.' -import { compileCss, run } from './test-utils/run' -import { isValidFunctionalUtilityName, isValidStaticUtilityName } from './utilities' +import { compileCss, optimizeCss, run } from './test-utils/run' const css = String.raw test('sr-only', async () => { expect(await run(['sr-only'])).toMatchInlineSnapshot(` - " - .sr-only { + ".sr-only { clip-path: inset(50%); white-space: nowrap; border-width: 0; @@ -18,16 +16,14 @@ test('sr-only', async () => { padding: 0; position: absolute; overflow: hidden; - } - " + }" `) expect(await run(['-sr-only', 'sr-only-[var(--value)]', 'sr-only/foo'])).toEqual('') }) test('not-sr-only', async () => { expect(await run(['not-sr-only'])).toMatchInlineSnapshot(` - " - .not-sr-only { + ".not-sr-only { clip-path: none; white-space: normal; width: auto; @@ -36,23 +32,20 @@ test('not-sr-only', async () => { padding: 0; position: static; overflow: visible; - } - " + }" `) expect(await run(['-not-sr-only', 'not-sr-only-[var(--value)]', 'not-sr-only/foo'])).toEqual('') }) test('pointer-events', async () => { expect(await run(['pointer-events-none', 'pointer-events-auto'])).toMatchInlineSnapshot(` - " - .pointer-events-auto { + ".pointer-events-auto { pointer-events: auto; } .pointer-events-none { pointer-events: none; - } - " + }" `) expect( await run([ @@ -66,8 +59,7 @@ test('pointer-events', async () => { test('visibility', async () => { expect(await run(['visible', 'invisible', 'collapse'])).toMatchInlineSnapshot(` - " - .collapse { + ".collapse { visibility: collapse; } @@ -77,8 +69,7 @@ test('visibility', async () => { .visible { visibility: visible; - } - " + }" `) expect( await run([ @@ -94,8 +85,7 @@ test('visibility', async () => { test('position', async () => { expect(await run(['static', 'fixed', 'absolute', 'relative', 'sticky'])).toMatchInlineSnapshot(` - " - .absolute { + ".absolute { position: absolute; } @@ -113,8 +103,7 @@ test('position', async () => { .sticky { position: sticky; - } - " + }" `) expect( await run([ @@ -134,7 +123,15 @@ test('position', async () => { test('inset', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); + --inset-shadowned: 1940px; + } + @tailwind utilities; + `, [ 'inset-auto', 'inset-shadow-sm', @@ -146,18 +143,9 @@ test('inset', async () => { '-inset-4', 'inset-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-shadow: 0 0 #0000; @@ -297,8 +285,7 @@ test('inset', async () => { syntax: "*"; inherits: false; initial-value: 0 0 #0000; - } - " + }" `) expect( await run([ @@ -320,7 +307,14 @@ test('inset', async () => { test('inset-x', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + --inset-shadowned: 1940px; + } + @tailwind utilities; + `, [ 'inset-x-shadowned', 'inset-x-auto', @@ -331,17 +325,9 @@ test('inset-x', async () => { '-inset-x-4', 'inset-x-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --inset-shadowned: 1940px; } @@ -376,11 +362,17 @@ test('inset-x', async () => { .inset-x-shadowned { inset-inline: var(--inset-shadowned); - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme reference { + --spacing-4: 1rem; + --inset-shadow-sm: inset 0 1px 1px #0000000d; + } + @tailwind utilities; + `, [ 'inset-x-shadow-sm', 'inset-x', @@ -396,20 +388,20 @@ test('inset-x', async () => { '-inset-x-4/foo', 'inset-x-[4px]/foo', ], - css` - @theme reference { - --spacing-4: 1rem; - --inset-shadow-sm: inset 0 1px 1px #0000000d; - } - @tailwind utilities; - `, ), ).toEqual('') }) test('inset-y', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + --inset-shadowned: 1940px; + } + @tailwind utilities; + `, [ 'inset-y-shadowned', 'inset-y-auto', @@ -420,17 +412,9 @@ test('inset-y', async () => { '-inset-y-4', 'inset-y-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --inset-shadowned: 1940px; } @@ -465,11 +449,17 @@ test('inset-y', async () => { .inset-y-shadowned { inset-block: var(--inset-shadowned); - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme reference { + --spacing-4: 1rem; + --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); + } + @tailwind utilities; + `, [ 'inset-y-shadow-sm', 'inset-y', @@ -485,30 +475,13 @@ test('inset-y', async () => { '-inset-y-4/foo', 'inset-y-[4px]/foo', ], - css` - @theme reference { - --spacing-4: 1rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, ), ).toEqual('') }) -test('inset-s', async () => { +test('start', async () => { expect( - await run( - [ - 'inset-s-shadowned', - 'inset-s-auto', - '-inset-s-full', - 'inset-s-full', - 'inset-s-3/4', - 'inset-s-4', - '-inset-s-4', - 'inset-s-[4px]', - ], + await compileCss( css` @theme { --spacing-4: 1rem; @@ -516,64 +489,57 @@ test('inset-s', async () => { } @tailwind utilities; `, + [ + 'start-shadowned', + 'start-auto', + '-start-full', + 'start-full', + 'start-3/4', + 'start-4', + '-start-4', + 'start-[4px]', + ], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --inset-shadowned: 1940px; } - .-inset-s-4 { + .-start-4 { inset-inline-start: calc(var(--spacing-4) * -1); } - .-inset-s-full { + .-start-full { inset-inline-start: -100%; } - .inset-s-3\\/4 { + .start-3\\/4 { inset-inline-start: 75%; } - .inset-s-4 { + .start-4 { inset-inline-start: var(--spacing-4); } - .inset-s-\\[4px\\] { + .start-\\[4px\\] { inset-inline-start: 4px; } - .inset-s-auto { + .start-auto { inset-inline-start: auto; } - .inset-s-full { + .start-full { inset-inline-start: 100%; } - .inset-s-shadowned { + .start-shadowned { inset-inline-start: var(--inset-shadowned); - } - " + }" `) expect( - await run( - [ - 'inset-s-shadow-sm', - 'inset-s', - 'inset-s--1', - 'inset-s--1/2', - 'inset-s--1/-2', - 'inset-s-1/-2', - 'inset-s-auto/foo', - '-inset-s-full/foo', - 'inset-s-full/foo', - 'inset-s-3/4/foo', - 'inset-s-4/foo', - '-inset-s-4/foo', - 'inset-s-[4px]/foo', - ], + await compileCss( css` @theme reference { --spacing-4: 1rem; @@ -581,23 +547,28 @@ test('inset-s', async () => { } @tailwind utilities; `, + [ + 'start-shadow-sm', + 'start', + 'start--1', + 'start--1/2', + 'start--1/-2', + 'start-1/-2', + 'start-auto/foo', + '-start-full/foo', + 'start-full/foo', + 'start-3/4/foo', + 'start-4/foo', + '-start-4/foo', + 'start-[4px]/foo', + ], ), ).toEqual('') }) -test('inset-e', async () => { +test('end', async () => { expect( - await run( - [ - 'inset-e-shadowned', - 'inset-e-auto', - '-inset-e-full', - 'inset-e-full', - 'inset-e-3/4', - 'inset-e-4', - '-inset-e-4', - 'inset-e-[4px]', - ], + await compileCss( css` @theme { --spacing-4: 1rem; @@ -605,64 +576,57 @@ test('inset-e', async () => { } @tailwind utilities; `, + [ + 'end-shadowned', + 'end-auto', + '-end-full', + 'end-full', + 'end-3/4', + 'end-4', + '-end-4', + 'end-[4px]', + ], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --inset-shadowned: 1940px; } - .-inset-e-4 { + .-end-4 { inset-inline-end: calc(var(--spacing-4) * -1); } - .-inset-e-full { + .-end-full { inset-inline-end: -100%; } - .inset-e-3\\/4 { + .end-3\\/4 { inset-inline-end: 75%; } - .inset-e-4 { + .end-4 { inset-inline-end: var(--spacing-4); } - .inset-e-\\[4px\\] { + .end-\\[4px\\] { inset-inline-end: 4px; } - .inset-e-auto { + .end-auto { inset-inline-end: auto; } - .inset-e-full { + .end-full { inset-inline-end: 100%; } - .inset-e-shadowned { + .end-shadowned { inset-inline-end: var(--inset-shadowned); - } - " + }" `) expect( - await run( - [ - 'inset-e-shadow-sm', - 'inset-e', - 'inset-e--1', - 'inset-e--1/2', - 'inset-e--1/-2', - 'inset-e-1/-2', - 'inset-e-auto/foo', - '-inset-e-full/foo', - 'inset-e-full/foo', - 'inset-e-3/4/foo', - 'inset-e-4/foo', - '-inset-e-4/foo', - 'inset-e-[4px]/foo', - ], + await compileCss( css` @theme reference { --spacing-4: 1rem; @@ -670,191 +634,36 @@ test('inset-e', async () => { } @tailwind utilities; `, - ), - ).toEqual('') -}) - -test('inset-bs', async () => { - expect( - await run( [ - 'inset-bs-shadowned', - 'inset-bs-auto', - '-inset-bs-full', - 'inset-bs-full', - 'inset-bs-3/4', - 'inset-bs-4', - '-inset-bs-4', - 'inset-bs-[4px]', + 'end-shadow-sm', + 'end', + 'end--1', + 'end--1/2', + 'end--1/-2', + 'end-1/-2', + 'end-auto/foo', + '-end-full/foo', + 'end-full/foo', + 'end-3/4/foo', + 'end-4/foo', + '-end-4/foo', + 'end-[4px]/foo', ], - css` - @theme { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - - .-inset-bs-4 { - inset-block-start: calc(var(--spacing-4) * -1); - } - - .-inset-bs-full { - inset-block-start: -100%; - } - - .inset-bs-3\\/4 { - inset-block-start: 75%; - } - - .inset-bs-4 { - inset-block-start: var(--spacing-4); - } - - .inset-bs-\\[4px\\] { - inset-block-start: 4px; - } - - .inset-bs-auto { - inset-block-start: auto; - } - - .inset-bs-full { - inset-block-start: 100%; - } - - .inset-bs-shadowned { - inset-block-start: var(--inset-shadowned); - } - " - `) - expect( - await run( - [ - 'inset-bs-shadow-sm', - 'inset-bs', - 'inset-bs--1', - 'inset-bs--1/2', - 'inset-bs--1/-2', - 'inset-bs-1/-2', - 'inset-bs-auto/foo', - '-inset-bs-full/foo', - 'inset-bs-full/foo', - 'inset-bs-3/4/foo', - 'inset-bs-4/foo', - '-inset-bs-4/foo', - 'inset-bs-[4px]/foo', - ], - css` - @theme reference { - --spacing-4: 1rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, - ), - ).toEqual('') -}) - -test('inset-be', async () => { - expect( - await run( - [ - 'inset-be-shadowned', - 'inset-be-auto', - '-inset-be-full', - 'inset-be-full', - 'inset-be-3/4', - 'inset-be-4', - '-inset-be-4', - 'inset-be-[4px]', - ], - css` - @theme { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - - .-inset-be-4 { - inset-block-end: calc(var(--spacing-4) * -1); - } - - .-inset-be-full { - inset-block-end: -100%; - } - - .inset-be-3\\/4 { - inset-block-end: 75%; - } - - .inset-be-4 { - inset-block-end: var(--spacing-4); - } - - .inset-be-\\[4px\\] { - inset-block-end: 4px; - } - - .inset-be-auto { - inset-block-end: auto; - } - - .inset-be-full { - inset-block-end: 100%; - } - - .inset-be-shadowned { - inset-block-end: var(--inset-shadowned); - } - " - `) - expect( - await run( - [ - 'inset-be-shadow-sm', - 'inset-be', - 'inset-be--1', - 'inset-be--1/2', - 'inset-be--1/-2', - 'inset-be-1/-2', - 'inset-be-auto/foo', - '-inset-be-full/foo', - 'inset-be-full/foo', - 'inset-be-3/4/foo', - 'inset-be-4/foo', - '-inset-be-4/foo', - 'inset-be-[4px]/foo', - ], - css` - @theme reference { - --spacing-4: 1rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, ), ).toEqual('') }) test('top', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + --inset-shadowned: 1940px; + } + @tailwind utilities; + `, + [ 'top-shadowned', 'top-auto', @@ -865,17 +674,9 @@ test('top', async () => { '-top-4', 'top-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --inset-shadowned: 1940px; } @@ -910,11 +711,17 @@ test('top', async () => { .top-shadowned { top: var(--inset-shadowned); - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme reference { + --spacing-4: 1rem; + --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); + } + @tailwind utilities; + `, [ 'top-shadow-sm', 'top', @@ -930,20 +737,20 @@ test('top', async () => { '-top-4/foo', 'top-[4px]/foo', ], - css` - @theme reference { - --spacing-4: 1rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, ), ).toEqual('') }) test('right', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + --inset-shadowned: 1940px; + } + @tailwind utilities; + `, [ 'right-shadowned', 'right-auto', @@ -954,17 +761,9 @@ test('right', async () => { '-right-4', 'right-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --inset-shadowned: 1940px; } @@ -999,11 +798,17 @@ test('right', async () => { .right-shadowned { right: var(--inset-shadowned); - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme reference { + --spacing-4: 1rem; + --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); + } + @tailwind utilities; + `, [ 'right-shadow-sm', 'right', @@ -1019,20 +824,20 @@ test('right', async () => { '-right-4/foo', 'right-[4px]/foo', ], - css` - @theme reference { - --spacing-4: 1rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, ), ).toEqual('') }) test('bottom', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + --inset-shadowned: 1940px; + } + @tailwind utilities; + `, [ 'bottom-shadowned', 'bottom-auto', @@ -1043,17 +848,9 @@ test('bottom', async () => { '-bottom-4', 'bottom-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --inset-shadowned: 1940px; } @@ -1088,11 +885,17 @@ test('bottom', async () => { .bottom-shadowned { bottom: var(--inset-shadowned); - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme reference { + --spacing-4: 1rem; + --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); + } + @tailwind utilities; + `, [ 'bottom-shadow-sm', 'bottom', @@ -1108,20 +911,20 @@ test('bottom', async () => { '-bottom-4/foo', 'bottom-[4px]/foo', ], - css` - @theme reference { - --spacing-4: 1rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, ), ).toEqual('') }) test('left', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + --inset-shadowned: 1940px; + } + @tailwind utilities; + `, [ 'left-shadowned', 'left-auto', @@ -1131,21 +934,10 @@ test('left', async () => { 'left-4', '-left-4', 'left-[4px]', - - // https://github.com/tailwindlabs/tailwindcss/issues/20010 - '-left-[(var(--my-var1)+var(--my-var2))]', ], - css` - @theme { - --spacing-4: 1rem; - --inset-shadowned: 1940px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --inset-shadowned: 1940px; } @@ -1154,10 +946,6 @@ test('left', async () => { left: calc(var(--spacing-4) * -1); } - .-left-\\[\\(var\\(--my-var1\\)\\+var\\(--my-var2\\)\\)\\] { - left: calc((var(--my-var1) + var(--my-var2)) * -1); - } - .-left-full { left: -100%; } @@ -1184,11 +972,17 @@ test('left', async () => { .left-shadowned { left: var(--inset-shadowned); - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme reference { + --spacing-4: 1rem; + --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); + } + @tailwind utilities; + `, [ 'left-shadow-sm', 'left', @@ -1204,28 +998,19 @@ test('left', async () => { '-left-4/foo', 'left-[4px]/foo', ], - css` - @theme reference { - --spacing-4: 1rem; - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, ), ).toEqual('') }) test('isolation', async () => { expect(await run(['isolate', 'isolation-auto'])).toMatchInlineSnapshot(` - " - .isolate { + ".isolate { isolation: isolate; } .isolation-auto { isolation: auto; - } - " + }" `) expect(await run(['-isolate', '-isolation-auto', 'isolate/foo', 'isolation-auto/foo'])).toEqual( '', @@ -1235,8 +1020,7 @@ test('isolation', async () => { test('z-index', async () => { expect(await run(['z-auto', 'z-10', '-z-10', 'z-[123]', '-z-[var(--value)]'])) .toMatchInlineSnapshot(` - " - .-z-10 { + ".-z-10 { z-index: calc(10 * -1); } @@ -1254,8 +1038,7 @@ test('z-index', async () => { .z-auto { z-index: auto; - } - " + }" `) expect( await run([ @@ -1273,25 +1056,23 @@ test('z-index', async () => { ).toEqual('') expect( - await run( - ['z-auto'], + await compileCss( css` @theme { --z-index-auto: 42; } @tailwind utilities; `, + ['z-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --z-index-auto: 42; } .z-auto { z-index: var(--z-index-auto); - } - " + }" `) }) @@ -1307,8 +1088,7 @@ test('order', async () => { 'order-none', ]), ).toMatchInlineSnapshot(` - " - .-order-4 { + ".-order-4 { order: calc(4 * -1); } @@ -1334,8 +1114,7 @@ test('order', async () => { .order-none { order: 0; - } - " + }" `) expect( await run([ @@ -1357,47 +1136,43 @@ test('order', async () => { ).toEqual('') expect( - await run( - ['order-first'], + await compileCss( css` @theme { --order-first: 1; } @tailwind utilities; `, + ['order-first'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --order-first: 1; } .order-first { order: var(--order-first); - } - " + }" `) expect( - await run( - ['order-last'], + await compileCss( css` @theme { --order-last: -1; } @tailwind utilities; `, + ['order-last'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --order-last: -1; } .order-last { order: var(--order-last); - } - " + }" `) }) @@ -1414,8 +1189,7 @@ test('col', async () => { 'col-span-[var(--my-variable)]', ]), ).toMatchInlineSnapshot(` - " - .-col-12 { + ".-col-12 { grid-column: calc(12 * -1); } @@ -1445,8 +1219,7 @@ test('col', async () => { .col-span-full { grid-column: 1 / -1; - } - " + }" `) expect( await run([ @@ -1465,31 +1238,35 @@ test('col', async () => { ).toEqual('') expect( - await run( - ['col-auto'], + await compileCss( css` @theme { --grid-column-auto: 5; } @tailwind utilities; `, + ['col-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-column-auto: 5; } .col-auto { grid-column: var(--grid-column-auto); - } - " + }" `) }) test('col-start', async () => { expect( - await run( + await compileCss( + css` + @theme { + --grid-column-start-custom: 1 column-start; + } + @tailwind utilities; + `, [ 'col-start-auto', 'col-start-4', @@ -1498,16 +1275,9 @@ test('col-start', async () => { '-col-start-4', 'col-start-custom', ], - css` - @theme { - --grid-column-start-custom: 1 column-start; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-column-start-custom: 1 column-start; } @@ -1533,8 +1303,7 @@ test('col-start', async () => { .col-start-custom { grid-column-start: var(--grid-column-start-custom); - } - " + }" `) expect( await run([ @@ -1550,42 +1319,39 @@ test('col-start', async () => { ).toEqual('') expect( - await run( - ['col-start-auto'], + await compileCss( css` @theme { --grid-column-start-auto: 7; } @tailwind utilities; `, + ['col-start-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-column-start-auto: 7; } .col-start-auto { grid-column-start: var(--grid-column-start-auto); - } - " + }" `) }) test('col-end', async () => { expect( - await run( - ['col-end-auto', 'col-end-4', 'col-end-99', 'col-end-[123]', '-col-end-4', 'col-end-custom'], + await compileCss( css` @theme { --grid-column-end-custom: 1 column-end; } @tailwind utilities; `, + ['col-end-auto', 'col-end-4', 'col-end-99', 'col-end-[123]', '-col-end-4', 'col-end-custom'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-column-end-custom: 1 column-end; } @@ -1611,8 +1377,7 @@ test('col-end', async () => { .col-end-custom { grid-column-end: var(--grid-column-end-custom); - } - " + }" `) expect( await run([ @@ -1628,25 +1393,23 @@ test('col-end', async () => { ).toEqual('') expect( - await run( - ['col-end-auto'], + await compileCss( css` @theme { --grid-column-end-auto: 3; } @tailwind utilities; `, + ['col-end-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-column-end-auto: 3; } .col-end-auto { grid-column-end: var(--grid-column-end-auto); - } - " + }" `) }) @@ -1663,8 +1426,7 @@ test('row', async () => { 'row-span-[var(--my-variable)]', ]), ).toMatchInlineSnapshot(` - " - .-row-12 { + ".-row-12 { grid-row: calc(12 * -1); } @@ -1694,8 +1456,7 @@ test('row', async () => { .row-span-full { grid-row: 1 / -1; - } - " + }" `) expect( await run([ @@ -1710,42 +1471,39 @@ test('row', async () => { 'row-span-full/foo', 'row-[span_123/span_123]/foo', 'row-span-[var(--my-variable)]/foo', - - // Candidates matching Object.prototype properties should not crash or - // produce output (see: https://github.com/tailwindlabs/tailwindcss/issues/19721) - 'row-constructor', - 'row-hasOwnProperty', - 'row-toString', - 'row-valueOf', ]), ).toEqual('') expect( - await run( - ['row-auto'], + await compileCss( css` @theme { --grid-row-auto: 9; } @tailwind utilities; `, + ['row-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-row-auto: 9; } .row-auto { grid-row: var(--grid-row-auto); - } - " + }" `) }) test('row-start', async () => { expect( - await run( + await compileCss( + css` + @theme { + --grid-row-start-custom: 1 row-start; + } + @tailwind utilities; + `, [ 'row-start-auto', 'row-start-4', @@ -1754,16 +1512,9 @@ test('row-start', async () => { '-row-start-4', 'row-start-custom', ], - css` - @theme { - --grid-row-start-custom: 1 row-start; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-row-start-custom: 1 row-start; } @@ -1789,8 +1540,7 @@ test('row-start', async () => { .row-start-custom { grid-row-start: var(--grid-row-start-custom); - } - " + }" `) expect( await run([ @@ -1806,42 +1556,39 @@ test('row-start', async () => { ).toEqual('') expect( - await run( - ['row-start-auto'], + await compileCss( css` @theme { --grid-row-start-auto: 11; } @tailwind utilities; `, + ['row-start-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-row-start-auto: 11; } .row-start-auto { grid-row-start: var(--grid-row-start-auto); - } - " + }" `) }) test('row-end', async () => { expect( - await run( - ['row-end-auto', 'row-end-4', 'row-end-99', 'row-end-[123]', '-row-end-4', 'row-end-custom'], + await compileCss( css` @theme { --grid-row-end-custom: 1 row-end; } @tailwind utilities; `, + ['row-end-auto', 'row-end-4', 'row-end-99', 'row-end-[123]', '-row-end-4', 'row-end-custom'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-row-end-custom: 1 row-end; } @@ -1867,8 +1614,7 @@ test('row-end', async () => { .row-end-custom { grid-row-end: var(--grid-row-end-custom); - } - " + }" `) expect( await run([ @@ -1884,33 +1630,30 @@ test('row-end', async () => { ).toEqual('') expect( - await run( - ['row-end-auto'], + await compileCss( css` @theme { --grid-row-end-auto: 13; } @tailwind utilities; `, + ['row-end-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-row-end-auto: 13; } .row-end-auto { grid-row-end: var(--grid-row-end-auto); - } - " + }" `) }) test('float', async () => { expect(await run(['float-start', 'float-end', 'float-right', 'float-left', 'float-none'])) .toMatchInlineSnapshot(` - " - .float-end { + ".float-end { float: inline-end; } @@ -1928,8 +1671,7 @@ test('float', async () => { .float-start { float: inline-start; - } - " + }" `) expect( await run([ @@ -1959,8 +1701,7 @@ test('clear', async () => { 'clear-none', ]), ).toMatchInlineSnapshot(` - " - .clear-both { + ".clear-both { clear: both; } @@ -1982,8 +1723,7 @@ test('clear', async () => { .clear-start { clear: inline-start; - } - " + }" `) expect( await run([ @@ -2006,18 +1746,17 @@ test('clear', async () => { test('margin', async () => { expect( - await run( - ['m-auto', 'm-4', 'm-[4px]', '-m-4', '-m-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['m-auto', 'm-4', 'm-[4px]', '-m-4', '-m-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -2039,8 +1778,7 @@ test('margin', async () => { .m-auto { margin: auto; - } - " + }" `) expect( await run(['m', 'm-auto/foo', 'm-4/foo', 'm-[4px]/foo', '-m-4/foo', '-m-[var(--value)]/foo']), @@ -2049,7 +1787,14 @@ test('margin', async () => { test('mx', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + --spacing-big: 100rem; + } + @tailwind utilities; + `, [ 'mx-auto', 'mx-1', @@ -2063,17 +1808,9 @@ test('mx', async () => { 'mx-[var(--my-var)]', '-mx-[var(--my-var)]', ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } @@ -2095,7 +1832,7 @@ test('mx', async () => { } .mx-1 { - margin-inline: var(--spacing); + margin-inline: calc(var(--spacing) * 1); } .mx-4 { @@ -2120,8 +1857,7 @@ test('mx', async () => { .mx-big { margin-inline: var(--spacing-big); - } - " + }" `) expect( await run([ @@ -2137,7 +1873,14 @@ test('mx', async () => { test('my', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + --spacing-big: 100rem; + } + @tailwind utilities; + `, [ 'my-1', 'my-99', @@ -2151,17 +1894,9 @@ test('my', async () => { 'my-[var(--my-var)]', '-my-[var(--my-var)]', ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } @@ -2187,7 +1922,7 @@ test('my', async () => { } .my-1 { - margin-block: var(--spacing); + margin-block: calc(var(--spacing) * 1); } .my-2\\.5 { @@ -2208,8 +1943,7 @@ test('my', async () => { .my-big { margin-block: var(--spacing-big); - } - " + }" `) expect( await run([ @@ -2225,7 +1959,14 @@ test('my', async () => { test('mt', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + --spacing-big: 100rem; + } + @tailwind utilities; + `, [ 'mt-1', 'mt-99', @@ -2239,17 +1980,9 @@ test('mt', async () => { 'mt-[var(--my-var)]', '-mt-[var(--my-var)]', ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } @@ -2275,7 +2008,7 @@ test('mt', async () => { } .mt-1 { - margin-top: var(--spacing); + margin-top: calc(var(--spacing) * 1); } .mt-2\\.5 { @@ -2296,8 +2029,7 @@ test('mt', async () => { .mt-big { margin-top: var(--spacing-big); - } - " + }" `) expect( await run([ @@ -2313,7 +2045,14 @@ test('mt', async () => { test('ms', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + --spacing-big: 100rem; + } + @tailwind utilities; + `, [ 'ms-1', 'ms-99', @@ -2327,17 +2066,9 @@ test('ms', async () => { 'ms-[var(--my-var)]', '-ms-[var(--my-var)]', ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } @@ -2363,7 +2094,7 @@ test('ms', async () => { } .ms-1 { - margin-inline-start: var(--spacing); + margin-inline-start: calc(var(--spacing) * 1); } .ms-2\\.5 { @@ -2384,8 +2115,7 @@ test('ms', async () => { .ms-big { margin-inline-start: var(--spacing-big); - } - " + }" `) expect( await run([ @@ -2401,7 +2131,14 @@ test('ms', async () => { test('me', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + --spacing-big: 100rem; + } + @tailwind utilities; + `, [ 'me-1', 'me-99', @@ -2415,17 +2152,9 @@ test('me', async () => { 'me-[var(--my-var)]', '-me-[var(--my-var)]', ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } @@ -2451,7 +2180,7 @@ test('me', async () => { } .me-1 { - margin-inline-end: var(--spacing); + margin-inline-end: calc(var(--spacing) * 1); } .me-2\\.5 { @@ -2472,8 +2201,7 @@ test('me', async () => { .me-big { margin-inline-end: var(--spacing-big); - } - " + }" `) expect( await run([ @@ -2487,185 +2215,16 @@ test('me', async () => { ).toEqual('') }) -test('mbs', async () => { - expect( - await run( - [ - 'mbs-1', - 'mbs-99', - 'mbs-2.5', - 'mbs-big', - 'mbs-[4px]', - '-mbs-4', - '-mbs-2.5', - '-mbs-big', - '-mbs-[4px]', - 'mbs-[var(--my-var)]', - '-mbs-[var(--my-var)]', - ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing: .25rem; - --spacing-big: 100rem; - } - - .-mbs-2\\.5 { - margin-block-start: calc(var(--spacing) * -2.5); - } - - .-mbs-4 { - margin-block-start: calc(var(--spacing) * -4); - } - - .-mbs-\\[4px\\] { - margin-block-start: -4px; - } - - .-mbs-\\[var\\(--my-var\\)\\] { - margin-block-start: calc(var(--my-var) * -1); - } - - .-mbs-big { - margin-block-start: calc(var(--spacing-big) * -1); - } - - .mbs-1 { - margin-block-start: var(--spacing); - } - - .mbs-2\\.5 { - margin-block-start: calc(var(--spacing) * 2.5); - } - - .mbs-99 { - margin-block-start: calc(var(--spacing) * 99); - } - - .mbs-\\[4px\\] { - margin-block-start: 4px; - } - - .mbs-\\[var\\(--my-var\\)\\] { - margin-block-start: var(--my-var); - } - - .mbs-big { - margin-block-start: var(--spacing-big); - } - " - `) - expect( - await run([ - 'mbs', - 'mbs-auto/foo', - 'mbs-4/foo', - 'mbs-[4px]/foo', - '-mbs-4/foo', - '-mbs-[var(--value)]/foo', - ]), - ).toEqual('') -}) - -test('mbe', async () => { - expect( - await run( - [ - 'mbe-1', - 'mbe-99', - 'mbe-2.5', - 'mbe-big', - 'mbe-[4px]', - '-mbe-4', - '-mbe-2.5', - '-mbe-big', - '-mbe-[4px]', - 'mbe-[var(--my-var)]', - '-mbe-[var(--my-var)]', - ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing: .25rem; - --spacing-big: 100rem; - } - - .-mbe-2\\.5 { - margin-block-end: calc(var(--spacing) * -2.5); - } - - .-mbe-4 { - margin-block-end: calc(var(--spacing) * -4); - } - - .-mbe-\\[4px\\] { - margin-block-end: -4px; - } - - .-mbe-\\[var\\(--my-var\\)\\] { - margin-block-end: calc(var(--my-var) * -1); - } - - .-mbe-big { - margin-block-end: calc(var(--spacing-big) * -1); - } - - .mbe-1 { - margin-block-end: var(--spacing); - } - - .mbe-2\\.5 { - margin-block-end: calc(var(--spacing) * 2.5); - } - - .mbe-99 { - margin-block-end: calc(var(--spacing) * 99); - } - - .mbe-\\[4px\\] { - margin-block-end: 4px; - } - - .mbe-\\[var\\(--my-var\\)\\] { - margin-block-end: var(--my-var); - } - - .mbe-big { - margin-block-end: var(--spacing-big); - } - " - `) - expect( - await run([ - 'mbe', - 'mbe-auto/foo', - 'mbe-4/foo', - 'mbe-[4px]/foo', - '-mbe-4/foo', - '-mbe-[var(--value)]/foo', - ]), - ).toEqual('') -}) - test('mr', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + --spacing-big: 100rem; + } + @tailwind utilities; + `, [ 'mr-1', 'mr-99', @@ -2679,17 +2238,9 @@ test('mr', async () => { 'mr-[var(--my-var)]', '-mr-[var(--my-var)]', ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } @@ -2715,7 +2266,7 @@ test('mr', async () => { } .mr-1 { - margin-right: var(--spacing); + margin-right: calc(var(--spacing) * 1); } .mr-2\\.5 { @@ -2736,8 +2287,7 @@ test('mr', async () => { .mr-big { margin-right: var(--spacing-big); - } - " + }" `) expect( await run([ @@ -2753,7 +2303,14 @@ test('mr', async () => { test('mb', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + --spacing-big: 100rem; + } + @tailwind utilities; + `, [ 'mb-1', 'mb-99', @@ -2767,17 +2324,9 @@ test('mb', async () => { 'mb-[var(--my-var)]', '-mb-[var(--my-var)]', ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } @@ -2803,7 +2352,7 @@ test('mb', async () => { } .mb-1 { - margin-bottom: var(--spacing); + margin-bottom: calc(var(--spacing) * 1); } .mb-2\\.5 { @@ -2824,8 +2373,7 @@ test('mb', async () => { .mb-big { margin-bottom: var(--spacing-big); - } - " + }" `) expect( await run([ @@ -2841,7 +2389,14 @@ test('mb', async () => { test('ml', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + --spacing-big: 100rem; + } + @tailwind utilities; + `, [ 'ml-1', 'ml-99', @@ -2855,17 +2410,9 @@ test('ml', async () => { 'ml-[var(--my-var)]', '-ml-[var(--my-var)]', ], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } @@ -2891,7 +2438,7 @@ test('ml', async () => { } .ml-1 { - margin-left: var(--spacing); + margin-left: calc(var(--spacing) * 1); } .ml-2\\.5 { @@ -2912,8 +2459,7 @@ test('ml', async () => { .ml-big { margin-left: var(--spacing-big); - } - " + }" `) expect( await run([ @@ -2929,18 +2475,17 @@ test('ml', async () => { test('margin sort order', async () => { expect( - await run( - ['mb-4', 'me-4', 'mx-4', 'ml-4', 'ms-4', 'm-4', 'mr-4', 'mt-4', 'my-4'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['mb-4', 'me-4', 'mx-4', 'ml-4', 'ms-4', 'm-4', 'mr-4', 'mt-4', 'my-4'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -2978,8 +2523,7 @@ test('margin sort order', async () => { .ml-4 { margin-left: var(--spacing-4); - } - " + }" `) expect( await run([ @@ -2999,15 +2543,13 @@ test('margin sort order', async () => { test('box-sizing', async () => { expect(await run(['box-border', 'box-content'])).toMatchInlineSnapshot(` - " - .box-border { + ".box-border { box-sizing: border-box; } .box-content { box-sizing: content-box; - } - " + }" `) expect( await run(['box', '-box-border', '-box-content', 'box-border/foo', 'box-content/foo']), @@ -3017,8 +2559,7 @@ test('box-sizing', async () => { test('line-clamp', async () => { expect(await run(['line-clamp-4', 'line-clamp-99', 'line-clamp-[123]', 'line-clamp-none'])) .toMatchInlineSnapshot(` - " - .line-clamp-4 { + ".line-clamp-4 { -webkit-line-clamp: 4; -webkit-box-orient: vertical; display: -webkit-box; @@ -3044,8 +2585,7 @@ test('line-clamp', async () => { -webkit-box-orient: horizontal; display: block; overflow: visible; - } - " + }" `) expect( await run([ @@ -3064,18 +2604,17 @@ test('line-clamp', async () => { ).toEqual('') expect( - await run( - ['line-clamp-none'], + await compileCss( css` @theme { --line-clamp-none: 0; } @tailwind utilities; `, + ['line-clamp-none'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --line-clamp-none: 0; } @@ -3084,8 +2623,7 @@ test('line-clamp', async () => { -webkit-box-orient: vertical; display: -webkit-box; overflow: hidden; - } - " + }" `) }) @@ -3115,8 +2653,7 @@ test('display', async () => { 'hidden', ]), ).toMatchInlineSnapshot(` - " - .block { + ".block { display: block; } @@ -3198,8 +2735,7 @@ test('display', async () => { .table-row-group { display: table-row-group; - } - " + }" `) expect( await run([ @@ -3251,15 +2787,13 @@ test('display', async () => { test('field-sizing', async () => { expect(await run(['field-sizing-content', 'field-sizing-fixed'])).toMatchInlineSnapshot(` - " - .field-sizing-content { + ".field-sizing-content { field-sizing: content; } .field-sizing-fixed { field-sizing: fixed; - } - " + }" `) expect( await run(['field-sizing-[other]', '-field-sizing-content', '-field-sizing-fixed']), @@ -3268,18 +2802,17 @@ test('field-sizing', async () => { test('aspect-ratio', async () => { expect( - await run( - ['aspect-video', 'aspect-[10/9]', 'aspect-4/3', 'aspect-8.5/11'], + await compileCss( css` @theme { --aspect-video: 16 / 9; } @tailwind utilities; `, + ['aspect-video', 'aspect-[10/9]', 'aspect-4/3'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --aspect-video: 16 / 9; } @@ -3287,18 +2820,13 @@ test('aspect-ratio', async () => { aspect-ratio: 4 / 3; } - .aspect-8\\.5\\/11 { - aspect-ratio: 8.5 / 11; - } - .aspect-\\[10\\/9\\] { aspect-ratio: 10 / 9; } .aspect-video { aspect-ratio: var(--aspect-video); - } - " + }" `) expect( await run([ @@ -3313,14 +2841,19 @@ test('aspect-ratio', async () => { 'aspect--4/3', 'aspect--4/-3', 'aspect-4/-3', - 'aspect-1.23/4.56', ]), ).toEqual('') }) test('size', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + } + @tailwind utilities; + `, [ 'size-auto', 'size-full', @@ -3331,16 +2864,9 @@ test('size', async () => { 'size-1/2', 'size-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -3382,8 +2908,7 @@ test('size', async () => { .size-min { width: min-content; height: min-content; - } - " + }" `) expect( await run([ @@ -3409,7 +2934,14 @@ test('size', async () => { test('width', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + --width-xl: 36rem; + } + @tailwind utilities; + `, [ 'w-full', 'w-auto', @@ -3425,17 +2957,9 @@ test('width', async () => { 'w-1/2', 'w-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - --width-xl: 36rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --width-xl: 36rem; } @@ -3490,8 +3014,7 @@ test('width', async () => { .w-xl { width: var(--width-xl); - } - " + }" `) expect( await run([ @@ -3522,7 +3045,14 @@ test('width', async () => { test('min-width', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + --container-xl: 36rem; + } + @tailwind utilities; + `, [ 'min-w-full', 'min-w-auto', @@ -3533,17 +3063,9 @@ test('min-width', async () => { 'min-w-xl', 'min-w-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - --container-xl: 36rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --container-xl: 36rem; } @@ -3578,8 +3100,7 @@ test('min-width', async () => { .min-w-xl { min-width: var(--container-xl); - } - " + }" `) expect( await run([ @@ -3600,8 +3121,7 @@ test('min-width', async () => { test('max-width', async () => { expect( - await run( - ['max-w-none', 'max-w-full', 'max-w-max', 'max-w-fit', 'max-w-4', 'max-w-xl', 'max-w-[4px]'], + await compileCss( css` @theme { --spacing-4: 1rem; @@ -3609,10 +3129,10 @@ test('max-width', async () => { } @tailwind utilities; `, + ['max-w-none', 'max-w-full', 'max-w-max', 'max-w-fit', 'max-w-4', 'max-w-xl', 'max-w-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; --container-xl: 36rem; } @@ -3643,8 +3163,7 @@ test('max-width', async () => { .max-w-xl { max-width: var(--container-xl); - } - " + }" `) expect( await run([ @@ -3666,7 +3185,13 @@ test('max-width', async () => { test('height', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + } + @tailwind utilities; + `, [ 'h-full', 'h-auto', @@ -3682,16 +3207,9 @@ test('height', async () => { 'h-1/2', 'h-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -3745,8 +3263,7 @@ test('height', async () => { .h-svh { height: 100svh; - } - " + }" `) expect( await run([ @@ -3777,7 +3294,13 @@ test('height', async () => { test('min-height', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + } + @tailwind utilities; + `, [ 'min-h-full', 'min-h-auto', @@ -3792,16 +3315,9 @@ test('min-height', async () => { 'min-h-4', 'min-h-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -3851,8 +3367,7 @@ test('min-height', async () => { .min-h-svh { min-height: 100svh; - } - " + }" `) expect( await run([ @@ -3877,7 +3392,13 @@ test('min-height', async () => { test('max-height', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing-4: 1rem; + } + @tailwind utilities; + `, [ 'max-h-none', 'max-h-full', @@ -3892,16 +3413,9 @@ test('max-height', async () => { 'max-h-4', 'max-h-[4px]', ], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -3951,8 +3465,7 @@ test('max-height', async () => { .max-h-svh { max-height: 100svh; - } - " + }" `) expect( await run([ @@ -3976,586 +3489,10 @@ test('max-height', async () => { ).toEqual('') }) -test('inline-size', async () => { - expect( - await run( - [ - 'inline-full', - 'inline-auto', - 'inline-screen', - 'inline-svw', - 'inline-lvw', - 'inline-dvw', - 'inline-min', - 'inline-max', - 'inline-fit', - 'inline-4', - 'inline-xl', - 'inline-1/2', - 'inline-[4px]', - ], - css` - @theme { - --spacing-4: 1rem; - --container-xl: 36rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - --container-xl: 36rem; - } - - .inline-1\\/2 { - inline-size: 50%; - } - - .inline-4 { - inline-size: var(--spacing-4); - } - - .inline-\\[4px\\] { - inline-size: 4px; - } - - .inline-auto { - inline-size: auto; - } - - .inline-dvw { - inline-size: 100dvw; - } - - .inline-fit { - inline-size: fit-content; - } - - .inline-full { - inline-size: 100%; - } - - .inline-lvw { - inline-size: 100lvw; - } - - .inline-max { - inline-size: max-content; - } - - .inline-min { - inline-size: min-content; - } - - .inline-screen { - inline-size: 100vw; - } - - .inline-svw { - inline-size: 100svw; - } - - .inline-xl { - inline-size: var(--container-xl); - } - " - `) - expect( - await run([ - 'inline--1', - 'inline--1/2', - 'inline--1/-2', - 'inline-1/-2', - '-inline-4', - '-inline-1/2', - '-inline-[4px]', - 'inline-full/foo', - 'inline-auto/foo', - 'inline-screen/foo', - 'inline-svw/foo', - 'inline-lvw/foo', - 'inline-dvw/foo', - 'inline-min/foo', - 'inline-max/foo', - 'inline-fit/foo', - 'inline-4/foo', - 'inline-xl/foo', - 'inline-1/2/foo', - 'inline-[4px]/foo', - ]), - ).toEqual('') -}) - -test('min-inline-size', async () => { - expect( - await run( - [ - 'min-inline-full', - 'min-inline-auto', - 'min-inline-min', - 'min-inline-max', - 'min-inline-fit', - 'min-inline-4', - 'min-inline-xl', - 'min-inline-[4px]', - ], - css` - @theme { - --spacing-4: 1rem; - --container-xl: 36rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - --container-xl: 36rem; - } - - .min-inline-4 { - min-inline-size: var(--spacing-4); - } - - .min-inline-\\[4px\\] { - min-inline-size: 4px; - } - - .min-inline-auto { - min-inline-size: auto; - } - - .min-inline-fit { - min-inline-size: fit-content; - } - - .min-inline-full { - min-inline-size: 100%; - } - - .min-inline-max { - min-inline-size: max-content; - } - - .min-inline-min { - min-inline-size: min-content; - } - - .min-inline-xl { - min-inline-size: var(--container-xl); - } - " - `) - expect( - await run([ - 'min-inline', - '-min-inline-4', - '-min-inline-[4px]', - 'min-inline-auto/foo', - 'min-inline-full/foo', - 'min-inline-min/foo', - 'min-inline-max/foo', - 'min-inline-fit/foo', - 'min-inline-4/foo', - 'min-inline-xl/foo', - 'min-inline-[4px]/foo', - ]), - ).toEqual('') -}) - -test('max-inline-size', async () => { - expect( - await run( - [ - 'max-inline-none', - 'max-inline-full', - 'max-inline-max', - 'max-inline-fit', - 'max-inline-4', - 'max-inline-xl', - 'max-inline-[4px]', - ], - css` - @theme { - --spacing-4: 1rem; - --container-xl: 36rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - --container-xl: 36rem; - } - - .max-inline-4 { - max-inline-size: var(--spacing-4); - } - - .max-inline-\\[4px\\] { - max-inline-size: 4px; - } - - .max-inline-fit { - max-inline-size: fit-content; - } - - .max-inline-full { - max-inline-size: 100%; - } - - .max-inline-max { - max-inline-size: max-content; - } - - .max-inline-none { - max-inline-size: none; - } - - .max-inline-xl { - max-inline-size: var(--container-xl); - } - " - `) - expect( - await run([ - 'max-inline', - 'max-inline-auto', - '-max-inline-4', - '-max-inline-[4px]', - 'max-inline-none/foo', - 'max-inline-full/foo', - 'max-inline-max/foo', - 'max-inline-max/foo', - 'max-inline-fit/foo', - 'max-inline-4/foo', - 'max-inline-xl/foo', - 'max-inline-[4px]/foo', - ]), - ).toEqual('') -}) - -test('block-size', async () => { - expect( - await run( - [ - 'block-full', - 'block-auto', - 'block-screen', - 'block-svh', - 'block-lvh', - 'block-dvh', - 'block-min', - 'block-lh', - 'block-max', - 'block-fit', - 'block-4', - 'block-1/2', - 'block-[4px]', - ], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - } - - .block-1\\/2 { - block-size: 50%; - } - - .block-4 { - block-size: var(--spacing-4); - } - - .block-\\[4px\\] { - block-size: 4px; - } - - .block-auto { - block-size: auto; - } - - .block-dvh { - block-size: 100dvh; - } - - .block-fit { - block-size: fit-content; - } - - .block-full { - block-size: 100%; - } - - .block-lh { - block-size: 1lh; - } - - .block-lvh { - block-size: 100lvh; - } - - .block-max { - block-size: max-content; - } - - .block-min { - block-size: min-content; - } - - .block-screen { - block-size: 100vh; - } - - .block-svh { - block-size: 100svh; - } - " - `) - expect( - await run([ - '-block-4', - 'block--1', - 'block--1/2', - 'block--1/-2', - 'block-1/-2', - '-block-1/2', - '-block-[4px]', - 'block-full/foo', - 'block-auto/foo', - 'block-screen/foo', - 'block-svh/foo', - 'block-lvh/foo', - 'block-dvh/foo', - 'block-lh/foo', - 'block-min/foo', - 'block-max/foo', - 'block-fit/foo', - 'block-4/foo', - 'block-1/2/foo', - 'block-[4px]/foo', - ]), - ).toEqual('') -}) - -test('min-block-size', async () => { - expect( - await run( - [ - 'min-block-full', - 'min-block-auto', - 'min-block-screen', - 'min-block-svh', - 'min-block-lvh', - 'min-block-dvh', - 'min-block-min', - 'min-block-lh', - 'min-block-max', - 'min-block-fit', - 'min-block-4', - 'min-block-[4px]', - ], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - } - - .min-block-4 { - min-block-size: var(--spacing-4); - } - - .min-block-\\[4px\\] { - min-block-size: 4px; - } - - .min-block-auto { - min-block-size: auto; - } - - .min-block-dvh { - min-block-size: 100dvh; - } - - .min-block-fit { - min-block-size: fit-content; - } - - .min-block-full { - min-block-size: 100%; - } - - .min-block-lh { - min-block-size: 1lh; - } - - .min-block-lvh { - min-block-size: 100lvh; - } - - .min-block-max { - min-block-size: max-content; - } - - .min-block-min { - min-block-size: min-content; - } - - .min-block-screen { - min-block-size: 100vh; - } - - .min-block-svh { - min-block-size: 100svh; - } - " - `) - expect( - await run([ - 'min-block', - '-min-block-4', - '-min-block-[4px]', - 'min-block-auto/foo', - 'min-block-full/foo', - 'min-block-screen/foo', - 'min-block-svh/foo', - 'min-block-lvh/foo', - 'min-block-dvh/foo', - 'min-block-lh/foo', - 'min-block-min/foo', - 'min-block-max/foo', - 'min-block-fit/foo', - 'min-block-4/foo', - 'min-block-[4px]/foo', - ]), - ).toEqual('') -}) - -test('max-block-size', async () => { - expect( - await run( - [ - 'max-block-none', - 'max-block-full', - 'max-block-screen', - 'max-block-svh', - 'max-block-lvh', - 'max-block-dvh', - 'max-block-lh', - 'max-block-min', - 'max-block-max', - 'max-block-fit', - 'max-block-4', - 'max-block-[4px]', - ], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - } - - .max-block-4 { - max-block-size: var(--spacing-4); - } - - .max-block-\\[4px\\] { - max-block-size: 4px; - } - - .max-block-dvh { - max-block-size: 100dvh; - } - - .max-block-fit { - max-block-size: fit-content; - } - - .max-block-full { - max-block-size: 100%; - } - - .max-block-lh { - max-block-size: 1lh; - } - - .max-block-lvh { - max-block-size: 100lvh; - } - - .max-block-max { - max-block-size: max-content; - } - - .max-block-min { - max-block-size: min-content; - } - - .max-block-none { - max-block-size: none; - } - - .max-block-screen { - max-block-size: 100vh; - } - - .max-block-svh { - max-block-size: 100svh; - } - " - `) - expect( - await run([ - 'max-block', - 'max-block-auto', - '-max-block-4', - '-max-block-[4px]', - 'max-block-none/foo', - 'max-block-full/foo', - 'max-block-screen/foo', - 'max-block-svh/foo', - 'max-block-lvh/foo', - 'max-block-dvh/foo', - 'max-block-lh/foo', - 'max-block-min/foo', - 'max-block-max/foo', - 'max-block-fit/foo', - 'max-block-4/foo', - 'max-block-[4px]/foo', - ]), - ).toEqual('') -}) - describe('container', () => { test('creates the right media queries and sorts it before width', async () => { expect( - await run( - ['w-1/2', 'container', 'max-w-[var(--breakpoint-sm)]'], + await compileCss( css` @theme { --breakpoint-sm: 40rem; @@ -4566,10 +3503,10 @@ describe('container', () => { } @tailwind utilities; `, + ['w-1/2', 'container', 'max-w-[var(--breakpoint-sm)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --breakpoint-sm: 40rem; } @@ -4613,15 +3550,13 @@ describe('container', () => { .max-w-\\[var\\(--breakpoint-sm\\)\\] { max-width: var(--breakpoint-sm); - } - " + }" `) }) test('sorts breakpoints based on unit and then in ascending aOrder', async () => { expect( - await run( - ['container'], + await compileCss( css` @theme reference { --breakpoint-lg: 64rem; @@ -4634,10 +3569,10 @@ describe('container', () => { } @tailwind utilities; `, + ['container'], ), ).toMatchInlineSnapshot(` - " - .container { + ".container { width: 100%; } @@ -4681,15 +3616,13 @@ describe('container', () => { .container { max-width: 96rem; } - } - " + }" `) }) test('custom `@utility container` always follow the core utility ', async () => { expect( - await run( - ['w-1/2', 'container', 'max-w-[var(--breakpoint-sm)]'], + await compileCss( css` @theme { --breakpoint-sm: 40rem; @@ -4709,10 +3642,10 @@ describe('container', () => { } } `, + ['w-1/2', 'container', 'max-w-[var(--breakpoint-sm)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --breakpoint-sm: 40rem; } @@ -4767,8 +3700,7 @@ describe('container', () => { .max-w-\\[var\\(--breakpoint-sm\\)\\] { max-width: var(--breakpoint-sm); - } - " + }" `) }) }) @@ -4785,8 +3717,7 @@ test('flex', async () => { 'flex-[123]', ]), ).toMatchInlineSnapshot(` - " - .flex-1 { + ".flex-1 { flex: 1; } @@ -4812,8 +3743,7 @@ test('flex', async () => { .flex-none { flex: none; - } - " + }" `) expect( await run([ @@ -4840,8 +3770,7 @@ test('flex', async () => { test('flex-shrink', async () => { expect(await run(['shrink', 'shrink-0', 'shrink-[123]'])).toMatchInlineSnapshot(` - " - .shrink { + ".shrink { flex-shrink: 1; } @@ -4851,8 +3780,7 @@ test('flex-shrink', async () => { .shrink-\\[123\\] { flex-shrink: 123; - } - " + }" `) expect( await run([ @@ -4871,8 +3799,7 @@ test('flex-shrink', async () => { test('flex-grow', async () => { expect(await run(['grow', 'grow-0', 'grow-[123]'])).toMatchInlineSnapshot(` - " - .grow { + ".grow { flex-grow: 1; } @@ -4882,8 +3809,7 @@ test('flex-grow', async () => { .grow-\\[123\\] { flex-grow: 123; - } - " + }" `) expect( await run([ @@ -4902,18 +3828,17 @@ test('flex-grow', async () => { test('flex-basis', async () => { expect( - await run( - ['basis-auto', 'basis-full', 'basis-xl', 'basis-11/12', 'basis-[123px]'], + await compileCss( css` @theme { --container-xl: 36rem; } @tailwind utilities; `, + ['basis-auto', 'basis-full', 'basis-xl', 'basis-11/12', 'basis-[123px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --container-xl: 36rem; } @@ -4935,8 +3860,7 @@ test('flex-basis', async () => { .basis-xl { flex-basis: var(--container-xl); - } - " + }" `) expect( await run([ @@ -4958,15 +3882,13 @@ test('flex-basis', async () => { test('table-layout', async () => { expect(await run(['table-auto', 'table-fixed'])).toMatchInlineSnapshot(` - " - .table-auto { + ".table-auto { table-layout: auto; } .table-fixed { table-layout: fixed; - } - " + }" `) expect(await run(['-table-auto', '-table-fixed', 'table-auto/foo', 'table-fixed/foo'])).toEqual( '', @@ -4975,15 +3897,13 @@ test('table-layout', async () => { test('caption-side', async () => { expect(await run(['caption-top', 'caption-bottom'])).toMatchInlineSnapshot(` - " - .caption-bottom { + ".caption-bottom { caption-side: bottom; } .caption-top { caption-side: top; - } - " + }" `) expect( await run(['-caption-top', '-caption-bottom', 'caption-top/foo', 'caption-bottom/foo']), @@ -4992,15 +3912,13 @@ test('caption-side', async () => { test('border-collapse', async () => { expect(await run(['border-collapse', 'border-separate'])).toMatchInlineSnapshot(` - " - .border-collapse { + ".border-collapse { border-collapse: collapse; } .border-separate { border-collapse: separate; - } - " + }" `) expect( await run([ @@ -5014,18 +3932,17 @@ test('border-collapse', async () => { test('border-spacing', async () => { expect( - await run( - ['border-spacing-1', 'border-spacing-[123px]'], + await compileCss( css` @theme { --spacing-1: 0.25rem; } @tailwind utilities; `, + ['border-spacing-1', 'border-spacing-[123px]'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-border-spacing-x: 0; @@ -5060,8 +3977,7 @@ test('border-spacing', async () => { syntax: ""; inherits: false; initial-value: 0; - } - " + }" `) expect( await run([ @@ -5076,18 +3992,17 @@ test('border-spacing', async () => { test('border-spacing-x', async () => { expect( - await run( - ['border-spacing-x-1', 'border-spacing-x-[123px]'], + await compileCss( css` @theme { --spacing-1: 0.25rem; } @tailwind utilities; `, + ['border-spacing-x-1', 'border-spacing-x-[123px]'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-border-spacing-x: 0; @@ -5120,8 +4035,7 @@ test('border-spacing-x', async () => { syntax: ""; inherits: false; initial-value: 0; - } - " + }" `) expect( await run([ @@ -5136,18 +4050,17 @@ test('border-spacing-x', async () => { test('border-spacing-y', async () => { expect( - await run( - ['border-spacing-y-1', 'border-spacing-y-[123px]'], + await compileCss( css` @theme { --spacing-1: 0.25rem; } @tailwind utilities; `, + ['border-spacing-y-1', 'border-spacing-y-[123px]'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-border-spacing-x: 0; @@ -5180,8 +4093,7 @@ test('border-spacing-y', async () => { syntax: ""; inherits: false; initial-value: 0; - } - " + }" `) expect( await run([ @@ -5210,8 +4122,7 @@ test('origin', async () => { 'origin-[var(--value)]', ]), ).toMatchInlineSnapshot(` - " - .origin-\\[50px_100px\\] { + ".origin-\\[50px_100px\\] { transform-origin: 50px 100px; } @@ -5253,8 +4164,7 @@ test('origin', async () => { .origin-top-right { transform-origin: 100% 0; - } - " + }" `) expect( await run([ @@ -5275,25 +4185,23 @@ test('origin', async () => { ).toEqual('') expect( - await run( - ['origin-top'], + await compileCss( css` @theme { --transform-origin-top: 10px 20px; } @tailwind utilities; `, + ['origin-top'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --transform-origin-top: 10px 20px; } .origin-top { transform-origin: var(--transform-origin-top); - } - " + }" `) }) @@ -5313,8 +4221,7 @@ test('perspective-origin', async () => { 'perspective-origin-[var(--value)]', ]), ).toMatchInlineSnapshot(` - " - .perspective-origin-\\[50px_100px\\] { + ".perspective-origin-\\[50px_100px\\] { perspective-origin: 50px 100px; } @@ -5356,8 +4263,7 @@ test('perspective-origin', async () => { .perspective-origin-top-right { perspective-origin: 100% 0; - } - " + }" `) expect( await run([ @@ -5378,26 +4284,24 @@ test('perspective-origin', async () => { ).toEqual('') expect( - await run( - ['perspective-origin-top'], + await compileCss( css` @theme { --perspective-origin-top: 10px 20px; } @tailwind utilities; `, + ['perspective-origin-top'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --perspective-origin-top: 10px 20px; } .perspective-origin-top { perspective-origin: var(--perspective-origin-top); perspective: var(--perspective-origin-top); - } - " + }" `) }) @@ -5411,8 +4315,7 @@ test('translate', async () => { '-translate-[var(--value)]', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-translate-x: 0; @@ -5468,8 +4371,7 @@ test('translate', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect( await run([ @@ -5496,8 +4398,7 @@ test('translate-x', async () => { '-translate-x-[var(--value)]', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-translate-x: 0; @@ -5543,8 +4444,7 @@ test('translate-x', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect( await run([ @@ -5561,18 +4461,17 @@ test('translate-x', async () => { ).toEqual('') expect( - await run( - ['translate-x-full', '-translate-x-full', 'translate-x-px', '-translate-x-[var(--value)]'], + await compileCss( css` @theme { --spacing: 0.25rem; } @tailwind utilities; `, + ['translate-x-full', '-translate-x-full', 'translate-x-px', '-translate-x-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-translate-x: 0; @@ -5618,8 +4517,7 @@ test('translate-x', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect( await run([ @@ -5644,8 +4542,7 @@ test('translate-y', async () => { '-translate-y-[var(--value)]', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-translate-x: 0; @@ -5691,8 +4588,7 @@ test('translate-y', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect( await run([ @@ -5709,18 +4605,17 @@ test('translate-y', async () => { ).toEqual('') expect( - await run( - ['translate-y-full', '-translate-y-full', 'translate-y-px', '-translate-y-[var(--value)]'], + await compileCss( css` @theme { --spacing: 0.25rem; } @tailwind utilities; `, + ['translate-y-full', '-translate-y-full', 'translate-y-px', '-translate-y-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-translate-x: 0; @@ -5766,8 +4661,7 @@ test('translate-y', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect( await run([ @@ -5786,8 +4680,7 @@ test('translate-y', async () => { test('translate-z', async () => { expect(await run(['-translate-z-px', 'translate-z-px', '-translate-z-[var(--value)]'])) .toMatchInlineSnapshot(` - " - @layer properties { + "@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-translate-x: 0; @@ -5828,8 +4721,7 @@ test('translate-z', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect( await run([ @@ -5849,8 +4741,7 @@ test('translate-z', async () => { test('translate-3d', async () => { expect(await run(['translate-3d'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-translate-x: 0; @@ -5880,8 +4771,7 @@ test('translate-3d', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect(await run(['-translate-3d', 'translate-3d/foo'])).toEqual('') }) @@ -5898,8 +4788,7 @@ test('rotate', async () => { '-rotate-(--var)', ]), ).toMatchInlineSnapshot(` - " - .-rotate-\\(--var\\) { + ".-rotate-\\(--var\\) { rotate: calc(var(--var) * -1); } @@ -5925,8 +4814,7 @@ test('rotate', async () => { .rotate-\\[123deg\\] { rotate: 123deg; - } - " + }" `) expect( await run([ @@ -5952,8 +4840,7 @@ test('rotate-x', async () => { '-rotate-x-(--var)', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-rotate-x: initial; @@ -6013,8 +4900,7 @@ test('rotate-x', async () => { @property --tw-skew-y { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -6040,8 +4926,7 @@ test('rotate-y', async () => { '-rotate-y-(--var)', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-rotate-x: initial; @@ -6106,8 +4991,7 @@ test('rotate-y', async () => { @property --tw-skew-y { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -6133,8 +5017,7 @@ test('rotate-z', async () => { '-rotate-z-(--var)', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-rotate-x: initial; @@ -6199,8 +5082,7 @@ test('rotate-z', async () => { @property --tw-skew-y { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -6217,8 +5099,7 @@ test('rotate-z', async () => { test('skew', async () => { expect(await run(['skew-6', '-skew-6', 'skew-[123deg]'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-rotate-x: initial; @@ -6271,8 +5152,7 @@ test('skew', async () => { @property --tw-skew-y { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -6288,8 +5168,7 @@ test('skew', async () => { test('skew-x', async () => { expect(await run(['skew-x-6', '-skew-x-6', 'skew-x-[123deg]'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-rotate-x: initial; @@ -6339,8 +5218,7 @@ test('skew-x', async () => { @property --tw-skew-y { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -6356,8 +5234,7 @@ test('skew-x', async () => { test('skew-y', async () => { expect(await run(['skew-y-6', '-skew-y-6', 'skew-y-[123deg]'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-rotate-x: initial; @@ -6407,8 +5284,7 @@ test('skew-y', async () => { @property --tw-skew-y { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -6425,8 +5301,7 @@ test('skew-y', async () => { test('scale', async () => { expect(await run(['scale-50', '-scale-50', 'scale-[2]', 'scale-[2_1.5_3]'])) .toMatchInlineSnapshot(` - " - @layer properties { + "@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-scale-x: 1; @@ -6474,8 +5349,7 @@ test('scale', async () => { syntax: "*"; inherits: false; initial-value: 1; - } - " + }" `) expect( await run([ @@ -6493,8 +5367,7 @@ test('scale', async () => { test('scale-3d', async () => { expect(await run(['scale-3d'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-scale-x: 1; @@ -6524,16 +5397,14 @@ test('scale-3d', async () => { syntax: "*"; inherits: false; initial-value: 1; - } - " + }" `) expect(await run(['-scale-3d', 'scale-3d/foo'])).toEqual('') }) test('scale-x', async () => { expect(await run(['scale-x-50', '-scale-x-50', 'scale-x-[2]'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-scale-x: 1; @@ -6574,12 +5445,10 @@ test('scale-x', async () => { syntax: "*"; inherits: false; initial-value: 1; - } - " + }" `) expect(await run(['scale-200', 'scale-x-400'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-scale-x: 1; @@ -6617,8 +5486,7 @@ test('scale-x', async () => { syntax: "*"; inherits: false; initial-value: 1; - } - " + }" `) expect( await run([ @@ -6637,8 +5505,7 @@ test('scale-x', async () => { test('scale-y', async () => { expect(await run(['scale-y-50', '-scale-y-50', 'scale-y-[2]'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-scale-x: 1; @@ -6679,8 +5546,7 @@ test('scale-y', async () => { syntax: "*"; inherits: false; initial-value: 1; - } - " + }" `) expect( await run([ @@ -6697,8 +5563,7 @@ test('scale-y', async () => { test('scale-z', async () => { expect(await run(['scale-z-50', '-scale-z-50', 'scale-z-[123deg]'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-scale-x: 1; @@ -6739,8 +5604,7 @@ test('scale-z', async () => { syntax: "*"; inherits: false; initial-value: 1; - } - " + }" `) expect( await run([ @@ -6764,8 +5628,7 @@ test('transform', async () => { 'transform-[scaleZ(2)_rotateY(45deg)]', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-rotate-x: initial; @@ -6782,7 +5645,7 @@ test('transform', async () => { } .transform-\\[scaleZ\\(2\\)_rotateY\\(45deg\\)\\] { - transform: scaleZ(2) rotateY(45deg); + transform: scaleZ(2)rotateY(45deg); } .transform-cpu { @@ -6820,8 +5683,7 @@ test('transform', async () => { @property --tw-skew-y { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -6836,8 +5698,7 @@ test('transform', async () => { 'backface-hidden', ]), ).toMatchInlineSnapshot(` - " - .backface-hidden { + ".backface-hidden { backface-visibility: hidden; } @@ -6871,8 +5732,7 @@ test('transform', async () => { .transform-view { transform-box: view-box; - } - " + }" `) expect( await run([ @@ -6898,31 +5758,9 @@ test('transform', async () => { ).toEqual('') }) -test('zoom', async () => { - expect(await run(['zoom-50', 'zoom-100', 'zoom-[var(--zoom)]'])).toMatchInlineSnapshot(` - " - .zoom-50 { - zoom: 50%; - } - - .zoom-100 { - zoom: 100%; - } - - .zoom-\\[var\\(--zoom\\)\\] { - zoom: var(--zoom); - } - " - `) - expect( - await run(['zoom', '-zoom-50', 'zoom--50', 'zoom-1.5', 'zoom-unknown', 'zoom-50/foo']), - ).toEqual('') -}) - test('perspective', async () => { expect( - await run( - ['perspective-normal', 'perspective-dramatic', 'perspective-none', 'perspective-[456px]'], + await compileCss( css` @theme { --perspective-dramatic: 100px; @@ -6930,10 +5768,10 @@ test('perspective', async () => { } @tailwind utilities; `, + ['perspective-normal', 'perspective-dramatic', 'perspective-none', 'perspective-[456px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --perspective-dramatic: 100px; --perspective-normal: 500px; } @@ -6952,8 +5790,7 @@ test('perspective', async () => { .perspective-normal { perspective: var(--perspective-normal); - } - " + }" `) expect( await run([ @@ -6969,31 +5806,35 @@ test('perspective', async () => { ).toEqual('') expect( - await run( - ['perspective-none'], + await compileCss( css` @theme { --perspective-none: 400px; } @tailwind utilities; `, + ['perspective-none'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --perspective-none: 400px; } .perspective-none { perspective: var(--perspective-none); - } - " + }" `) }) test('cursor', async () => { expect( - await run( + await compileCss( + css` + @theme { + --cursor-custom: url(/my-cursor.png); + } + @tailwind utilities; + `, [ 'cursor-auto', 'cursor-default', @@ -7034,16 +5875,9 @@ test('cursor', async () => { 'cursor-[var(--value)]', 'cursor-custom', ], - css` - @theme { - --cursor-custom: url(/my-cursor.png); - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --cursor-custom: url("/my-cursor.png"); } @@ -7197,8 +6031,7 @@ test('cursor', async () => { .cursor-zoom-out { cursor: zoom-out; - } - " + }" `) expect( await run([ @@ -7285,8 +6118,7 @@ test('cursor', async () => { test('touch-action', async () => { expect(await run(['touch-auto', 'touch-none', 'touch-manipulation'])).toMatchInlineSnapshot(` - " - .touch-auto { + ".touch-auto { touch-action: auto; } @@ -7296,8 +6128,7 @@ test('touch-action', async () => { .touch-none { touch-action: none; - } - " + }" `) expect( await run([ @@ -7322,8 +6153,7 @@ test('touch-pan', async () => { 'touch-pan-down', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-pan-x: initial; @@ -7376,8 +6206,7 @@ test('touch-pan', async () => { @property --tw-pinch-zoom { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -7399,8 +6228,7 @@ test('touch-pan', async () => { test('touch-pinch-zoom', async () => { expect(await run(['touch-pinch-zoom'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-pan-x: initial; @@ -7428,8 +6256,7 @@ test('touch-pinch-zoom', async () => { @property --tw-pinch-zoom { syntax: "*"; inherits: false - } - " + }" `) expect(await run(['-touch-pinch-zoom', 'touch-pinch-zoom/foo'])).toEqual('') }) @@ -7437,8 +6264,7 @@ test('touch-pinch-zoom', async () => { test('select', async () => { expect(await run(['select-none', 'select-text', 'select-all', 'select-auto'])) .toMatchInlineSnapshot(` - " - .select-all { + ".select-all { -webkit-user-select: all; user-select: all; } @@ -7456,8 +6282,7 @@ test('select', async () => { .select-text { -webkit-user-select: text; user-select: text; - } - " + }" `) expect( await run([ @@ -7475,8 +6300,7 @@ test('select', async () => { test('resize', async () => { expect(await run(['resize-none', 'resize', 'resize-x', 'resize-y'])).toMatchInlineSnapshot(` - " - .resize { + ".resize { resize: both; } @@ -7490,8 +6314,7 @@ test('resize', async () => { .resize-y { resize: vertical; - } - " + }" `) expect( await run([ @@ -7509,8 +6332,7 @@ test('resize', async () => { test('scroll-snap-type', async () => { expect(await run(['snap-none', 'snap-x', 'snap-y', 'snap-both'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-scroll-snap-strictness: proximity; @@ -7538,8 +6360,7 @@ test('scroll-snap-type', async () => { syntax: "*"; inherits: false; initial-value: proximity; - } - " + }" `) expect( await run([ @@ -7557,8 +6378,7 @@ test('scroll-snap-type', async () => { test('--tw-scroll-snap-strictness', async () => { expect(await run(['snap-mandatory', 'snap-proximity'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-scroll-snap-strictness: proximity; @@ -7578,8 +6398,7 @@ test('--tw-scroll-snap-strictness', async () => { syntax: "*"; inherits: false; initial-value: proximity; - } - " + }" `) expect( await run(['-snap-mandatory', '-snap-proximity', 'snap-mandatory/foo', 'snap-proximity/foo']), @@ -7589,8 +6408,7 @@ test('--tw-scroll-snap-strictness', async () => { test('scroll-snap-align', async () => { expect(await run(['snap-align-none', 'snap-start', 'snap-end', 'snap-center'])) .toMatchInlineSnapshot(` - " - .snap-align-none { + ".snap-align-none { scroll-snap-align: none; } @@ -7604,8 +6422,7 @@ test('scroll-snap-align', async () => { .snap-start { scroll-snap-align: start; - } - " + }" `) expect( await run([ @@ -7623,15 +6440,13 @@ test('scroll-snap-align', async () => { test('scroll-snap-stop', async () => { expect(await run(['snap-normal', 'snap-always'])).toMatchInlineSnapshot(` - " - .snap-always { + ".snap-always { scroll-snap-stop: always; } .snap-normal { scroll-snap-stop: normal; - } - " + }" `) expect(await run(['-snap-normal', '-snap-always', 'snap-normal/foo', 'snap-always/foo'])).toEqual( '', @@ -7640,18 +6455,17 @@ test('scroll-snap-stop', async () => { test('scroll-m', async () => { expect( - await run( - ['scroll-m-4', 'scroll-m-[4px]', '-scroll-m-4', '-scroll-m-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-m-4', 'scroll-m-[4px]', '-scroll-m-4', '-scroll-m-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -7669,8 +6483,7 @@ test('scroll-m', async () => { .scroll-m-\\[4px\\] { scroll-margin: 4px; - } - " + }" `) expect( await run([ @@ -7685,18 +6498,17 @@ test('scroll-m', async () => { test('scroll-mx', async () => { expect( - await run( - ['scroll-mx-4', 'scroll-mx-[4px]', '-scroll-mx-4', '-scroll-mx-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-mx-4', 'scroll-mx-[4px]', '-scroll-mx-4', '-scroll-mx-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -7714,8 +6526,7 @@ test('scroll-mx', async () => { .scroll-mx-\\[4px\\] { scroll-margin-inline: 4px; - } - " + }" `) expect( await run([ @@ -7730,18 +6541,17 @@ test('scroll-mx', async () => { test('scroll-my', async () => { expect( - await run( - ['scroll-my-4', 'scroll-my-[4px]', '-scroll-my-4', '-scroll-my-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-my-4', 'scroll-my-[4px]', '-scroll-my-4', '-scroll-my-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -7759,8 +6569,7 @@ test('scroll-my', async () => { .scroll-my-\\[4px\\] { scroll-margin-block: 4px; - } - " + }" `) expect( await run([ @@ -7775,18 +6584,17 @@ test('scroll-my', async () => { test('scroll-ms', async () => { expect( - await run( - ['scroll-ms-4', 'scroll-ms-[4px]', '-scroll-ms-4', '-scroll-ms-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-ms-4', 'scroll-ms-[4px]', '-scroll-ms-4', '-scroll-ms-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -7804,8 +6612,7 @@ test('scroll-ms', async () => { .scroll-ms-\\[4px\\] { scroll-margin-inline-start: 4px; - } - " + }" `) expect( await run([ @@ -7820,18 +6627,17 @@ test('scroll-ms', async () => { test('scroll-me', async () => { expect( - await run( - ['scroll-me-4', 'scroll-me-[4px]', '-scroll-me-4', '-scroll-me-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-me-4', 'scroll-me-[4px]', '-scroll-me-4', '-scroll-me-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -7849,8 +6655,7 @@ test('scroll-me', async () => { .scroll-me-\\[4px\\] { scroll-margin-inline-end: 4px; - } - " + }" `) expect( await run([ @@ -7863,110 +6668,19 @@ test('scroll-me', async () => { ).toEqual('') }) -test('scroll-mbs', async () => { - expect( - await run( - ['scroll-mbs-4', 'scroll-mbs-[4px]', '-scroll-mbs-4', '-scroll-mbs-[var(--value)]'], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - } - - .-scroll-mbs-4 { - scroll-margin-block-start: calc(var(--spacing-4) * -1); - } - - .-scroll-mbs-\\[var\\(--value\\)\\] { - scroll-margin-block-start: calc(var(--value) * -1); - } - - .scroll-mbs-4 { - scroll-margin-block-start: var(--spacing-4); - } - - .scroll-mbs-\\[4px\\] { - scroll-margin-block-start: 4px; - } - " - `) - expect( - await run([ - 'scroll-mbs', - 'scroll-mbs-4/foo', - 'scroll-mbs-[4px]/foo', - '-scroll-mbs-4/foo', - '-scroll-mbs-[var(--value)]/foo', - ]), - ).toEqual('') -}) - -test('scroll-mbe', async () => { - expect( - await run( - ['scroll-mbe-4', 'scroll-mbe-[4px]', '-scroll-mbe-4', '-scroll-mbe-[var(--value)]'], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - } - - .-scroll-mbe-4 { - scroll-margin-block-end: calc(var(--spacing-4) * -1); - } - - .-scroll-mbe-\\[var\\(--value\\)\\] { - scroll-margin-block-end: calc(var(--value) * -1); - } - - .scroll-mbe-4 { - scroll-margin-block-end: var(--spacing-4); - } - - .scroll-mbe-\\[4px\\] { - scroll-margin-block-end: 4px; - } - " - `) - expect( - await run([ - 'scroll-mbe', - 'scroll-mbe-4/foo', - 'scroll-mbe-[4px]/foo', - '-scroll-mbe-4/foo', - '-scroll-mbe-[var(--value)]/foo', - ]), - ).toEqual('') -}) - test('scroll-mt', async () => { expect( - await run( - ['scroll-mt-4', 'scroll-mt-[4px]', '-scroll-mt-4', '-scroll-mt-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-mt-4', 'scroll-mt-[4px]', '-scroll-mt-4', '-scroll-mt-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -7984,8 +6698,7 @@ test('scroll-mt', async () => { .scroll-mt-\\[4px\\] { scroll-margin-top: 4px; - } - " + }" `) expect( await run([ @@ -8000,18 +6713,17 @@ test('scroll-mt', async () => { test('scroll-mr', async () => { expect( - await run( - ['scroll-mr-4', 'scroll-mr-[4px]', '-scroll-mr-4', '-scroll-mr-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-mr-4', 'scroll-mr-[4px]', '-scroll-mr-4', '-scroll-mr-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8029,8 +6741,7 @@ test('scroll-mr', async () => { .scroll-mr-\\[4px\\] { scroll-margin-right: 4px; - } - " + }" `) expect( await run([ @@ -8045,18 +6756,17 @@ test('scroll-mr', async () => { test('scroll-mb', async () => { expect( - await run( - ['scroll-mb-4', 'scroll-mb-[4px]', '-scroll-mb-4', '-scroll-mb-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-mb-4', 'scroll-mb-[4px]', '-scroll-mb-4', '-scroll-mb-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8074,8 +6784,7 @@ test('scroll-mb', async () => { .scroll-mb-\\[4px\\] { scroll-margin-bottom: 4px; - } - " + }" `) expect( await run([ @@ -8090,18 +6799,17 @@ test('scroll-mb', async () => { test('scroll-ml', async () => { expect( - await run( - ['scroll-ml-4', 'scroll-ml-[4px]', '-scroll-ml-4', '-scroll-ml-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-ml-4', 'scroll-ml-[4px]', '-scroll-ml-4', '-scroll-ml-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8119,8 +6827,7 @@ test('scroll-ml', async () => { .scroll-ml-\\[4px\\] { scroll-margin-left: 4px; - } - " + }" `) expect( await run([ @@ -8135,18 +6842,17 @@ test('scroll-ml', async () => { test('scroll-p', async () => { expect( - await run( - ['scroll-p-4', 'scroll-p-[4px]', '-scroll-p-4', '-scroll-p-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-p-4', 'scroll-p-[4px]', '-scroll-p-4', '-scroll-p-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8156,8 +6862,7 @@ test('scroll-p', async () => { .scroll-p-\\[4px\\] { scroll-padding: 4px; - } - " + }" `) expect( await run([ @@ -8172,18 +6877,17 @@ test('scroll-p', async () => { test('scroll-px', async () => { expect( - await run( - ['scroll-px-4', 'scroll-px-[4px]', '-scroll-px-4', '-scroll-px-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-px-4', 'scroll-px-[4px]', '-scroll-px-4', '-scroll-px-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8193,8 +6897,7 @@ test('scroll-px', async () => { .scroll-px-\\[4px\\] { scroll-padding-inline: 4px; - } - " + }" `) expect( await run([ @@ -8209,18 +6912,17 @@ test('scroll-px', async () => { test('scroll-py', async () => { expect( - await run( - ['scroll-py-4', 'scroll-py-[4px]', '-scroll-py-4', '-scroll-py-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-py-4', 'scroll-py-[4px]', '-scroll-py-4', '-scroll-py-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8230,8 +6932,7 @@ test('scroll-py', async () => { .scroll-py-\\[4px\\] { scroll-padding-block: 4px; - } - " + }" `) expect( await run([ @@ -8246,18 +6947,17 @@ test('scroll-py', async () => { test('scroll-ps', async () => { expect( - await run( - ['scroll-ps-4', 'scroll-ps-[4px]', '-scroll-ps-4', '-scroll-ps-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-ps-4', 'scroll-ps-[4px]', '-scroll-ps-4', '-scroll-ps-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8267,8 +6967,7 @@ test('scroll-ps', async () => { .scroll-ps-\\[4px\\] { scroll-padding-inline-start: 4px; - } - " + }" `) expect( await run([ @@ -8283,18 +6982,17 @@ test('scroll-ps', async () => { test('scroll-pe', async () => { expect( - await run( - ['scroll-pe-4', 'scroll-pe-[4px]', '-scroll-pe-4', '-scroll-pe-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-pe-4', 'scroll-pe-[4px]', '-scroll-pe-4', '-scroll-pe-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8304,8 +7002,7 @@ test('scroll-pe', async () => { .scroll-pe-\\[4px\\] { scroll-padding-inline-end: 4px; - } - " + }" `) expect( await run([ @@ -8318,94 +7015,19 @@ test('scroll-pe', async () => { ).toEqual('') }) -test('scroll-pbs', async () => { - expect( - await run( - ['scroll-pbs-4', 'scroll-pbs-[4px]'], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - } - - .scroll-pbs-4 { - scroll-padding-block-start: var(--spacing-4); - } - - .scroll-pbs-\\[4px\\] { - scroll-padding-block-start: 4px; - } - " - `) - expect( - await run([ - 'scroll-pbs', - 'scroll-pbs-4/foo', - 'scroll-pbs-[4px]/foo', - '-scroll-pbs-4', - '-scroll-pbs-[var(--value)]', - ]), - ).toEqual('') -}) - -test('scroll-pbe', async () => { - expect( - await run( - ['scroll-pbe-4', 'scroll-pbe-[4px]'], - css` - @theme { - --spacing-4: 1rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing-4: 1rem; - } - - .scroll-pbe-4 { - scroll-padding-block-end: var(--spacing-4); - } - - .scroll-pbe-\\[4px\\] { - scroll-padding-block-end: 4px; - } - " - `) - expect( - await run([ - 'scroll-pbe', - 'scroll-pbe-4/foo', - 'scroll-pbe-[4px]/foo', - '-scroll-pbe-4', - '-scroll-pbe-[var(--value)]', - ]), - ).toEqual('') -}) - test('scroll-pt', async () => { expect( - await run( - ['scroll-pt-4', 'scroll-pt-[4px]', '-scroll-pt-4', '-scroll-pt-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-pt-4', 'scroll-pt-[4px]', '-scroll-pt-4', '-scroll-pt-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8415,8 +7037,7 @@ test('scroll-pt', async () => { .scroll-pt-\\[4px\\] { scroll-padding-top: 4px; - } - " + }" `) expect( await run([ @@ -8431,18 +7052,17 @@ test('scroll-pt', async () => { test('scroll-pr', async () => { expect( - await run( - ['scroll-pr-4', 'scroll-pr-[4px]', '-scroll-pr-4', '-scroll-pr-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-pr-4', 'scroll-pr-[4px]', '-scroll-pr-4', '-scroll-pr-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8452,8 +7072,7 @@ test('scroll-pr', async () => { .scroll-pr-\\[4px\\] { scroll-padding-right: 4px; - } - " + }" `) expect( await run([ @@ -8468,18 +7087,17 @@ test('scroll-pr', async () => { test('scroll-pb', async () => { expect( - await run( - ['scroll-pb-4', 'scroll-pb-[4px]', '-scroll-pb-4', '-scroll-pb-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-pb-4', 'scroll-pb-[4px]', '-scroll-pb-4', '-scroll-pb-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8489,8 +7107,7 @@ test('scroll-pb', async () => { .scroll-pb-\\[4px\\] { scroll-padding-bottom: 4px; - } - " + }" `) expect( await run([ @@ -8505,18 +7122,17 @@ test('scroll-pb', async () => { test('scroll-pl', async () => { expect( - await run( - ['scroll-pl-4', 'scroll-pl-[4px]', '-scroll-pl-4', '-scroll-pl-[var(--value)]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['scroll-pl-4', 'scroll-pl-[4px]', '-scroll-pl-4', '-scroll-pl-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -8526,8 +7142,7 @@ test('scroll-pl', async () => { .scroll-pl-\\[4px\\] { scroll-padding-left: 4px; - } - " + }" `) expect( await run([ @@ -8542,15 +7157,13 @@ test('scroll-pl', async () => { test('list-style-position', async () => { expect(await run(['list-inside', 'list-outside'])).toMatchInlineSnapshot(` - " - .list-inside { + ".list-inside { list-style-position: inside; } .list-outside { list-style-position: outside; - } - " + }" `) expect( await run(['-list-inside', '-list-outside', 'list-inside/foo', 'list-outside/foo']), @@ -8560,8 +7173,7 @@ test('list-style-position', async () => { test('list', async () => { expect(await run(['list-none', 'list-disc', 'list-decimal', 'list-[var(--value)]'])) .toMatchInlineSnapshot(` - " - .list-\\[var\\(--value\\)\\] { + ".list-\\[var\\(--value\\)\\] { list-style-type: var(--value); } @@ -8575,8 +7187,7 @@ test('list', async () => { .list-none { list-style-type: none; - } - " + }" `) expect( await run([ @@ -8592,39 +7203,35 @@ test('list', async () => { ).toEqual('') expect( - await run( - ['list-none'], + await compileCss( css` @theme { --list-style-type-none: disc; } @tailwind utilities; `, + ['list-none'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --list-style-type-none: disc; } .list-none { list-style-type: var(--list-style-type-none); - } - " + }" `) }) test('list-image', async () => { expect(await run(['list-image-none', 'list-image-[var(--value)]'])).toMatchInlineSnapshot(` - " - .list-image-\\[var\\(--value\\)\\] { + ".list-image-\\[var\\(--value\\)\\] { list-style-image: var(--value); } .list-image-none { list-style-image: none; - } - " + }" `) expect( await run([ @@ -8637,39 +7244,35 @@ test('list-image', async () => { ).toEqual('') expect( - await run( - ['list-image-none'], + await compileCss( css` @theme { --list-style-image-none: url(../foo.png); } @tailwind utilities; `, + ['list-image-none'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --list-style-image-none: url("../foo.png"); } .list-image-none { list-style-image: var(--list-style-image-none); - } - " + }" `) }) test('appearance', async () => { expect(await run(['appearance-none', 'appearance-auto'])).toMatchInlineSnapshot(` - " - .appearance-auto { + ".appearance-auto { appearance: auto; } .appearance-none { appearance: none; - } - " + }" `) expect( await run([ @@ -8693,8 +7296,7 @@ test('color-scheme', async () => { 'scheme-only-light', ]), ).toMatchInlineSnapshot(` - " - .scheme-dark { + ".scheme-dark { color-scheme: dark; } @@ -8716,8 +7318,7 @@ test('color-scheme', async () => { .scheme-only-light { color-scheme: light only; - } - " + }" `) expect( await run([ @@ -8733,7 +7334,14 @@ test('color-scheme', async () => { test('columns', async () => { expect( - await run( + await compileCss( + css` + @theme { + --container-3xs: 16rem; + --container-7xl: 80rem; + } + @tailwind utilities; + `, [ 'columns-auto', 'columns-3xs', @@ -8743,17 +7351,9 @@ test('columns', async () => { 'columns-[123]', 'columns-[var(--value)]', ], - css` - @theme { - --container-3xs: 16rem; - --container-7xl: 80rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --container-3xs: 16rem; --container-7xl: 80rem; } @@ -8784,8 +7384,7 @@ test('columns', async () => { .columns-auto { columns: auto; - } - " + }" `) expect( await run([ @@ -8806,25 +7405,23 @@ test('columns', async () => { ).toEqual('') expect( - await run( - ['columns-auto'], + await compileCss( css` @theme { --columns-auto: 3; } @tailwind utilities; `, + ['columns-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --columns-auto: 3; } .columns-auto { columns: var(--columns-auto); - } - " + }" `) }) @@ -8841,8 +7438,7 @@ test('break-before', async () => { 'break-before-column', ]), ).toMatchInlineSnapshot(` - " - .break-before-all { + ".break-before-all { break-before: all; } @@ -8872,8 +7468,7 @@ test('break-before', async () => { .break-before-right { break-before: right; - } - " + }" `) expect( await run([ @@ -8907,8 +7502,7 @@ test('break-inside', async () => { 'break-inside-avoid-column', ]), ).toMatchInlineSnapshot(` - " - .break-inside-auto { + ".break-inside-auto { break-inside: auto; } @@ -8922,8 +7516,7 @@ test('break-inside', async () => { .break-inside-avoid-page { break-inside: avoid-page; - } - " + }" `) expect( await run([ @@ -8953,8 +7546,7 @@ test('break-after', async () => { 'break-after-column', ]), ).toMatchInlineSnapshot(` - " - .break-after-all { + ".break-after-all { break-after: all; } @@ -8984,8 +7576,7 @@ test('break-after', async () => { .break-after-right { break-after: right; - } - " + }" `) expect( await run([ @@ -9012,29 +7603,15 @@ test('break-after', async () => { test('auto-cols', async () => { expect( - await run( - [ - 'auto-cols-auto', - 'auto-cols-min', - 'auto-cols-max', - 'auto-cols-fr', - 'auto-cols-[2fr]', - 'auto-cols-12', - ], - css` - @tailwind utilities; - @theme { - --spacing: 0.25rem; - } - `, - ), + await run([ + 'auto-cols-auto', + 'auto-cols-min', + 'auto-cols-max', + 'auto-cols-fr', + 'auto-cols-[2fr]', + ]), ).toMatchInlineSnapshot(` - " - .auto-cols-12 { - grid-auto-columns: calc(var(--spacing) * 12); - } - - .auto-cols-\\[2fr\\] { + ".auto-cols-\\[2fr\\] { grid-auto-columns: 2fr; } @@ -9052,12 +7629,7 @@ test('auto-cols', async () => { .auto-cols-min { grid-auto-columns: min-content; - } - - :root, :host { - --spacing: .25rem; - } - " + }" `) expect( await run([ @@ -9073,25 +7645,23 @@ test('auto-cols', async () => { ).toEqual('') expect( - await run( - ['auto-cols-auto'], + await compileCss( css` @theme { --grid-auto-columns-auto: 2fr; } @tailwind utilities; `, + ['auto-cols-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-auto-columns-auto: 2fr; } .auto-cols-auto { grid-auto-columns: var(--grid-auto-columns-auto); - } - " + }" `) }) @@ -9105,8 +7675,7 @@ test('grid-flow', async () => { 'grid-flow-col-dense', ]), ).toMatchInlineSnapshot(` - " - .grid-flow-col { + ".grid-flow-col { grid-auto-flow: column; } @@ -9124,8 +7693,7 @@ test('grid-flow', async () => { .grid-flow-row-dense { grid-auto-flow: row dense; - } - " + }" `) expect( await run([ @@ -9146,29 +7714,15 @@ test('grid-flow', async () => { test('auto-rows', async () => { expect( - await run( - [ - 'auto-rows-auto', - 'auto-rows-min', - 'auto-rows-max', - 'auto-rows-fr', - 'auto-rows-[2fr]', - 'auto-rows-12', - ], - css` - @tailwind utilities; - @theme { - --spacing: 0.25rem; - } - `, - ), + await run([ + 'auto-rows-auto', + 'auto-rows-min', + 'auto-rows-max', + 'auto-rows-fr', + 'auto-rows-[2fr]', + ]), ).toMatchInlineSnapshot(` - " - .auto-rows-12 { - grid-auto-rows: calc(var(--spacing) * 12); - } - - .auto-rows-\\[2fr\\] { + ".auto-rows-\\[2fr\\] { grid-auto-rows: 2fr; } @@ -9186,12 +7740,7 @@ test('auto-rows', async () => { .auto-rows-min { grid-auto-rows: min-content; - } - - :root, :host { - --spacing: .25rem; - } - " + }" `) expect( await run([ @@ -9207,25 +7756,23 @@ test('auto-rows', async () => { ).toEqual('') expect( - await run( - ['auto-rows-auto'], + await compileCss( css` @theme { --grid-auto-rows-auto: 2fr; } @tailwind utilities; `, + ['auto-rows-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-auto-rows-auto: 2fr; } .auto-rows-auto { grid-auto-rows: var(--grid-auto-rows-auto); - } - " + }" `) }) @@ -9239,8 +7786,7 @@ test('grid-cols', async () => { 'grid-cols-[123]', ]), ).toMatchInlineSnapshot(` - " - .grid-cols-12 { + ".grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); } @@ -9258,8 +7804,7 @@ test('grid-cols', async () => { .grid-cols-subgrid { grid-template-columns: subgrid; - } - " + }" `) expect( await run([ @@ -9280,25 +7825,23 @@ test('grid-cols', async () => { ).toEqual('') expect( - await run( - ['grid-cols-none'], + await compileCss( css` @theme { --grid-template-columns-none: 200px 1fr; } @tailwind utilities; `, + ['grid-cols-none'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-template-columns-none: 200px 1fr; } .grid-cols-none { grid-template-columns: var(--grid-template-columns-none); - } - " + }" `) }) @@ -9312,8 +7855,7 @@ test('grid-rows', async () => { 'grid-rows-[123]', ]), ).toMatchInlineSnapshot(` - " - .grid-rows-12 { + ".grid-rows-12 { grid-template-rows: repeat(12, minmax(0, 1fr)); } @@ -9331,8 +7873,7 @@ test('grid-rows', async () => { .grid-rows-subgrid { grid-template-rows: subgrid; - } - " + }" `) expect( await run([ @@ -9353,33 +7894,30 @@ test('grid-rows', async () => { ).toEqual('') expect( - await run( - ['grid-rows-none'], + await compileCss( css` @theme { --grid-template-rows-none: 200px 1fr; } @tailwind utilities; `, + ['grid-rows-none'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --grid-template-rows-none: 200px 1fr; } .grid-rows-none { grid-template-rows: var(--grid-template-rows-none); - } - " + }" `) }) test('flex-direction', async () => { expect(await run(['flex-row', 'flex-row-reverse', 'flex-col', 'flex-col-reverse'])) .toMatchInlineSnapshot(` - " - .flex-col { + ".flex-col { flex-direction: column; } @@ -9393,8 +7931,7 @@ test('flex-direction', async () => { .flex-row-reverse { flex-direction: row-reverse; - } - " + }" `) expect( await run([ @@ -9412,8 +7949,7 @@ test('flex-direction', async () => { test('flex-wrap', async () => { expect(await run(['flex-wrap', 'flex-wrap-reverse', 'flex-nowrap'])).toMatchInlineSnapshot(` - " - .flex-nowrap { + ".flex-nowrap { flex-wrap: nowrap; } @@ -9423,8 +7959,7 @@ test('flex-wrap', async () => { .flex-wrap-reverse { flex-wrap: wrap-reverse; - } - " + }" `) expect( await run([ @@ -9453,8 +7988,7 @@ test('place-content', async () => { 'place-content-stretch', ]), ).toMatchInlineSnapshot(` - " - .place-content-around { + ".place-content-around { place-content: space-around; } @@ -9492,8 +8026,7 @@ test('place-content', async () => { .place-content-stretch { place-content: stretch; - } - " + }" `) expect( await run([ @@ -9530,8 +8063,7 @@ test('place-items', async () => { 'place-items-stretch', ]), ).toMatchInlineSnapshot(` - " - .place-items-baseline { + ".place-items-baseline { place-items: baseline; } @@ -9557,8 +8089,7 @@ test('place-items', async () => { .place-items-stretch { place-items: stretch stretch; - } - " + }" `) expect( await run([ @@ -9593,8 +8124,7 @@ test('align-content', async () => { 'content-stretch', ]), ).toMatchInlineSnapshot(` - " - .content-around { + ".content-around { align-content: space-around; } @@ -9636,8 +8166,7 @@ test('align-content', async () => { .content-stretch { align-content: stretch; - } - " + }" `) expect( await run([ @@ -9677,8 +8206,7 @@ test('items', async () => { 'items-stretch', ]), ).toMatchInlineSnapshot(` - " - .items-baseline { + ".items-baseline { align-items: baseline; } @@ -9708,8 +8236,7 @@ test('items', async () => { .items-stretch { align-items: stretch; - } - " + }" `) expect( await run([ @@ -9745,8 +8272,7 @@ test('justify', async () => { 'justify-stretch', ]), ).toMatchInlineSnapshot(` - " - .justify-around { + ".justify-around { justify-content: space-around; } @@ -9784,8 +8310,7 @@ test('justify', async () => { .justify-stretch { justify-content: stretch; - } - " + }" `) expect( await run([ @@ -9821,8 +8346,7 @@ test('justify-items', async () => { 'justify-items-stretch', ]), ).toMatchInlineSnapshot(` - " - .justify-items-center { + ".justify-items-center { justify-items: center; } @@ -9844,8 +8368,7 @@ test('justify-items', async () => { .justify-items-stretch { justify-items: stretch; - } - " + }" `) expect( await run([ @@ -9864,18 +8387,17 @@ test('justify-items', async () => { test('gap', async () => { expect( - await run( - ['gap-4', 'gap-[4px]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['gap-4', 'gap-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -9885,26 +8407,24 @@ test('gap', async () => { .gap-\\[4px\\] { gap: 4px; - } - " + }" `) expect(await run(['gap', '-gap-4', '-gap-[4px]', 'gap-4/foo', 'gap-[4px]/foo'])).toEqual('') }) test('gap-x', async () => { expect( - await run( - ['gap-x-4', 'gap-x-[4px]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['gap-x-4', 'gap-x-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -9914,8 +8434,7 @@ test('gap-x', async () => { .gap-x-\\[4px\\] { column-gap: 4px; - } - " + }" `) expect( await run(['gap-x', '-gap-x-4', '-gap-x-[4px]', 'gap-x-4/foo', 'gap-x-[4px]/foo']), @@ -9924,18 +8443,17 @@ test('gap-x', async () => { test('gap-y', async () => { expect( - await run( - ['gap-y-4', 'gap-y-[4px]'], + await compileCss( css` @theme { --spacing-4: 1rem; } @tailwind utilities; `, + ['gap-y-4', 'gap-y-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing-4: 1rem; } @@ -9945,8 +8463,7 @@ test('gap-y', async () => { .gap-y-\\[4px\\] { row-gap: 4px; - } - " + }" `) expect( await run(['gap-y', '-gap-y-4', '-gap-y-[4px]', 'gap-y-4/foo', 'gap-y-[4px]/foo']), @@ -9955,31 +8472,17 @@ test('gap-y', async () => { test('space-x', async () => { expect( - await run( - [ - 'space-x-0', - 'space-x-[0]', - 'space-x-[0px]', - '-space-x-0', - 'space-x-[-0]', - 'space-x-[-0px]', - 'space-x-1', - '-space-x-1', - 'space-x-4', - 'space-x-[4px]', - '-space-x-4', - ], + await compileCss( css` @theme { - --spacing: 0.25rem; --spacing-4: 1rem; } @tailwind utilities; `, + ['space-x-4', 'space-x-[4px]', '-space-x-4'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-space-x-reverse: 0; @@ -9988,49 +8491,21 @@ test('space-x', async () => { } :root, :host { - --spacing: .25rem; --spacing-4: 1rem; } - :where(.-space-x-0 > :not(:last-child)) { - --tw-space-x-reverse: 0; - margin-inline: 0; - } - - :where(.-space-x-1 > :not(:last-child)) { - --tw-space-x-reverse: 0; - margin-inline-start: calc(calc(var(--spacing) * -1) * var(--tw-space-x-reverse)); - margin-inline-end: calc(calc(var(--spacing) * -1) * calc(1 - var(--tw-space-x-reverse))); - } - :where(.-space-x-4 > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline-start: calc(calc(var(--spacing-4) * -1) * var(--tw-space-x-reverse)); margin-inline-end: calc(calc(var(--spacing-4) * -1) * calc(1 - var(--tw-space-x-reverse))); } - :where(.space-x-0 > :not(:last-child)) { - --tw-space-x-reverse: 0; - margin-inline: 0; - } - - :where(.space-x-1 > :not(:last-child)) { - --tw-space-x-reverse: 0; - margin-inline-start: calc(var(--spacing) * var(--tw-space-x-reverse)); - margin-inline-end: calc(var(--spacing) * calc(1 - var(--tw-space-x-reverse))); - } - :where(.space-x-4 > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline-start: calc(var(--spacing-4) * var(--tw-space-x-reverse)); margin-inline-end: calc(var(--spacing-4) * calc(1 - var(--tw-space-x-reverse))); } - :where(.space-x-\\[-0\\] > :not(:last-child)), :where(.space-x-\\[-0px\\] > :not(:last-child)), :where(.space-x-\\[0\\] > :not(:last-child)), :where(.space-x-\\[0px\\] > :not(:last-child)) { - --tw-space-x-reverse: 0; - margin-inline: 0; - } - :where(.space-x-\\[4px\\] > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline-start: calc(4px * var(--tw-space-x-reverse)); @@ -10041,39 +8516,24 @@ test('space-x', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect(await run(['space-x', 'space-x-4/foo', 'space-x-[4px]/foo', '-space-x-4/foo'])).toEqual('') }) test('space-y', async () => { expect( - await run( - [ - 'space-y-0', - 'space-y-[0]', - 'space-y-[0px]', - '-space-y-0', - 'space-y-[-0]', - 'space-y-[-0px]', - 'space-y-1', - '-space-y-1', - 'space-y-4', - 'space-y-[4px]', - '-space-y-4', - ], + await compileCss( css` @theme { - --spacing: 0.25rem; --spacing-4: 1rem; } @tailwind utilities; `, + ['space-y-4', 'space-y-[4px]', '-space-y-4'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-space-y-reverse: 0; @@ -10082,49 +8542,21 @@ test('space-y', async () => { } :root, :host { - --spacing: .25rem; --spacing-4: 1rem; } - :where(.-space-y-0 > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block: 0; - } - - :where(.-space-y-1 > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * -1) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * -1) * calc(1 - var(--tw-space-y-reverse))); - } - :where(.-space-y-4 > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(calc(var(--spacing-4) * -1) * var(--tw-space-y-reverse)); margin-block-end: calc(calc(var(--spacing-4) * -1) * calc(1 - var(--tw-space-y-reverse))); } - :where(.space-y-0 > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block: 0; - } - - :where(.space-y-1 > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(var(--spacing) * var(--tw-space-y-reverse)); - margin-block-end: calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse))); - } - :where(.space-y-4 > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(var(--spacing-4) * var(--tw-space-y-reverse)); margin-block-end: calc(var(--spacing-4) * calc(1 - var(--tw-space-y-reverse))); } - :where(.space-y-\\[-0\\] > :not(:last-child)), :where(.space-y-\\[-0px\\] > :not(:last-child)), :where(.space-y-\\[0\\] > :not(:last-child)), :where(.space-y-\\[0px\\] > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block: 0; - } - :where(.space-y-\\[4px\\] > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(4px * var(--tw-space-y-reverse)); @@ -10135,16 +8567,14 @@ test('space-y', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect(await run(['space-y', 'space-y-4/foo', 'space-y-[4px]/foo', '-space-y-4/foo'])).toEqual('') }) test('space-x-reverse', async () => { expect(await run(['space-x-reverse'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-space-x-reverse: 0; @@ -10160,16 +8590,14 @@ test('space-x-reverse', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect(await run(['-space-x-reverse', 'space-x-reverse/foo'])).toEqual('') }) test('space-y-reverse', async () => { expect(await run(['space-y-reverse'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-space-y-reverse: 0; @@ -10185,73 +8613,69 @@ test('space-y-reverse', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect(await run(['-space-y-reverse', 'space-y-reverse/foo'])).toEqual('') }) test('divide-x', async () => { - expect(await run(['divide-x', 'divide-x-0', 'divide-x-4', 'divide-x-123', 'divide-x-[4px]'])) - .toMatchInlineSnapshot(` - " - @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-divide-x-reverse: 0; - --tw-border-style: solid; - } + expect( + await compileCss( + css` + @tailwind utilities; + `, + ['divide-x', 'divide-x-4', 'divide-x-123', 'divide-x-[4px]'], + ), + ).toMatchInlineSnapshot(` + "@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-divide-x-reverse: 0; + --tw-border-style: solid; } } + } - :where(.divide-x > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-0 > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(0px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(0px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x-4 > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-4 > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x-123 > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(123px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(123px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-123 > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(123px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(123px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x-\\[4px\\] > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-\\[4px\\] > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); - } + @property --tw-divide-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; + } - @property --tw-divide-x-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; - } - - @property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; - } - " - `) + @property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; + }" + `) expect( await run([ '-divide-x', @@ -10269,18 +8693,17 @@ test('divide-x', async () => { test('divide-x with custom default border width', async () => { expect( - await run( - ['divide-x'], + await compileCss( css` @theme { --default-border-width: 2px; } @tailwind utilities; `, + ['divide-x'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-divide-x-reverse: 0; @@ -10306,78 +8729,73 @@ test('divide-x with custom default border width', async () => { syntax: "*"; inherits: false; initial-value: solid; - } - " + }" `) expect(await run(['divide-x/foo'])).toEqual('') }) test('divide-y', async () => { - expect(await run(['divide-y', 'divide-y-0', 'divide-y-4', 'divide-y-123', 'divide-y-[4px]'])) - .toMatchInlineSnapshot(` - " - @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-divide-y-reverse: 0; - --tw-border-style: solid; - } + expect( + await compileCss( + css` + @tailwind utilities; + `, + ['divide-y', 'divide-y-4', 'divide-y-123', 'divide-y-[4px]'], + ), + ).toMatchInlineSnapshot(` + "@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-divide-y-reverse: 0; + --tw-border-style: solid; } } + } - :where(.divide-y > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(1px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-0 > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(0px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(0px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y-4 > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(4px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-4 > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(4px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y-123 > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(123px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(123px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-123 > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(123px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(123px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y-\\[4px\\] > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(4px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-\\[4px\\] > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(4px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); - } + @property --tw-divide-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; + } - @property --tw-divide-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; - } - - @property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; - } - " - `) + @property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; + }" + `) expect( await run([ '-divide-y', @@ -10395,18 +8813,17 @@ test('divide-y', async () => { test('divide-y with custom default border width', async () => { expect( - await run( - ['divide-y'], + await compileCss( css` @theme { --default-border-width: 2px; } @tailwind utilities; `, + ['divide-y'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-divide-y-reverse: 0; @@ -10433,16 +8850,14 @@ test('divide-y with custom default border width', async () => { syntax: "*"; inherits: false; initial-value: solid; - } - " + }" `) expect(await run(['divide-y/foo'])).toEqual('') }) test('divide-x-reverse', async () => { expect(await run(['divide-x-reverse'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-divide-x-reverse: 0; @@ -10458,16 +8873,14 @@ test('divide-x-reverse', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect(await run(['-divide-x-reverse', 'divide-x-reverse/foo'])).toEqual('') }) test('divide-y-reverse', async () => { expect(await run(['divide-y-reverse'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-divide-y-reverse: 0; @@ -10483,8 +8896,7 @@ test('divide-y-reverse', async () => { syntax: "*"; inherits: false; initial-value: 0; - } - " + }" `) expect(await run(['-divide-y-reverse', 'divide-y-reverse/foo'])).toEqual('') }) @@ -10493,8 +8905,7 @@ test('divide-style', async () => { expect( await run(['divide-solid', 'divide-dashed', 'divide-dotted', 'divide-double', 'divide-none']), ).toMatchInlineSnapshot(` - " - :where(.divide-dashed > :not(:last-child)) { + ":where(.divide-dashed > :not(:last-child)) { --tw-border-style: dashed; border-style: dashed; } @@ -10517,8 +8928,7 @@ test('divide-style', async () => { :where(.divide-solid > :not(:last-child)) { --tw-border-style: solid; border-style: solid; - } - " + }" `) expect( await run([ @@ -10539,7 +8949,14 @@ test('divide-style', async () => { test('accent', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --accent-color-blue-500: #3b82f6; + } + @tailwind utilities; + `, [ 'accent-red-500', 'accent-red-500/50', @@ -10560,17 +8977,9 @@ test('accent', async () => { 'accent-[#0088cc]/[0.5]', 'accent-[#0088cc]/[50%]', ], - css` - @theme { - --color-red-500: #ef4444; - --accent-color-blue-500: #3b82f6; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #ef4444; --accent-color-blue-500: #3b82f6; } @@ -10687,8 +9096,7 @@ test('accent', async () => { .accent-transparent { accent-color: #0000; - } - " + }" `) expect( await run([ @@ -10729,7 +9137,14 @@ test('accent', async () => { test('caret', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --caret-color-blue-500: #3b82f6; + } + @tailwind utilities; + `, [ 'caret-red-500', 'caret-red-500/50', @@ -10750,17 +9165,9 @@ test('caret', async () => { 'caret-[#0088cc]/[0.5]', 'caret-[#0088cc]/[50%]', ], - css` - @theme { - --color-red-500: #ef4444; - --caret-color-blue-500: #3b82f6; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #ef4444; --caret-color-blue-500: #3b82f6; } @@ -10877,8 +9284,7 @@ test('caret', async () => { .caret-transparent { caret-color: #0000; - } - " + }" `) expect( await run([ @@ -10917,7 +9323,14 @@ test('caret', async () => { test('divide-color', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --border-color-best-blue: #6495ed; + } + @tailwind utilities; + `, [ 'divide-red-500', 'divide-red-500/50', @@ -10938,17 +9351,9 @@ test('divide-color', async () => { 'divide-[#0088cc]/[0.5]', 'divide-[#0088cc]/[50%]', ], - css` - @theme { - --color-red-500: #ef4444; - --border-color-best-blue: #6495ed; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #ef4444; --border-color-best-blue: #6495ed; } @@ -11065,8 +9470,7 @@ test('divide-color', async () => { :where(.divide-transparent > :not(:last-child)) { border-color: #0000; - } - " + }" `) expect( await run([ @@ -11115,8 +9519,7 @@ test('place-self', async () => { 'place-self-stretch', ]), ).toMatchInlineSnapshot(` - " - .place-self-auto { + ".place-self-auto { place-self: auto; } @@ -11142,8 +9545,7 @@ test('place-self', async () => { .place-self-stretch { place-self: stretch stretch; - } - " + }" `) expect( await run([ @@ -11176,8 +9578,7 @@ test('self', async () => { 'self-baseline-last', ]), ).toMatchInlineSnapshot(` - " - .self-auto { + ".self-auto { align-self: auto; } @@ -11211,8 +9612,7 @@ test('self', async () => { .self-stretch { align-self: stretch; - } - " + }" `) expect( await run([ @@ -11246,8 +9646,7 @@ test('justify-self', async () => { 'justify-self-baseline', ]), ).toMatchInlineSnapshot(` - " - .justify-self-auto { + ".justify-self-auto { justify-self: auto; } @@ -11273,8 +9672,7 @@ test('justify-self', async () => { .justify-self-stretch { justify-self: stretch; - } - " + }" `) expect( await run([ @@ -11305,8 +9703,7 @@ test('overflow', async () => { 'overflow-scroll', ]), ).toMatchInlineSnapshot(` - " - .overflow-auto { + ".overflow-auto { overflow: auto; } @@ -11324,8 +9721,7 @@ test('overflow', async () => { .overflow-visible { overflow: visible; - } - " + }" `) expect( await run([ @@ -11354,8 +9750,7 @@ test('overflow-x', async () => { 'overflow-x-scroll', ]), ).toMatchInlineSnapshot(` - " - .overflow-x-auto { + ".overflow-x-auto { overflow-x: auto; } @@ -11373,8 +9768,7 @@ test('overflow-x', async () => { .overflow-x-visible { overflow-x: visible; - } - " + }" `) expect( await run([ @@ -11403,8 +9797,7 @@ test('overflow-y', async () => { 'overflow-y-scroll', ]), ).toMatchInlineSnapshot(` - " - .overflow-y-auto { + ".overflow-y-auto { overflow-y: auto; } @@ -11422,8 +9815,7 @@ test('overflow-y', async () => { .overflow-y-visible { overflow-y: visible; - } - " + }" `) expect( await run([ @@ -11445,8 +9837,7 @@ test('overflow-y', async () => { test('overscroll', async () => { expect(await run(['overscroll-auto', 'overscroll-contain', 'overscroll-none'])) .toMatchInlineSnapshot(` - " - .overscroll-auto { + ".overscroll-auto { overscroll-behavior: auto; } @@ -11456,8 +9847,7 @@ test('overscroll', async () => { .overscroll-none { overscroll-behavior: none; - } - " + }" `) expect( await run([ @@ -11475,8 +9865,7 @@ test('overscroll', async () => { test('overscroll-x', async () => { expect(await run(['overscroll-x-auto', 'overscroll-x-contain', 'overscroll-x-none'])) .toMatchInlineSnapshot(` - " - .overscroll-x-auto { + ".overscroll-x-auto { overscroll-behavior-x: auto; } @@ -11486,8 +9875,7 @@ test('overscroll-x', async () => { .overscroll-x-none { overscroll-behavior-x: none; - } - " + }" `) expect( await run([ @@ -11505,8 +9893,7 @@ test('overscroll-x', async () => { test('overscroll-y', async () => { expect(await run(['overscroll-y-auto', 'overscroll-y-contain', 'overscroll-y-none'])) .toMatchInlineSnapshot(` - " - .overscroll-y-auto { + ".overscroll-y-auto { overscroll-behavior-y: auto; } @@ -11516,8 +9903,7 @@ test('overscroll-y', async () => { .overscroll-y-none { overscroll-behavior-y: none; - } - " + }" `) expect( await run([ @@ -11534,338 +9920,39 @@ test('overscroll-y', async () => { test('scroll-behavior', async () => { expect(await run(['scroll-auto', 'scroll-smooth'])).toMatchInlineSnapshot(` - " - .scroll-auto { + ".scroll-auto { scroll-behavior: auto; } .scroll-smooth { scroll-behavior: smooth; - } - " + }" `) expect( await run(['scroll', '-scroll-auto', '-scroll-smooth', 'scroll-auto/foo', 'scroll-smooth/foo']), ).toEqual('') }) -test('scrollbar-width', async () => { - expect(await run(['scrollbar-auto', 'scrollbar-thin', 'scrollbar-none'])).toMatchInlineSnapshot(` - " - .scrollbar-auto { - scrollbar-width: auto; - } - - .scrollbar-none { - scrollbar-width: none; - } - - .scrollbar-thin { - scrollbar-width: thin; - } - " - `) - expect( - await run([ - 'scrollbar', - '-scrollbar-auto', - '-scrollbar-thin', - '-scrollbar-none', - 'scrollbar-auto/foo', - 'scrollbar-thin/foo', - 'scrollbar-none/foo', - ]), - ).toEqual('') -}) - -test('scrollbar-gutter', async () => { - expect(await run(['scrollbar-gutter-auto', 'scrollbar-gutter-stable', 'scrollbar-gutter-both'])) - .toMatchInlineSnapshot(` - " - .scrollbar-gutter-auto { - scrollbar-gutter: auto; - } - - .scrollbar-gutter-both { - scrollbar-gutter: stable both-edges; - } - - .scrollbar-gutter-stable { - scrollbar-gutter: stable; - } - " - `) - expect( - await run([ - 'scrollbar-gutter', - '-scrollbar-gutter-auto', - '-scrollbar-gutter-stable', - '-scrollbar-gutter-both', - 'scrollbar-gutter-auto/foo', - 'scrollbar-gutter-stable/foo', - 'scrollbar-gutter-both/foo', - ]), - ).toEqual('') -}) - -test('scrollbar-thumb', async () => { - expect( - await run( - [ - 'scrollbar-thumb-red-500', - 'scrollbar-thumb-red-500/50', - 'scrollbar-thumb-red-500/[0.5]', - 'scrollbar-thumb-current', - 'scrollbar-thumb-inherit', - 'scrollbar-thumb-transparent', - 'scrollbar-thumb-[#0088cc]', - 'scrollbar-thumb-[#0088cc]/50', - ], - css` - @theme { - --color-red-500: #ef4444; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - @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-scrollbar-thumb: #0000; - --tw-scrollbar-track: #0000; - } - } - } - - :root, :host { - --color-red-500: #ef4444; - } - - .scrollbar-thumb-\\[\\#0088cc\\] { - --tw-scrollbar-thumb: #08c; - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-thumb-\\[\\#0088cc\\]\\/50 { - --tw-scrollbar-thumb: oklab(59.9824% -.067 -.124 / .5); - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-thumb-current { - --tw-scrollbar-thumb: currentcolor; - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-thumb-inherit { - --tw-scrollbar-thumb: inherit; - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-thumb-red-500 { - --tw-scrollbar-thumb: var(--color-red-500); - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-thumb-red-500\\/50 { - --tw-scrollbar-thumb: #ef444480; - } - - @supports (color: color-mix(in lab, red, red)) { - .scrollbar-thumb-red-500\\/50 { - --tw-scrollbar-thumb: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } - } - - .scrollbar-thumb-red-500\\/50 { - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-thumb-red-500\\/\\[0\\.5\\] { - --tw-scrollbar-thumb: #ef444480; - } - - @supports (color: color-mix(in lab, red, red)) { - .scrollbar-thumb-red-500\\/\\[0\\.5\\] { - --tw-scrollbar-thumb: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } - } - - .scrollbar-thumb-red-500\\/\\[0\\.5\\] { - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-thumb-transparent { - --tw-scrollbar-thumb: transparent; - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - @property --tw-scrollbar-thumb { - syntax: ""; - inherits: false; - initial-value: #0000; - } - - @property --tw-scrollbar-track { - syntax: ""; - inherits: false; - initial-value: #0000; - } - " - `) - expect( - await run([ - 'scrollbar-thumb', - '-scrollbar-thumb-red-500', - '-scrollbar-thumb-red-500/50', - 'scrollbar-thumb-red-500/foo', - 'scrollbar-thumb-[#0088cc]/foo', - ]), - ).toEqual('') -}) - -test('scrollbar-track', async () => { - expect( - await run( - [ - 'scrollbar-track-red-500', - 'scrollbar-track-red-500/50', - 'scrollbar-track-red-500/[0.5]', - 'scrollbar-track-current', - 'scrollbar-track-inherit', - 'scrollbar-track-transparent', - 'scrollbar-track-[#0088cc]', - 'scrollbar-track-[#0088cc]/50', - ], - css` - @theme { - --color-red-500: #ef4444; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - @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-scrollbar-thumb: #0000; - --tw-scrollbar-track: #0000; - } - } - } - - :root, :host { - --color-red-500: #ef4444; - } - - .scrollbar-track-\\[\\#0088cc\\] { - --tw-scrollbar-track: #08c; - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-track-\\[\\#0088cc\\]\\/50 { - --tw-scrollbar-track: oklab(59.9824% -.067 -.124 / .5); - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-track-current { - --tw-scrollbar-track: currentcolor; - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-track-inherit { - --tw-scrollbar-track: inherit; - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-track-red-500 { - --tw-scrollbar-track: var(--color-red-500); - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-track-red-500\\/50 { - --tw-scrollbar-track: #ef444480; - } - - @supports (color: color-mix(in lab, red, red)) { - .scrollbar-track-red-500\\/50 { - --tw-scrollbar-track: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } - } - - .scrollbar-track-red-500\\/50 { - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-track-red-500\\/\\[0\\.5\\] { - --tw-scrollbar-track: #ef444480; - } - - @supports (color: color-mix(in lab, red, red)) { - .scrollbar-track-red-500\\/\\[0\\.5\\] { - --tw-scrollbar-track: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } - } - - .scrollbar-track-red-500\\/\\[0\\.5\\] { - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - .scrollbar-track-transparent { - --tw-scrollbar-track: transparent; - scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); - } - - @property --tw-scrollbar-thumb { - syntax: ""; - inherits: false; - initial-value: #0000; - } - - @property --tw-scrollbar-track { - syntax: ""; - inherits: false; - initial-value: #0000; - } - " - `) - expect( - await run([ - 'scrollbar-track', - '-scrollbar-track-red-500', - '-scrollbar-track-red-500/50', - 'scrollbar-track-red-500/foo', - 'scrollbar-track-[#0088cc]/foo', - ]), - ).toEqual('') -}) - test('truncate', async () => { expect(await run(['truncate'])).toMatchInlineSnapshot(` - " - .truncate { + ".truncate { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - } - " + }" `) expect(await run(['-truncate', 'truncate/foo'])).toEqual('') }) test('text-overflow', async () => { expect(await run(['text-ellipsis', 'text-clip'])).toMatchInlineSnapshot(` - " - .text-clip { + ".text-clip { text-overflow: clip; } .text-ellipsis { text-overflow: ellipsis; - } - " + }" `) expect(await run(['-text-ellipsis', '-text-clip', 'text-ellipsis/foo', 'text-clip/foo'])).toEqual( '', @@ -11874,8 +9961,7 @@ test('text-overflow', async () => { test('hyphens', async () => { expect(await run(['hyphens-none', 'hyphens-manual', 'hyphens-auto'])).toMatchInlineSnapshot(` - " - .hyphens-auto { + ".hyphens-auto { -webkit-hyphens: auto; hyphens: auto; } @@ -11888,8 +9974,7 @@ test('hyphens', async () => { .hyphens-none { -webkit-hyphens: none; hyphens: none; - } - " + }" `) expect( await run([ @@ -11915,8 +10000,7 @@ test('whitespace', async () => { 'whitespace-break-spaces', ]), ).toMatchInlineSnapshot(` - " - .whitespace-break-spaces { + ".whitespace-break-spaces { white-space: break-spaces; } @@ -11938,8 +10022,7 @@ test('whitespace', async () => { .whitespace-pre-wrap { white-space: pre-wrap; - } - " + }" `) expect( await run([ @@ -11960,44 +10043,10 @@ test('whitespace', async () => { ).toEqual('') }) -test('tab', async () => { - expect(await run(['tab-2', 'tab-8', 'tab-[12px]', 'tab-[3]'])).toMatchInlineSnapshot(` - " - .tab-2 { - tab-size: 2; - } - - .tab-8 { - tab-size: 8; - } - - .tab-\\[3\\] { - tab-size: 3; - } - - .tab-\\[12px\\] { - tab-size: 12px; - } - " - `) - expect( - await run([ - 'tab', - '-tab-2', - 'tab-2.5', - 'tab-1/2', - 'tab-unknown', - 'tab-2/foo', - 'tab-[12px]/foo', - ]), - ).toEqual('') -}) - test('text-wrap', async () => { expect(await run(['text-wrap', 'text-nowrap', 'text-balance', 'text-pretty'])) .toMatchInlineSnapshot(` - " - .text-balance { + ".text-balance { text-wrap: balance; } @@ -12011,8 +10060,7 @@ test('text-wrap', async () => { .text-wrap { text-wrap: wrap; - } - " + }" `) expect( await run([ @@ -12031,8 +10079,7 @@ test('text-wrap', async () => { test('word-break', async () => { expect(await run(['break-normal', 'break-words', 'break-all', 'break-keep'])) .toMatchInlineSnapshot(` - " - .break-normal { + ".break-normal { overflow-wrap: normal; word-break: normal; } @@ -12047,8 +10094,7 @@ test('word-break', async () => { .break-keep { word-break: keep-all; - } - " + }" `) expect( await run([ @@ -12066,8 +10112,7 @@ test('word-break', async () => { test('overflow-wrap', async () => { expect(await run(['wrap-anywhere', 'wrap-break-word', 'wrap-normal'])).toMatchInlineSnapshot(` - " - .wrap-anywhere { + ".wrap-anywhere { overflow-wrap: anywhere; } @@ -12077,8 +10122,7 @@ test('overflow-wrap', async () => { .wrap-normal { overflow-wrap: normal; - } - " + }" `) expect( await run([ @@ -12094,8 +10138,7 @@ test('overflow-wrap', async () => { test('rounded', async () => { expect( - await run( - ['rounded', 'rounded-full', 'rounded-none', 'rounded-sm', 'rounded-[4px]'], + await compileCss( css` @theme { --radius-sm: 0.125rem; @@ -12103,10 +10146,10 @@ test('rounded', async () => { } @tailwind utilities; `, + ['rounded', 'rounded-full', 'rounded-none', 'rounded-sm', 'rounded-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-sm: .125rem; --radius: .25rem; } @@ -12120,7 +10163,7 @@ test('rounded', async () => { } .rounded-full { - border-radius: 2147483647px; + border-radius: 3.40282e38px; } .rounded-none { @@ -12129,60 +10172,46 @@ test('rounded', async () => { .rounded-sm { border-radius: var(--radius-sm); - } - " + }" `) expect( - await run( - ['rounded-full'], + await compileCss( css` @theme { --radius-full: 99999px; } @tailwind utilities; `, + ['rounded-full'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-full: 99999px; } .rounded-full { border-radius: var(--radius-full); - } - " + }" `) expect( - await run( - [ - '-rounded', - '-rounded-full', - '-rounded-none', - '-rounded-sm', - '-rounded-[4px]', - 'rounded/foo', - 'rounded-full/foo', - 'rounded-none/foo', - 'rounded-sm/foo', - 'rounded-[4px]/foo', - 'rounded-sm/[5]', - 'rounded-sm/(--foo)', - ], - css` - @theme { - --radius-sm: 0.125rem; - } - @tailwind utilities; - `, - ), + await run([ + '-rounded', + '-rounded-full', + '-rounded-none', + '-rounded-sm', + '-rounded-[4px]', + 'rounded/foo', + 'rounded-full/foo', + 'rounded-none/foo', + 'rounded-sm/foo', + 'rounded-[4px]/foo', + ]), ).toEqual('') }) test('rounded-s', async () => { expect( - await run( - ['rounded-s', 'rounded-s-full', 'rounded-s-none', 'rounded-s-sm', 'rounded-s-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12192,10 +10221,10 @@ test('rounded-s', async () => { } @tailwind utilities; `, + ['rounded-s', 'rounded-s-full', 'rounded-s-none', 'rounded-s-sm', 'rounded-s-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12225,8 +10254,7 @@ test('rounded-s', async () => { .rounded-s-sm { border-start-start-radius: var(--radius-sm); border-end-start-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12246,8 +10274,7 @@ test('rounded-s', async () => { test('rounded-e', async () => { expect( - await run( - ['rounded-e', 'rounded-e-full', 'rounded-e-none', 'rounded-e-sm', 'rounded-e-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12257,10 +10284,10 @@ test('rounded-e', async () => { } @tailwind utilities; `, + ['rounded-e', 'rounded-e-full', 'rounded-e-none', 'rounded-e-sm', 'rounded-e-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12290,8 +10317,7 @@ test('rounded-e', async () => { .rounded-e-sm { border-start-end-radius: var(--radius-sm); border-end-end-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12311,8 +10337,7 @@ test('rounded-e', async () => { test('rounded-t', async () => { expect( - await run( - ['rounded-t', 'rounded-t-full', 'rounded-t-none', 'rounded-t-sm', 'rounded-t-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12322,10 +10347,10 @@ test('rounded-t', async () => { } @tailwind utilities; `, + ['rounded-t', 'rounded-t-full', 'rounded-t-none', 'rounded-t-sm', 'rounded-t-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12355,8 +10380,7 @@ test('rounded-t', async () => { .rounded-t-sm { border-top-left-radius: var(--radius-sm); border-top-right-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12376,8 +10400,7 @@ test('rounded-t', async () => { test('rounded-r', async () => { expect( - await run( - ['rounded-r', 'rounded-r-full', 'rounded-r-none', 'rounded-r-sm', 'rounded-r-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12387,10 +10410,10 @@ test('rounded-r', async () => { } @tailwind utilities; `, + ['rounded-r', 'rounded-r-full', 'rounded-r-none', 'rounded-r-sm', 'rounded-r-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12420,8 +10443,7 @@ test('rounded-r', async () => { .rounded-r-sm { border-top-right-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12441,8 +10463,7 @@ test('rounded-r', async () => { test('rounded-b', async () => { expect( - await run( - ['rounded-b', 'rounded-b-full', 'rounded-b-none', 'rounded-b-sm', 'rounded-b-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12452,10 +10473,10 @@ test('rounded-b', async () => { } @tailwind utilities; `, + ['rounded-b', 'rounded-b-full', 'rounded-b-none', 'rounded-b-sm', 'rounded-b-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12485,8 +10506,7 @@ test('rounded-b', async () => { .rounded-b-sm { border-bottom-right-radius: var(--radius-sm); border-bottom-left-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12506,8 +10526,7 @@ test('rounded-b', async () => { test('rounded-l', async () => { expect( - await run( - ['rounded-l', 'rounded-l-full', 'rounded-l-none', 'rounded-l-sm', 'rounded-l-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12517,10 +10536,10 @@ test('rounded-l', async () => { } @tailwind utilities; `, + ['rounded-l', 'rounded-l-full', 'rounded-l-none', 'rounded-l-sm', 'rounded-l-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12550,8 +10569,7 @@ test('rounded-l', async () => { .rounded-l-sm { border-top-left-radius: var(--radius-sm); border-bottom-left-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12571,8 +10589,7 @@ test('rounded-l', async () => { test('rounded-ss', async () => { expect( - await run( - ['rounded-ss', 'rounded-ss-full', 'rounded-ss-none', 'rounded-ss-sm', 'rounded-ss-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12582,10 +10599,10 @@ test('rounded-ss', async () => { } @tailwind utilities; `, + ['rounded-ss', 'rounded-ss-full', 'rounded-ss-none', 'rounded-ss-sm', 'rounded-ss-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12610,8 +10627,7 @@ test('rounded-ss', async () => { .rounded-ss-sm { border-start-start-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12631,8 +10647,7 @@ test('rounded-ss', async () => { test('rounded-se', async () => { expect( - await run( - ['rounded-se', 'rounded-se-full', 'rounded-se-none', 'rounded-se-sm', 'rounded-se-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12642,10 +10657,10 @@ test('rounded-se', async () => { } @tailwind utilities; `, + ['rounded-se', 'rounded-se-full', 'rounded-se-none', 'rounded-se-sm', 'rounded-se-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12670,8 +10685,7 @@ test('rounded-se', async () => { .rounded-se-sm { border-start-end-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12691,8 +10705,7 @@ test('rounded-se', async () => { test('rounded-ee', async () => { expect( - await run( - ['rounded-ee', 'rounded-ee-full', 'rounded-ee-none', 'rounded-ee-sm', 'rounded-ee-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12702,10 +10715,10 @@ test('rounded-ee', async () => { } @tailwind utilities; `, + ['rounded-ee', 'rounded-ee-full', 'rounded-ee-none', 'rounded-ee-sm', 'rounded-ee-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12730,8 +10743,7 @@ test('rounded-ee', async () => { .rounded-ee-sm { border-end-end-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12751,8 +10763,7 @@ test('rounded-ee', async () => { test('rounded-es', async () => { expect( - await run( - ['rounded-es', 'rounded-es-full', 'rounded-es-none', 'rounded-es-sm', 'rounded-es-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12762,10 +10773,10 @@ test('rounded-es', async () => { } @tailwind utilities; `, + ['rounded-es', 'rounded-es-full', 'rounded-es-none', 'rounded-es-sm', 'rounded-es-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12790,8 +10801,7 @@ test('rounded-es', async () => { .rounded-es-sm { border-end-start-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12811,8 +10821,7 @@ test('rounded-es', async () => { test('rounded-tl', async () => { expect( - await run( - ['rounded-tl', 'rounded-tl-full', 'rounded-tl-none', 'rounded-tl-sm', 'rounded-tl-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12822,10 +10831,10 @@ test('rounded-tl', async () => { } @tailwind utilities; `, + ['rounded-tl', 'rounded-tl-full', 'rounded-tl-none', 'rounded-tl-sm', 'rounded-tl-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12850,8 +10859,7 @@ test('rounded-tl', async () => { .rounded-tl-sm { border-top-left-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12871,8 +10879,7 @@ test('rounded-tl', async () => { test('rounded-tr', async () => { expect( - await run( - ['rounded-tr', 'rounded-tr-full', 'rounded-tr-none', 'rounded-tr-sm', 'rounded-tr-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12882,10 +10889,10 @@ test('rounded-tr', async () => { } @tailwind utilities; `, + ['rounded-tr', 'rounded-tr-full', 'rounded-tr-none', 'rounded-tr-sm', 'rounded-tr-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12910,8 +10917,7 @@ test('rounded-tr', async () => { .rounded-tr-sm { border-top-right-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12931,8 +10937,7 @@ test('rounded-tr', async () => { test('rounded-br', async () => { expect( - await run( - ['rounded-br', 'rounded-br-full', 'rounded-br-none', 'rounded-br-sm', 'rounded-br-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -12942,10 +10947,10 @@ test('rounded-br', async () => { } @tailwind utilities; `, + ['rounded-br', 'rounded-br-full', 'rounded-br-none', 'rounded-br-sm', 'rounded-br-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -12970,8 +10975,7 @@ test('rounded-br', async () => { .rounded-br-sm { border-bottom-right-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -12991,8 +10995,7 @@ test('rounded-br', async () => { test('rounded-bl', async () => { expect( - await run( - ['rounded-bl', 'rounded-bl-full', 'rounded-bl-none', 'rounded-bl-sm', 'rounded-bl-[4px]'], + await compileCss( css` @theme { --radius-none: 0px; @@ -13002,10 +11005,10 @@ test('rounded-bl', async () => { } @tailwind utilities; `, + ['rounded-bl', 'rounded-bl-full', 'rounded-bl-none', 'rounded-bl-sm', 'rounded-bl-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --radius-none: 0px; --radius-full: 9999px; --radius-sm: .125rem; @@ -13030,8 +11033,7 @@ test('rounded-bl', async () => { .rounded-bl-sm { border-bottom-left-radius: var(--radius-sm); - } - " + }" `) expect( await run([ @@ -13060,8 +11062,7 @@ test('border-style', async () => { 'border-none', ]), ).toMatchInlineSnapshot(` - " - .border-dashed { + ".border-dashed { --tw-border-style: dashed; border-style: dashed; } @@ -13089,8 +11090,7 @@ test('border-style', async () => { .border-solid { --tw-border-style: solid; border-style: solid; - } - " + }" `) expect( await run([ @@ -13118,8 +11118,6 @@ const prefixes = [ 'border-y', 'border-s', 'border-e', - 'border-bs', - 'border-be', 'border-t', 'border-r', 'border-b', @@ -13170,8 +11168,7 @@ for (let prefix of prefixes) { classes.push(`${prefix}-[color:var(--my-color)]/50`) expect( - await run( - classes, + await compileCss( css` @theme { --radius-none: 0px; @@ -13182,6 +11179,7 @@ for (let prefix of prefixes) { } @tailwind utilities; `, + classes, ), ).toMatchSnapshot() @@ -13207,18 +11205,17 @@ for (let prefix of prefixes) { test('border with custom default border width', async () => { expect( - await run( - ['border'], + await compileCss( css` @theme { --default-border-width: 2px; } @tailwind utilities; `, + ['border'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-border-style: solid; @@ -13235,15 +11232,21 @@ test('border with custom default border width', async () => { syntax: "*"; inherits: false; initial-value: solid; - } - " + }" `) expect(await run(['-border', 'border/foo'])).toEqual('') }) test('bg', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --background-color-blue-500: #3b82f6; + } + @tailwind utilities; + `, [ // background-color 'bg-red-500', @@ -13385,17 +11388,9 @@ test('bg', async () => { 'bg-repeat-round', 'bg-repeat-space', ], - css` - @theme { - --color-red-500: #ef4444; - --background-color-blue-500: #3b82f6; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #ef4444; --background-color-blue-500: #3b82f6; } @@ -14210,8 +12205,7 @@ test('bg', async () => { .bg-repeat-y { background-repeat: repeat-y; - } - " + }" `) expect( await run([ @@ -14309,8 +12303,7 @@ test('bg', async () => { ).toEqual('') expect( - await run( - ['bg-current/half', 'bg-current/custom', '[color:red]/half'], + await compileCss( css` @theme reference { --opacity-half: 0.5; @@ -14318,10 +12311,10 @@ test('bg', async () => { } @tailwind utilities; `, + ['bg-current/half', 'bg-current/custom', '[color:red]/half'], ), ).toMatchInlineSnapshot(` - " - .bg-current\\/custom { + ".bg-current\\/custom { background-color: currentColor; } @@ -14349,25 +12342,23 @@ test('bg', async () => { .\\[color\\:red\\]\\/half { color: color-mix(in oklab, red var(--opacity-half, .5), transparent); } - } - " + }" `) }) test('bg-position', async () => { expect( - await run( - ['bg-position-[120px]', 'bg-position-[120px_120px]', 'bg-position-[var(--some-var)]'], + await compileCss( css` @theme { --color-red-500: #ef4444; } @tailwind utilities; `, + ['bg-position-[120px]', 'bg-position-[120px_120px]', 'bg-position-[var(--some-var)]'], ), ).toMatchInlineSnapshot(` - " - .bg-position-\\[120px\\] { + ".bg-position-\\[120px\\] { background-position: 120px; } @@ -14377,8 +12368,7 @@ test('bg-position', async () => { .bg-position-\\[var\\(--some-var\\)\\] { background-position: var(--some-var); - } - " + }" `) expect( await run([ @@ -14397,18 +12387,17 @@ test('bg-position', async () => { test('bg-size', async () => { expect( - await run( - ['bg-size-[120px]', 'bg-size-[120px_120px]', 'bg-size-[var(--some-var)]'], + await compileCss( css` @theme { --color-red-500: #ef4444; } @tailwind utilities; `, + ['bg-size-[120px]', 'bg-size-[120px_120px]', 'bg-size-[var(--some-var)]'], ), ).toMatchInlineSnapshot(` - " - .bg-size-\\[120px\\] { + ".bg-size-\\[120px\\] { background-size: 120px; } @@ -14418,8 +12407,7 @@ test('bg-size', async () => { .bg-size-\\[var\\(--some-var\\)\\] { background-size: var(--some-var); - } - " + }" `) expect( await run([ @@ -14438,7 +12426,13 @@ test('bg-size', async () => { test('from', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + } + @tailwind utilities; + `, [ // --tw-gradient-from 'from-red-500', @@ -14473,16 +12467,9 @@ test('from', async () => { 'from-[length:var(--my-position)]', 'from-[percentage:var(--my-position)]', ], - css` - @theme { - --color-red-500: #ef4444; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-gradient-position: initial; @@ -14783,8 +12770,7 @@ test('from', async () => { syntax: ""; inherits: false; initial-value: 100%; - } - " + }" `) expect( await run([ @@ -14823,7 +12809,13 @@ test('from', async () => { test('via', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + } + @tailwind utilities; + `, [ // --tw-gradient-stops 'via-red-500', @@ -14858,16 +12850,9 @@ test('via', async () => { 'via-[length:var(--my-position)]', 'via-[percentage:var(--my-position)]', ], - css` - @theme { - --color-red-500: #ef4444; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-gradient-position: initial; @@ -15188,8 +13173,7 @@ test('via', async () => { syntax: ""; inherits: false; initial-value: 100%; - } - " + }" `) expect( await run([ @@ -15226,7 +13210,13 @@ test('via', async () => { test('to', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + } + @tailwind utilities; + `, [ // --tw-gradient-to 'to-red-500', @@ -15261,16 +13251,9 @@ test('to', async () => { 'to-[length:var(--my-position)]', 'to-[percentage:var(--my-position)]', ], - css` - @theme { - --color-red-500: #ef4444; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-gradient-position: initial; @@ -15571,8 +13554,7 @@ test('to', async () => { syntax: ""; inherits: false; initial-value: 100%; - } - " + }" `) expect( await run([ @@ -15609,7 +13591,13 @@ test('to', async () => { test('mask', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + } + @tailwind utilities; + `, [ // mask-image 'mask-none', @@ -15667,16 +13655,9 @@ test('mask', async () => { 'mask-repeat-round', 'mask-repeat-space', ], - css` - @theme { - --color-red-500: #ef4444; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - .mask-\\[image\\:var\\(--some-var\\)\\] { + ".mask-\\[image\\:var\\(--some-var\\)\\] { -webkit-mask-image: var(--some-var); -webkit-mask-image: var(--some-var); mask-image: var(--some-var); @@ -15871,8 +13852,7 @@ test('mask', async () => { .mask-repeat-y { -webkit-mask-repeat: repeat-y; mask-repeat: repeat-y; - } - " + }" `) expect( await run([ @@ -15962,8 +13942,7 @@ test('mask', async () => { ).toEqual('') expect( - await run( - ['mask-current/half', 'mask-current/custom', '[color:red]/half'], + await compileCss( css` @theme reference { --opacity-half: 0.5; @@ -15971,10 +13950,10 @@ test('mask', async () => { } @tailwind utilities; `, + ['mask-current/half', 'mask-current/custom', '[color:red]/half'], ), ).toMatchInlineSnapshot(` - " - .\\[color\\:red\\]\\/half { + ".\\[color\\:red\\]\\/half { color: color-mix(in srgb, red .5, transparent); } @@ -15982,25 +13961,23 @@ test('mask', async () => { .\\[color\\:red\\]\\/half { color: color-mix(in oklab, red var(--opacity-half, .5), transparent); } - } - " + }" `) }) test('mask-position', async () => { expect( - await run( - ['mask-position-[120px]', 'mask-position-[120px_120px]', 'mask-position-[var(--some-var)]'], + await compileCss( css` @theme { --color-red-500: #ef4444; } @tailwind utilities; `, + ['mask-position-[120px]', 'mask-position-[120px_120px]', 'mask-position-[var(--some-var)]'], ), ).toMatchInlineSnapshot(` - " - .mask-position-\\[120px\\] { + ".mask-position-\\[120px\\] { -webkit-mask-position: 120px; mask-position: 120px; } @@ -16014,8 +13991,7 @@ test('mask-position', async () => { -webkit-mask-position: var(--some-var); -webkit-mask-position: var(--some-var); mask-position: var(--some-var); - } - " + }" `) expect( await run([ @@ -16034,18 +14010,17 @@ test('mask-position', async () => { test('mask-size', async () => { expect( - await run( - ['mask-size-[120px]', 'mask-size-[120px_120px]', 'mask-size-[var(--some-var)]'], + await compileCss( css` @theme { --color-red-500: #ef4444; } @tailwind utilities; `, + ['mask-size-[120px]', 'mask-size-[120px_120px]', 'mask-size-[var(--some-var)]'], ), ).toMatchInlineSnapshot(` - " - .mask-size-\\[120px\\] { + ".mask-size-\\[120px\\] { -webkit-mask-size: 120px; mask-size: 120px; } @@ -16059,8 +14034,7 @@ test('mask-size', async () => { -webkit-mask-size: var(--some-var); -webkit-mask-size: var(--some-var); mask-size: var(--some-var); - } - " + }" `) expect( await run([ @@ -16078,15 +14052,14 @@ test('mask-size', async () => { }) test('mask-t-from', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-t-from-0', 'mask-t-from-1.5', @@ -16099,11 +14072,9 @@ test('mask-t-from', async () => { 'mask-t-from-(color:--my-var)', 'mask-t-from-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -16155,7 +14126,7 @@ test('mask-t-from', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position)); - --tw-mask-top-from-position: 0px; + --tw-mask-top-from-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -16297,11 +14268,16 @@ test('mask-t-from', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-t-from', 'mask-t-from-2.8175', @@ -16332,21 +14308,19 @@ test('mask-t-from', async () => { '-mask-l-from-[25%]/foo', '-mask-l-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-t-to', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-t-to-0', 'mask-t-to-1.5', @@ -16359,11 +14333,9 @@ test('mask-t-to', async () => { 'mask-t-to-(color:--my-var)', 'mask-t-to-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -16415,7 +14387,7 @@ test('mask-t-to', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position)); - --tw-mask-top-to-position: 0px; + --tw-mask-top-to-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -16557,11 +14529,16 @@ test('mask-t-to', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-t-to', 'mask-t-to-2.8175', @@ -16592,21 +14569,19 @@ test('mask-t-to', async () => { '-mask-l-from-[25%]/foo', '-mask-l-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-r-from', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-r-from-0', 'mask-r-from-1.5', @@ -16620,11 +14595,9 @@ test('mask-r-from', async () => { 'mask-r-from-(color:--my-var)', 'mask-r-from-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -16676,7 +14649,7 @@ test('mask-r-from', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position)); - --tw-mask-right-from-position: 0px; + --tw-mask-right-from-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -16818,11 +14791,16 @@ test('mask-r-from', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-r-from', 'mask-r-from-2.8175', @@ -16853,21 +14831,19 @@ test('mask-r-from', async () => { '-mask-r-from-[25%]/foo', '-mask-r-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-r-to', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-r-to-0', 'mask-r-to-1.5', @@ -16881,11 +14857,9 @@ test('mask-r-to', async () => { 'mask-r-to-(color:--my-var)', 'mask-r-to-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -16937,7 +14911,7 @@ test('mask-r-to', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position)); - --tw-mask-right-to-position: 0px; + --tw-mask-right-to-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -17079,11 +15053,16 @@ test('mask-r-to', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-r-to', 'mask-r-to-2.8175', @@ -17114,21 +15093,19 @@ test('mask-r-to', async () => { '-mask-r-to-[25%]/foo', '-mask-r-to-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-b-from', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-b-from-0', 'mask-b-from-1.5', @@ -17142,11 +15119,9 @@ test('mask-b-from', async () => { 'mask-b-from-(color:--my-var)', 'mask-b-from-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -17198,7 +15173,7 @@ test('mask-b-from', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position)); - --tw-mask-bottom-from-position: 0px; + --tw-mask-bottom-from-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -17340,11 +15315,16 @@ test('mask-b-from', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-b-from', 'mask-b-from-2.8175', @@ -17375,21 +15355,19 @@ test('mask-b-from', async () => { '-mask-b-from-[25%]/foo', '-mask-b-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-b-to', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-b-to-0', 'mask-b-to-1.5', @@ -17403,11 +15381,9 @@ test('mask-b-to', async () => { 'mask-b-to-(color:--my-var)', 'mask-b-to-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -17459,7 +15435,7 @@ test('mask-b-to', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position)); - --tw-mask-bottom-to-position: 0px; + --tw-mask-bottom-to-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -17601,11 +15577,16 @@ test('mask-b-to', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-b-to', 'mask-b-to-2.8175', @@ -17636,21 +15617,19 @@ test('mask-b-to', async () => { '-mask-b-to-[25%]/foo', '-mask-b-to-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-l-from', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-l-from-0', 'mask-l-from-1.5', @@ -17664,11 +15643,9 @@ test('mask-l-from', async () => { 'mask-l-from-(color:--my-var)', 'mask-l-from-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -17720,7 +15697,7 @@ test('mask-l-from', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position)); - --tw-mask-left-from-position: 0px; + --tw-mask-left-from-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -17862,11 +15839,16 @@ test('mask-l-from', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-l-from', 'mask-l-from-2.8175', @@ -17897,21 +15879,19 @@ test('mask-l-from', async () => { '-mask-l-from-[25%]/foo', '-mask-l-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-l-to', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-l-to-0', 'mask-l-to-1.5', @@ -17925,11 +15905,9 @@ test('mask-l-to', async () => { 'mask-l-to-(color:--my-var)', 'mask-l-to-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -17981,7 +15959,7 @@ test('mask-l-to', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position)); - --tw-mask-left-to-position: 0px; + --tw-mask-left-to-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -18123,11 +16101,16 @@ test('mask-l-to', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-l-to', 'mask-l-to-2.8175', @@ -18158,21 +16141,19 @@ test('mask-l-to', async () => { '-mask-l-to-[25%]/foo', '-mask-l-to-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-x-from', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-x-from-0', 'mask-x-from-1.5', @@ -18186,11 +16167,9 @@ test('mask-x-from', async () => { 'mask-x-from-(color:--my-var)', 'mask-x-from-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -18250,9 +16229,9 @@ test('mask-x-from', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position)); - --tw-mask-right-from-position: 0px; + --tw-mask-right-from-position: calc(var(--spacing) * 0); --tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position)); - --tw-mask-left-from-position: 0px; + --tw-mask-left-from-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -18430,11 +16409,16 @@ test('mask-x-from', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-x-from', 'mask-x-from-2.8175', @@ -18465,21 +16449,19 @@ test('mask-x-from', async () => { '-mask-x-from-[25%]/foo', '-mask-x-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-x-to', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-x-to-0', 'mask-x-to-1.5', @@ -18493,11 +16475,9 @@ test('mask-x-to', async () => { 'mask-x-to-(color:--my-var)', 'mask-x-to-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -18557,9 +16537,9 @@ test('mask-x-to', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position)); - --tw-mask-right-to-position: 0px; + --tw-mask-right-to-position: calc(var(--spacing) * 0); --tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position)); - --tw-mask-left-to-position: 0px; + --tw-mask-left-to-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -18737,11 +16717,16 @@ test('mask-x-to', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-x-to', 'mask-x-to-2.8175', @@ -18772,21 +16757,19 @@ test('mask-x-to', async () => { '-mask-x-to-[25%]/foo', '-mask-x-to-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-y-from', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-y-from-0', 'mask-y-from-1.5', @@ -18800,11 +16783,9 @@ test('mask-y-from', async () => { 'mask-y-from-(color:--my-var)', 'mask-y-from-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -18864,9 +16845,9 @@ test('mask-y-from', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position)); - --tw-mask-top-from-position: 0px; + --tw-mask-top-from-position: calc(var(--spacing) * 0); --tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position)); - --tw-mask-bottom-from-position: 0px; + --tw-mask-bottom-from-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -19044,11 +17025,16 @@ test('mask-y-from', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-y-from', 'mask-y-from-2.8175', @@ -19079,21 +17065,19 @@ test('mask-y-from', async () => { '-mask-y-from-[25%]/foo', '-mask-y-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-y-to', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-y-to-0', 'mask-y-to-1.5', @@ -19107,11 +17091,9 @@ test('mask-y-to', async () => { 'mask-y-to-(color:--my-var)', 'mask-y-to-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -19171,9 +17153,9 @@ test('mask-y-to', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top); --tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position)); - --tw-mask-top-to-position: 0px; + --tw-mask-top-to-position: calc(var(--spacing) * 0); --tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position)); - --tw-mask-bottom-to-position: 0px; + --tw-mask-bottom-to-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -19351,11 +17333,16 @@ test('mask-y-to', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-y-to', 'mask-y-to-2.8175', @@ -19386,24 +17373,20 @@ test('mask-y-to', async () => { '-mask-y-to-[25%]/foo', '-mask-y-to-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-linear', async () => { expect( - await run([ - 'mask-linear-0', - 'mask-linear-1', - '-mask-linear-1', - 'mask-linear-45', - 'mask-linear-[3rad]', - '-mask-linear-45', - ]), + await compileCss( + css` + @tailwind utilities; + `, + ['mask-linear-45', 'mask-linear-[3rad]', '-mask-linear-45'], + ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -19418,17 +17401,6 @@ test('mask-linear', async () => { } } - .-mask-linear-1 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); - --tw-mask-linear-position: -1deg; - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } - .-mask-linear-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -19440,28 +17412,6 @@ test('mask-linear', async () => { mask-composite: intersect; } - .mask-linear-0 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); - --tw-mask-linear-position: 0deg; - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } - - .mask-linear-1 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); - --tw-mask-linear-position: 1deg; - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } - .mask-linear-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -19530,8 +17480,7 @@ test('mask-linear', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( await run([ @@ -19553,15 +17502,14 @@ test('mask-linear', async () => { }) test('mask-linear-from', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-linear-from-0', 'mask-linear-from-1.5', @@ -19575,11 +17523,9 @@ test('mask-linear-from', async () => { 'mask-linear-from-(color:--my-var)', 'mask-linear-from-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -19628,7 +17574,7 @@ test('mask-linear-from', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear-stops: var(--tw-mask-linear-position), var(--tw-mask-linear-from-color) var(--tw-mask-linear-from-position), var(--tw-mask-linear-to-color) var(--tw-mask-linear-to-position); --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops)); - --tw-mask-linear-from-position: 0px; + --tw-mask-linear-from-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -19752,11 +17698,16 @@ test('mask-linear-from', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-linear-from', 'mask-linear-from-2.8175', @@ -19787,21 +17738,19 @@ test('mask-linear-from', async () => { '-mask-linear-from-[25%]/foo', '-mask-linear-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-linear-to', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-linear-to-0', 'mask-linear-to-1.5', @@ -19815,11 +17764,9 @@ test('mask-linear-to', async () => { 'mask-linear-to-(color:--my-var)', 'mask-linear-to-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -19868,7 +17815,7 @@ test('mask-linear-to', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-linear-stops: var(--tw-mask-linear-position), var(--tw-mask-linear-from-color) var(--tw-mask-linear-from-position), var(--tw-mask-linear-to-color) var(--tw-mask-linear-to-position); --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops)); - --tw-mask-linear-to-position: 0px; + --tw-mask-linear-to-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -19992,11 +17939,16 @@ test('mask-linear-to', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-linear-to', 'mask-linear-to-2.8175', @@ -20027,25 +17979,28 @@ test('mask-linear-to', async () => { '-mask-linear-to-[25%]/foo', '-mask-linear-to-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-radial', async () => { expect( - await run([ - 'mask-circle', - 'mask-ellipse', - 'mask-radial-closest-side', - 'mask-radial-farthest-side', - 'mask-radial-closest-corner', - 'mask-radial-farthest-corner', - 'mask-radial-[25%_25%]', - ]), + await compileCss( + css` + @tailwind utilities; + `, + [ + 'mask-circle', + 'mask-ellipse', + 'mask-radial-closest-side', + 'mask-radial-farthest-side', + 'mask-radial-closest-corner', + 'mask-radial-farthest-corner', + 'mask-radial-[25%_25%]', + ], + ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -20155,8 +18110,7 @@ test('mask-radial', async () => { syntax: "*"; inherits: false; initial-value: center; - } - " + }" `) expect( await run([ @@ -20180,7 +18134,13 @@ test('mask-radial', async () => { test('mask-radial-at', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-radial-at-top', 'mask-radial-at-top-left', @@ -20192,16 +18152,9 @@ test('mask-radial-at', async () => { 'mask-radial-at-right', 'mask-radial-at-[25%]', ], - css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - .mask-radial-at-\\[25\\%\\] { + ".mask-radial-at-\\[25\\%\\] { --tw-mask-radial-position: 25%; } @@ -20235,8 +18188,7 @@ test('mask-radial-at', async () => { .mask-radial-at-top-right { --tw-mask-radial-position: top right; - } - " + }" `) expect( await run([ @@ -20280,15 +18232,14 @@ test('mask-radial-at', async () => { }) test('mask-radial-from', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-radial-from-0', 'mask-radial-from-1.5', @@ -20302,11 +18253,9 @@ test('mask-radial-from', async () => { 'mask-radial-from-(color:--my-var)', 'mask-radial-from-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -20357,7 +18306,7 @@ test('mask-radial-from', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-radial-stops: var(--tw-mask-radial-shape) var(--tw-mask-radial-size) at var(--tw-mask-radial-position), var(--tw-mask-radial-from-color) var(--tw-mask-radial-from-position), var(--tw-mask-radial-to-color) var(--tw-mask-radial-to-position); --tw-mask-radial: radial-gradient(var(--tw-mask-radial-stops)); - --tw-mask-radial-from-position: 0px; + --tw-mask-radial-from-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -20493,11 +18442,16 @@ test('mask-radial-from', async () => { syntax: "*"; inherits: false; initial-value: center; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-radial-from', 'mask-radial-from-2.8175', @@ -20528,21 +18482,19 @@ test('mask-radial-from', async () => { '-mask-radial-from-[25%]/foo', '-mask-radial-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-radial-to', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-radial-to-0', 'mask-radial-to-1.5', @@ -20556,11 +18508,9 @@ test('mask-radial-to', async () => { 'mask-radial-to-(color:--my-var)', 'mask-radial-to-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -20611,7 +18561,7 @@ test('mask-radial-to', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-radial-stops: var(--tw-mask-radial-shape) var(--tw-mask-radial-size) at var(--tw-mask-radial-position), var(--tw-mask-radial-from-color) var(--tw-mask-radial-from-position), var(--tw-mask-radial-to-color) var(--tw-mask-radial-to-position); --tw-mask-radial: radial-gradient(var(--tw-mask-radial-stops)); - --tw-mask-radial-to-position: 0px; + --tw-mask-radial-to-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -20747,11 +18697,16 @@ test('mask-radial-to', async () => { syntax: "*"; inherits: false; initial-value: center; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-radial-to', 'mask-radial-to-2.8175', @@ -20782,24 +18737,20 @@ test('mask-radial-to', async () => { '-mask-radial-to-[25%]/foo', '-mask-radial-to-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-conic', async () => { expect( - await run([ - 'mask-conic-0', - 'mask-conic-1', - '-mask-conic-1', - 'mask-conic-45', - 'mask-conic-[3rad]', - '-mask-conic-45', - ]), + await compileCss( + css` + @tailwind utilities; + `, + ['mask-conic-45', 'mask-conic-[3rad]', '-mask-conic-45'], + ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -20814,17 +18765,6 @@ test('mask-conic', async () => { } } - .-mask-conic-1 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); - --tw-mask-conic-position: -1deg; - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } - .-mask-conic-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -20836,28 +18776,6 @@ test('mask-conic', async () => { mask-composite: intersect; } - .mask-conic-0 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); - --tw-mask-conic-position: 0deg; - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } - - .mask-conic-1 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); - --tw-mask-conic-position: 1deg; - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } - .mask-conic-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -20926,8 +18844,7 @@ test('mask-conic', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( await run([ @@ -20949,15 +18866,14 @@ test('mask-conic', async () => { }) test('mask-conic-from', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-conic-from-0', 'mask-conic-from-1.5', @@ -20971,11 +18887,9 @@ test('mask-conic-from', async () => { 'mask-conic-from-(color:--my-var)', 'mask-conic-from-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -21024,7 +18938,7 @@ test('mask-conic-from', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-conic-stops: from var(--tw-mask-conic-position), var(--tw-mask-conic-from-color) var(--tw-mask-conic-from-position), var(--tw-mask-conic-to-color) var(--tw-mask-conic-to-position); --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops)); - --tw-mask-conic-from-position: 0px; + --tw-mask-conic-from-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -21148,11 +19062,16 @@ test('mask-conic-from', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-conic-from', 'mask-conic-from-2.8175', @@ -21183,21 +19102,19 @@ test('mask-conic-from', async () => { '-mask-conic-from-[25%]/foo', '-mask-conic-from-[-25%]/foo', ], - input, ), ).toEqual('') }) test('mask-conic-to', async () => { - let input = css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-conic-to-0', 'mask-conic-to-1.5', @@ -21211,11 +19128,9 @@ test('mask-conic-to', async () => { 'mask-conic-to-(color:--my-var)', 'mask-conic-to-(length:--my-var)', ], - input, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-mask-linear: linear-gradient(#fff, #fff); @@ -21264,7 +19179,7 @@ test('mask-conic-to', async () => { mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); --tw-mask-conic-stops: from var(--tw-mask-conic-position), var(--tw-mask-conic-from-color) var(--tw-mask-conic-from-position), var(--tw-mask-conic-to-color) var(--tw-mask-conic-to-position); --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops)); - --tw-mask-conic-to-position: 0px; + --tw-mask-conic-to-position: calc(var(--spacing) * 0); -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -21388,11 +19303,16 @@ test('mask-conic-to', async () => { syntax: "*"; inherits: false; initial-value: transparent; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, [ 'mask-conic-to', 'mask-conic-to-2.8175', @@ -21423,15 +19343,13 @@ test('mask-conic-to', async () => { '-mask-conic-to-[25%]/foo', '-mask-conic-to-[-25%]/foo', ], - input, ), ).toEqual('') }) test('box-decoration', async () => { expect(await run(['box-decoration-slice', 'box-decoration-clone'])).toMatchInlineSnapshot(` - " - .box-decoration-clone { + ".box-decoration-clone { -webkit-box-decoration-break: clone; box-decoration-break: clone; } @@ -21439,8 +19357,7 @@ test('box-decoration', async () => { .box-decoration-slice { -webkit-box-decoration-break: slice; box-decoration-break: slice; - } - " + }" `) expect( await run([ @@ -21457,8 +19374,7 @@ test('box-decoration', async () => { test('bg-clip', async () => { expect(await run(['bg-clip-border', 'bg-clip-padding', 'bg-clip-content', 'bg-clip-text'])) .toMatchInlineSnapshot(` - " - .bg-clip-border { + ".bg-clip-border { background-clip: border-box; } @@ -21473,8 +19389,7 @@ test('bg-clip', async () => { .bg-clip-text { -webkit-background-clip: text; background-clip: text; - } - " + }" `) expect( await run([ @@ -21494,8 +19409,7 @@ test('bg-clip', async () => { test('bg-origin', async () => { expect(await run(['bg-origin-border', 'bg-origin-padding', 'bg-origin-content'])) .toMatchInlineSnapshot(` - " - .bg-origin-border { + ".bg-origin-border { background-origin: border-box; } @@ -21505,8 +19419,7 @@ test('bg-origin', async () => { .bg-origin-padding { background-origin: padding-box; - } - " + }" `) expect( await run([ @@ -21533,8 +19446,7 @@ test('mask-clip', async () => { 'mask-no-clip', ]), ).toMatchInlineSnapshot(` - " - .mask-clip-border { + ".mask-clip-border { -webkit-mask-clip: border-box; mask-clip: border-box; } @@ -21567,8 +19479,7 @@ test('mask-clip', async () => { .mask-no-clip { -webkit-mask-clip: no-clip; mask-clip: no-clip; - } - " + }" `) expect( await run([ @@ -21602,8 +19513,7 @@ test('mask-origin', async () => { 'mask-origin-view', ]), ).toMatchInlineSnapshot(` - " - .mask-origin-border { + ".mask-origin-border { -webkit-mask-origin: border-box; mask-origin: border-box; } @@ -21631,8 +19541,7 @@ test('mask-origin', async () => { .mask-origin-view { -webkit-mask-origin: view-box; mask-origin: view-box; - } - " + }" `) expect( await run([ @@ -21674,8 +19583,7 @@ test('bg-blend', async () => { 'bg-blend-luminosity', ]), ).toMatchInlineSnapshot(` - " - .bg-blend-color { + ".bg-blend-color { background-blend-mode: color; } @@ -21737,8 +19645,7 @@ test('bg-blend', async () => { .bg-blend-soft-light { background-blend-mode: soft-light; - } - " + }" `) expect( await run([ @@ -21802,8 +19709,7 @@ test('mix-blend', async () => { 'mix-blend-plus-lighter', ]), ).toMatchInlineSnapshot(` - " - .mix-blend-color { + ".mix-blend-color { mix-blend-mode: color; } @@ -21873,8 +19779,7 @@ test('mix-blend', async () => { .mix-blend-soft-light { mix-blend-mode: soft-light; - } - " + }" `) expect( await run([ @@ -21920,7 +19825,14 @@ test('mix-blend', async () => { test('fill', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --fill-blue-500: #3b82f6; + } + @tailwind utilities; + `, [ 'fill-red-500', 'fill-red-500/50', @@ -21941,17 +19853,9 @@ test('fill', async () => { 'fill-[#0088cc]/[0.5]', 'fill-[#0088cc]/[50%]', ], - css` - @theme { - --color-red-500: #ef4444; - --fill-blue-500: #3b82f6; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #ef4444; --fill-blue-500: #3b82f6; } @@ -22068,8 +19972,7 @@ test('fill', async () => { .fill-transparent { fill: #0000; - } - " + }" `) expect( await run([ @@ -22095,7 +19998,14 @@ test('fill', async () => { test('stroke', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --stroke-blue-500: #3b82f6; + } + @tailwind utilities; + `, [ // Color 'stroke-red-500', @@ -22137,17 +20047,9 @@ test('stroke', async () => { 'stroke-[length:var(--my-width)]', 'stroke-[percentage:var(--my-width)]', ], - css` - @theme { - --color-red-500: #ef4444; - --stroke-blue-500: #3b82f6; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #ef4444; --stroke-blue-500: #3b82f6; } @@ -22356,8 +20258,7 @@ test('stroke', async () => { .stroke-\\[length\\:var\\(--my-width\\)\\], .stroke-\\[number\\:var\\(--my-width\\)\\], .stroke-\\[percentage\\:var\\(--my-width\\)\\] { stroke-width: var(--my-width); - } - " + }" `) expect( await run([ @@ -22383,11 +20284,6 @@ test('stroke', async () => { // Width '-stroke-0', 'stroke--1', - 'stroke-0/foo', - 'stroke-1/50', - 'stroke-2/[0.5]', - 'stroke-2/(--my-opacity)', - 'stroke-[12px]/foo', ]), ).toEqual('') }) @@ -22421,8 +20317,7 @@ test('object', async () => { 'object-right-top', ]), ).toMatchInlineSnapshot(` - " - .object-contain { + ".object-contain { object-fit: contain; } @@ -22496,8 +20391,7 @@ test('object', async () => { .object-top-right { object-position: right top; - } - " + }" `) expect( await run([ @@ -22532,32 +20426,29 @@ test('object', async () => { ).toEqual('') expect( - await run( - ['object-center'], + await compileCss( css` @theme { --object-position-center: top left; } @tailwind utilities; `, + ['object-center'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --object-position-center: top left; } .object-center { object-position: var(--object-position-center); - } - " + }" `) }) test('p', async () => { expect( - await run( - ['p-1', 'p-4', 'p-99', 'p-big', 'p-[4px]'], + await compileCss( css` @theme { --spacing: 0.25rem; @@ -22565,16 +20456,16 @@ test('p', async () => { } @tailwind utilities; `, + ['p-1', 'p-4', 'p-99', 'p-big', 'p-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } .p-1 { - padding: var(--spacing); + padding: calc(var(--spacing) * 1); } .p-4 { @@ -22591,16 +20482,14 @@ test('p', async () => { .p-big { padding: var(--spacing-big); - } - " + }" `) expect(await run(['p', '-p-4', '-p-[4px]', 'p-4/foo', 'p-[4px]/foo'])).toEqual('') }) test('px', async () => { expect( - await run( - ['px-1', 'px-99', 'px-2.5', 'px-big', 'px-[4px]'], + await compileCss( css` @theme { --spacing: 0.25rem; @@ -22608,16 +20497,16 @@ test('px', async () => { } @tailwind utilities; `, + ['px-1', 'px-99', 'px-2.5', 'px-big', 'px-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } .px-1 { - padding-inline: var(--spacing); + padding-inline: calc(var(--spacing) * 1); } .px-2\\.5 { @@ -22634,16 +20523,14 @@ test('px', async () => { .px-big { padding-inline: var(--spacing-big); - } - " + }" `) expect(await run(['px', '-px-4', '-px-[4px]', 'px-4/foo', 'px-[4px]/foo'])).toEqual('') }) test('py', async () => { expect( - await run( - ['py-1', 'py-4', 'py-99', 'py-big', 'py-[4px]'], + await compileCss( css` @theme { --spacing: 0.25rem; @@ -22651,16 +20538,16 @@ test('py', async () => { } @tailwind utilities; `, + ['py-1', 'py-4', 'py-99', 'py-big', 'py-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } .py-1 { - padding-block: var(--spacing); + padding-block: calc(var(--spacing) * 1); } .py-4 { @@ -22677,16 +20564,14 @@ test('py', async () => { .py-big { padding-block: var(--spacing-big); - } - " + }" `) expect(await run(['py', '-py-4', '-py-[4px]', 'py-4/foo', 'py-[4px]/foo'])).toEqual('') }) test('pt', async () => { expect( - await run( - ['pt-1', 'pt-4', 'pt-99', 'pt-big', 'pt-[4px]'], + await compileCss( css` @theme { --spacing: 0.25rem; @@ -22694,16 +20579,16 @@ test('pt', async () => { } @tailwind utilities; `, + ['pt-1', 'pt-4', 'pt-99', 'pt-big', 'pt-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } .pt-1 { - padding-top: var(--spacing); + padding-top: calc(var(--spacing) * 1); } .pt-4 { @@ -22720,16 +20605,14 @@ test('pt', async () => { .pt-big { padding-top: var(--spacing-big); - } - " + }" `) expect(await run(['pt', '-pt-4', '-pt-[4px]', 'pt-4/foo', 'pt-[4px]/foo'])).toEqual('') }) test('ps', async () => { expect( - await run( - ['ps-1', 'ps-4', 'ps-99', 'ps-big', 'ps-[4px]'], + await compileCss( css` @theme { --spacing: 0.25rem; @@ -22737,16 +20620,16 @@ test('ps', async () => { } @tailwind utilities; `, + ['ps-1', 'ps-4', 'ps-99', 'ps-big', 'ps-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } .ps-1 { - padding-inline-start: var(--spacing); + padding-inline-start: calc(var(--spacing) * 1); } .ps-4 { @@ -22763,16 +20646,14 @@ test('ps', async () => { .ps-big { padding-inline-start: var(--spacing-big); - } - " + }" `) expect(await run(['ps', '-ps-4', '-ps-[4px]', 'ps-4/foo', 'ps-[4px]/foo'])).toEqual('') }) test('pe', async () => { expect( - await run( - ['pe-1', 'pe-4', 'pe-99', 'pe-big', 'pe-[4px]'], + await compileCss( css` @theme { --spacing: 0.25rem; @@ -22780,16 +20661,16 @@ test('pe', async () => { } @tailwind utilities; `, + ['pe-1', 'pe-4', 'pe-99', 'pe-big', 'pe-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } .pe-1 { - padding-inline-end: var(--spacing); + padding-inline-end: calc(var(--spacing) * 1); } .pe-4 { @@ -22806,102 +20687,14 @@ test('pe', async () => { .pe-big { padding-inline-end: var(--spacing-big); - } - " + }" `) expect(await run(['pe', '-pe-4', '-pe-[4px]', 'pe-4/foo', 'pe-[4px]/foo'])).toEqual('') }) -test('pbs', async () => { - expect( - await run( - ['pbs-1', 'pbs-4', 'pbs-99', 'pbs-big', 'pbs-[4px]'], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing: .25rem; - --spacing-big: 100rem; - } - - .pbs-1 { - padding-block-start: var(--spacing); - } - - .pbs-4 { - padding-block-start: calc(var(--spacing) * 4); - } - - .pbs-99 { - padding-block-start: calc(var(--spacing) * 99); - } - - .pbs-\\[4px\\] { - padding-block-start: 4px; - } - - .pbs-big { - padding-block-start: var(--spacing-big); - } - " - `) - expect(await run(['pbs', '-pbs-4', '-pbs-[4px]', 'pbs-4/foo', 'pbs-[4px]/foo'])).toEqual('') -}) - -test('pbe', async () => { - expect( - await run( - ['pbe-1', 'pbe-4', 'pbe-99', 'pbe-big', 'pbe-[4px]'], - css` - @theme { - --spacing: 0.25rem; - --spacing-big: 100rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --spacing: .25rem; - --spacing-big: 100rem; - } - - .pbe-1 { - padding-block-end: var(--spacing); - } - - .pbe-4 { - padding-block-end: calc(var(--spacing) * 4); - } - - .pbe-99 { - padding-block-end: calc(var(--spacing) * 99); - } - - .pbe-\\[4px\\] { - padding-block-end: 4px; - } - - .pbe-big { - padding-block-end: var(--spacing-big); - } - " - `) - expect(await run(['pbe', '-pbe-4', '-pbe-[4px]', 'pbe-4/foo', 'pbe-[4px]/foo'])).toEqual('') -}) - test('pr', async () => { expect( - await run( - ['pr-1', 'pr-4', 'pr-99', 'pr-big', 'pr-[4px]'], + await compileCss( css` @theme { --spacing: 0.25rem; @@ -22909,16 +20702,16 @@ test('pr', async () => { } @tailwind utilities; `, + ['pr-1', 'pr-4', 'pr-99', 'pr-big', 'pr-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } .pr-1 { - padding-right: var(--spacing); + padding-right: calc(var(--spacing) * 1); } .pr-4 { @@ -22935,16 +20728,14 @@ test('pr', async () => { .pr-big { padding-right: var(--spacing-big); - } - " + }" `) expect(await run(['pr', '-pr-4', '-pr-[4px]', 'pr-4/foo', 'pr-[4px]/foo'])).toEqual('') }) test('pb', async () => { expect( - await run( - ['pb-1', 'pb-4', 'pb-99', 'pb-big', 'pb-[4px]'], + await compileCss( css` @theme { --spacing: 0.25rem; @@ -22952,16 +20743,16 @@ test('pb', async () => { } @tailwind utilities; `, + ['pb-1', 'pb-4', 'pb-99', 'pb-big', 'pb-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } .pb-1 { - padding-bottom: var(--spacing); + padding-bottom: calc(var(--spacing) * 1); } .pb-4 { @@ -22978,16 +20769,14 @@ test('pb', async () => { .pb-big { padding-bottom: var(--spacing-big); - } - " + }" `) expect(await run(['pb', '-pb-4', '-pb-[4px]', 'pb-4/foo', 'pb-[4px]/foo'])).toEqual('') }) test('pl', async () => { expect( - await run( - ['pl-1', 'pl-4', 'pl-99', 'pl-big', 'pl-[4px]'], + await compileCss( css` @theme { --spacing: 0.25rem; @@ -22995,16 +20784,16 @@ test('pl', async () => { } @tailwind utilities; `, + ['pl-1', 'pl-4', 'pl-99', 'pl-big', 'pl-[4px]'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --spacing-big: 100rem; } .pl-1 { - padding-left: var(--spacing); + padding-left: calc(var(--spacing) * 1); } .pl-4 { @@ -23021,8 +20810,7 @@ test('pl', async () => { .pl-big { padding-left: var(--spacing-big); - } - " + }" `) expect(await run(['pl', '-pl-4', '-pl-[4px]', 'pl-4/foo', 'pl-[4px]/foo'])).toEqual('') }) @@ -23031,8 +20819,7 @@ test('text-align', async () => { expect( await run(['text-left', 'text-center', 'text-right', 'text-justify', 'text-start', 'text-end']), ).toMatchInlineSnapshot(` - " - .text-center { + ".text-center { text-align: center; } @@ -23054,8 +20841,7 @@ test('text-align', async () => { .text-start { text-align: start; - } - " + }" `) expect( await run([ @@ -23077,15 +20863,13 @@ test('text-align', async () => { test('indent', async () => { expect(await run(['indent-[4px]', '-indent-[4px]'])).toMatchInlineSnapshot(` - " - .-indent-\\[4px\\] { + ".-indent-\\[4px\\] { text-indent: -4px; } .indent-\\[4px\\] { text-indent: 4px; - } - " + }" `) expect(await run(['indent', 'indent-[4px]/foo', '-indent-[4px]/foo'])).toEqual('') }) @@ -23105,8 +20889,7 @@ test('align', async () => { 'align-[var(--value)]', ]), ).toMatchInlineSnapshot(` - " - .align-\\[var\\(--value\\)\\] { + ".align-\\[var\\(--value\\)\\] { vertical-align: var(--value); } @@ -23140,8 +20923,7 @@ test('align', async () => { .align-top { vertical-align: top; - } - " + }" `) expect( await run([ @@ -23172,7 +20954,16 @@ test('align', async () => { test('font', async () => { expect( - await run( + await compileCss( + css` + @theme { + --font-sans: + ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-weight-bold: 650; + } + @tailwind utilities; + `, [ // font-family 'font-sans', @@ -23187,19 +20978,9 @@ test('font', async () => { 'font-[100]', 'font-[number:var(--my-weight)]', ], - css` - @theme { - --font-sans: - ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', - 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-weight-bold: 650; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-font-weight: initial; @@ -23208,8 +20989,7 @@ test('font', async () => { } :root, :host { - --font-sans: ui-sans-serif, system-ui, 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-weight-bold: 650; } @@ -23252,11 +21032,19 @@ test('font', async () => { @property --tw-font-weight { syntax: "*"; inherits: false - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme reference { + --font-sans: + ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-weight-bold: 650; + } + @tailwind utilities; + `, [ 'font', // font-family @@ -23276,60 +21064,13 @@ test('font', async () => { 'font-[100]/foo', 'font-[number:var(--my-weight)]/foo', ], - css` - @theme reference { - --font-sans: - ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', - 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-weight-bold: 650; - } - @tailwind utilities; - `, ), ).toEqual('') }) -test('font-features', async () => { - expect( - await run([ - 'font-features-["smcp"]', - 'font-features-["c2sc","smcp"]', - 'font-features-[var(--my-features)]', - 'font-features-(--my-features)', - ]), - ).toMatchInlineSnapshot(` - " - .font-features-\\(--my-features\\) { - font-feature-settings: var(--my-features); - } - - .font-features-\\[\\"c2sc\\"\\,\\"smcp\\"\\] { - font-feature-settings: "c2sc","smcp"; - } - - .font-features-\\[\\"smcp\\"\\] { - font-feature-settings: "smcp"; - } - - .font-features-\\[var\\(--my-features\\)\\] { - font-feature-settings: var(--my-features); - } - " - `) - expect( - await run([ - 'font-features', - '-font-features-["smcp"]', - 'font-features-smcp', - 'font-features-["smcp"]/foo', - ]), - ).toEqual('') -}) - test('text-transform', async () => { expect(await run(['uppercase', 'lowercase', 'capitalize', 'normal-case'])).toMatchInlineSnapshot(` - " - .capitalize { + ".capitalize { text-transform: capitalize; } @@ -23343,8 +21084,7 @@ test('text-transform', async () => { .uppercase { text-transform: uppercase; - } - " + }" `) expect( await run([ @@ -23362,15 +21102,13 @@ test('text-transform', async () => { test('font-style', async () => { expect(await run(['italic', 'not-italic'])).toMatchInlineSnapshot(` - " - .italic { + ".italic { font-style: italic; } .not-italic { font-style: normal; - } - " + }" `) expect(await run(['-italic', '-not-italic', 'italic/foo', 'not-italic/foo'])).toEqual('') }) @@ -23378,8 +21116,7 @@ test('font-style', async () => { test('font-stretch', async () => { expect(await run(['font-stretch-ultra-expanded', 'font-stretch-50%', 'font-stretch-200%'])) .toMatchInlineSnapshot(` - " - .font-stretch-50\\% { + ".font-stretch-50\\% { font-stretch: 50%; } @@ -23389,8 +21126,7 @@ test('font-stretch', async () => { .font-stretch-ultra-expanded { font-stretch: ultra-expanded; - } - " + }" `) expect( await run([ @@ -23410,8 +21146,7 @@ test('font-stretch', async () => { test('text-decoration-line', async () => { expect(await run(['underline', 'overline', 'line-through', 'no-underline'])) .toMatchInlineSnapshot(` - " - .line-through { + ".line-through { text-decoration-line: line-through; } @@ -23425,8 +21160,7 @@ test('text-decoration-line', async () => { .underline { text-decoration-line: underline; - } - " + }" `) expect( await run([ @@ -23444,7 +21178,13 @@ test('text-decoration-line', async () => { test('placeholder', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + } + @tailwind utilities; + `, [ 'placeholder-red-500', 'placeholder-red-500/50', @@ -23464,16 +21204,9 @@ test('placeholder', async () => { 'placeholder-[#0088cc]/[0.5]', 'placeholder-[#0088cc]/[50%]', ], - css` - @theme { - --color-red-500: #ef4444; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #ef4444; } @@ -23585,8 +21318,7 @@ test('placeholder', async () => { .placeholder-transparent::placeholder { color: #0000; - } - " + }" `) expect( await run([ @@ -23611,7 +21343,14 @@ test('placeholder', async () => { test('decoration', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --text-decoration-color-blue-500: #3b82f6; + } + @tailwind utilities; + `, [ // text-decoration-color 'decoration-red-500', @@ -23658,17 +21397,9 @@ test('decoration', async () => { 'decoration-[length:var(--my-thickness)]', 'decoration-[percentage:var(--my-thickness)]', ], - css` - @theme { - --color-red-500: #ef4444; - --text-decoration-color-blue-500: #3b82f6; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #ef4444; --text-decoration-color-blue-500: #3b82f6; } @@ -23917,8 +21648,7 @@ test('decoration', async () => { .decoration-from-font { text-decoration-thickness: from-font; - } - " + }" `) expect( await run([ @@ -23977,18 +21707,17 @@ test('decoration', async () => { test('animate', async () => { expect( - await run( - ['animate-spin', 'animate-none', 'animate-[bounce_1s_infinite]', 'animate-not-found'], + await compileCss( css` @theme { --animate-spin: spin 1s linear infinite; } @tailwind utilities; `, + ['animate-spin', 'animate-none', 'animate-[bounce_1s_infinite]', 'animate-not-found'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --animate-spin: spin 1s linear infinite; } @@ -24002,8 +21731,7 @@ test('animate', async () => { .animate-spin { animation: var(--animate-spin); - } - " + }" `) expect( await run([ @@ -24020,31 +21748,41 @@ test('animate', async () => { ).toEqual('') expect( - await run( - ['animate-none'], + await compileCss( css` @theme { --animate-none: bounce 1s infinite; } @tailwind utilities; `, + ['animate-none'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --animate-none: bounce 1s infinite; } .animate-none { animation: var(--animate-none); - } - " + }" `) }) test('filter', async () => { expect( - await run( + await compileCss( + css` + @theme { + --blur-xl: 24px; + --color-red-500: #ef4444; + --drop-shadow: 0 1px 1px rgb(0 0 0 / 0.05); + --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1); + } + @theme inline { + --drop-shadow-multi: 0 1px 1px rgb(0 0 0 / 0.05), 0 9px 7px rgb(0 0 0 / 0.1); + } + @tailwind utilities; + `, [ 'filter', 'filter-none', @@ -24068,7 +21806,6 @@ test('filter', async () => { 'invert-[var(--value)]', 'drop-shadow', 'drop-shadow/25', - 'drop-shadow/12.5', 'drop-shadow-xl', 'drop-shadow-multi', 'drop-shadow-[0_0_red]', @@ -24076,7 +21813,6 @@ test('filter', async () => { 'drop-shadow-red-500/50', 'drop-shadow-none', 'drop-shadow-inherit', - 'drop-shadow-calc', 'saturate-0', 'saturate-[1.75]', 'saturate-[var(--value)]', @@ -24085,24 +21821,9 @@ test('filter', async () => { 'sepia-[50%]', 'sepia-[var(--value)]', ], - css` - @theme { - --spacing: 0.25rem; - --blur-xl: 24px; - --color-red-500: #ef4444; - --drop-shadow: 0 1px 1px rgb(0 0 0 / 0.05); - --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1); - --drop-shadow-calc: 0 0 calc(1 * var(--spacing)) black; - } - @theme inline { - --drop-shadow-multi: 0 1px 1px rgb(0 0 0 / 0.05), 0 9px 7px rgb(0 0 0 / 0.1); - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-blur: initial; @@ -24123,12 +21844,10 @@ test('filter', async () => { } :root, :host { - --spacing: .25rem; --blur-xl: 24px; --color-red-500: #ef4444; --drop-shadow: 0 1px 1px #0000000d; --drop-shadow-xl: 0 9px 7px #0000001a; - --drop-shadow-calc: 0 0 calc(1 * var(--spacing)) black; } .blur-\\[4px\\] { @@ -24137,7 +21856,7 @@ test('filter', async () => { } .blur-none { - --tw-blur: ; + --tw-blur: ; filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); } @@ -24166,13 +21885,6 @@ test('filter', async () => { filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); } - .drop-shadow\\/12\\.5 { - --tw-drop-shadow-alpha: 12.5%; - --tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, oklab(0% 0 0 / .125))); - --tw-drop-shadow: drop-shadow(var(--drop-shadow)); - filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); - } - .drop-shadow\\/25 { --tw-drop-shadow-alpha: 25%; --tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, oklab(0% 0 0 / .25))); @@ -24192,12 +21904,6 @@ test('filter', async () => { filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); } - .drop-shadow-calc { - --tw-drop-shadow-size: drop-shadow(0 0 calc(1 * var(--spacing)) var(--tw-drop-shadow-color, black)); - --tw-drop-shadow: drop-shadow(var(--drop-shadow-calc)); - filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); - } - .drop-shadow-multi { --tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, #0000000d)) drop-shadow(0 9px 7px var(--tw-drop-shadow-color, #0000001a)); --tw-drop-shadow: drop-shadow(0 1px 1px #0000000d) drop-shadow(0 9px 7px #0000001a); @@ -24211,7 +21917,7 @@ test('filter', async () => { } .drop-shadow-none { - --tw-drop-shadow: ; + --tw-drop-shadow: ; filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); } @@ -24409,109 +22115,99 @@ test('filter', async () => { @property --tw-drop-shadow-size { syntax: "*"; inherits: false - } - " + }" `) expect( - await run( - [ - '-filter', - '-filter-none', - '-filter-[var(--value)]', - '-blur-xl', - '-blur-[4px]', - 'brightness--50', - '-brightness-50', - '-brightness-[1.23]', - 'brightness-unknown', - 'contrast--50', - '-contrast-50', - '-contrast-[1.23]', - 'contrast-unknown', - '-grayscale', - '-grayscale-0', - 'grayscale--1', - '-grayscale-[var(--value)]', - 'grayscale-unknown', - 'hue-rotate--5', - 'hue-rotate-unknown', - '-invert', - 'invert--5', - '-invert-0', - '-invert-[var(--value)]', - 'invert-unknown', - '-drop-shadow-xl', - '-drop-shadow-[0_0_red]', + await run([ + '-filter', + '-filter-none', + '-filter-[var(--value)]', + '-blur-xl', + '-blur-[4px]', + 'brightness--50', + '-brightness-50', + '-brightness-[1.23]', + 'brightness-unknown', + 'contrast--50', + '-contrast-50', + '-contrast-[1.23]', + 'contrast-unknown', + '-grayscale', + '-grayscale-0', + 'grayscale--1', + '-grayscale-[var(--value)]', + 'grayscale-unknown', + 'hue-rotate--5', + 'hue-rotate-unknown', + '-invert', + 'invert--5', + '-invert-0', + '-invert-[var(--value)]', + 'invert-unknown', + '-drop-shadow-xl', + '-drop-shadow-[0_0_red]', - 'drop-shadow/foo', - '-drop-shadow/foo', - '-drop-shadow/25', - '-drop-shadow-red-500', - 'drop-shadow-red-500/foo', - '-drop-shadow-red-500/foo', - '-drop-shadow-red-500/50', + 'drop-shadow/foo', + '-drop-shadow/foo', + '-drop-shadow/25', + '-drop-shadow-red-500', + 'drop-shadow-red-500/foo', + '-drop-shadow-red-500/foo', + '-drop-shadow-red-500/50', - '-saturate-0', - 'saturate--5', - '-saturate-[1.75]', - '-saturate-[var(--value)]', - 'saturate-saturate', - '-sepia', - 'sepia--50', - '-sepia-0', - '-sepia-[50%]', - '-sepia-[var(--value)]', - 'sepia-unknown', - 'filter/foo', - 'filter-none/foo', - 'filter-[var(--value)]/foo', - 'blur-xl/foo', - 'blur-none/foo', - 'blur-[4px]/foo', - 'brightness-50/foo', - 'brightness-[1.23]/foo', - 'contrast-50/foo', - 'contrast-[1.23]/foo', - 'grayscale/foo', - 'grayscale-0/foo', - 'grayscale-[var(--value)]/foo', - 'hue-rotate-15/foo', - 'hue-rotate-[45deg]/foo', - 'invert/foo', - 'invert-0/foo', - 'invert-[var(--value)]/foo', - 'drop-shadow-xl/foo', - 'drop-shadow-[0_0_red]/foo', - 'saturate-0/foo', - 'saturate-[1.75]/foo', - 'saturate-[var(--value)]/foo', - 'sepia/foo', - 'sepia-0/foo', - 'sepia-[50%]/foo', - 'sepia-[var(--value)]/foo', - ], - css` - @theme { - --drop-shadow: 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, - ), + '-saturate-0', + 'saturate--5', + '-saturate-[1.75]', + '-saturate-[var(--value)]', + 'saturate-saturate', + '-sepia', + 'sepia--50', + '-sepia-0', + '-sepia-[50%]', + '-sepia-[var(--value)]', + 'sepia-unknown', + 'filter/foo', + 'filter-none/foo', + 'filter-[var(--value)]/foo', + 'blur-xl/foo', + 'blur-none/foo', + 'blur-[4px]/foo', + 'brightness-50/foo', + 'brightness-[1.23]/foo', + 'contrast-50/foo', + 'contrast-[1.23]/foo', + 'grayscale/foo', + 'grayscale-0/foo', + 'grayscale-[var(--value)]/foo', + 'hue-rotate-15/foo', + 'hue-rotate-[45deg]/foo', + 'invert/foo', + 'invert-0/foo', + 'invert-[var(--value)]/foo', + 'drop-shadow-xl/foo', + 'drop-shadow-[0_0_red]/foo', + 'saturate-0/foo', + 'saturate-[1.75]/foo', + 'saturate-[var(--value)]/foo', + 'sepia/foo', + 'sepia-0/foo', + 'sepia-[50%]/foo', + 'sepia-[var(--value)]/foo', + ]), ).toEqual('') expect( - await run( - ['blur-none'], + await compileCss( css` @theme { --blur-none: 2px; } @tailwind utilities; `, + ['blur-none'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-blur: initial; @@ -24604,14 +22300,19 @@ test('filter', async () => { @property --tw-drop-shadow-size { syntax: "*"; inherits: false - } - " + }" `) }) test('backdrop-filter', async () => { expect( - await run( + await compileCss( + css` + @theme { + --blur-xl: 24px; + } + @tailwind utilities; + `, [ 'backdrop-filter', 'backdrop-filter-none', @@ -24647,16 +22348,9 @@ test('backdrop-filter', async () => { 'backdrop-sepia-[50%]', 'backdrop-sepia-[var(--value)]', ], - css` - @theme { - --blur-xl: 24px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-backdrop-blur: initial; @@ -24683,7 +22377,7 @@ test('backdrop-filter', async () => { } .backdrop-blur-none { - --tw-backdrop-blur: ; + --tw-backdrop-blur: ; -webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, ); backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, ); } @@ -24914,8 +22608,7 @@ test('backdrop-filter', async () => { @property --tw-backdrop-sepia { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -24991,18 +22684,17 @@ test('backdrop-filter', async () => { ).toEqual('') expect( - await run( - ['backdrop-blur-none'], + await compileCss( css` @theme { --backdrop-blur-none: 2px; } @tailwind utilities; `, + ['backdrop-blur-none'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-backdrop-blur: initial; @@ -25071,24 +22763,13 @@ test('backdrop-filter', async () => { @property --tw-backdrop-sepia { syntax: "*"; inherits: false - } - " + }" `) }) test('transition', async () => { expect( - await run( - [ - 'transition', - 'transition-none', - 'transition-all', - 'transition-transform', - 'transition-shadow', - 'transition-colors', - 'transition-opacity', - 'transition-[var(--value)]', - ], + await compileCss( css` @theme { --default-transition-timing-function: ease; @@ -25100,10 +22781,19 @@ test('transition', async () => { } @tailwind utilities; `, + [ + 'transition', + 'transition-none', + 'transition-all', + 'transition-transform', + 'transition-shadow', + 'transition-colors', + 'transition-opacity', + 'transition-[var(--value)]', + ], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --default-transition-timing-function: ease; --default-transition-duration: .1s; --transition-property-opacity: opacity; @@ -25153,13 +22843,11 @@ test('transition', async () => { .transition-none { transition-property: none; - } - " + }" `) expect( - await run( - ['transition', 'transition-all', 'transition-colors'], + await compileCss( css` @theme inline { --default-transition-timing-function: ease; @@ -25167,10 +22855,10 @@ test('transition', async () => { } @tailwind utilities; `, + ['transition', 'transition-all', 'transition-colors'], ), ).toMatchInlineSnapshot(` - " - .transition { + ".transition { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, ease); transition-duration: var(--tw-duration, .1s); @@ -25186,18 +22874,22 @@ test('transition', async () => { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; transition-timing-function: var(--tw-ease, ease); transition-duration: var(--tw-duration, .1s); - } - " + }" `) - expect(await run(['transition-all'])).toMatchInlineSnapshot(` - " - .transition-all { + expect( + await compileCss( + css` + @tailwind utilities; + `, + ['transition-all'], + ), + ).toMatchInlineSnapshot(` + ".transition-all { transition-property: all; transition-timing-function: var(--tw-ease, ease); transition-duration: var(--tw-duration, 0s); - } - " + }" `) expect( @@ -25219,18 +22911,17 @@ test('transition', async () => { ).toEqual('') expect( - await run( - ['transition-colors'], + await compileCss( css` @theme { --transition-property-colors: transform; } @tailwind utilities; `, + ['transition-colors'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --transition-property-colors: transform; } @@ -25238,22 +22929,19 @@ test('transition', async () => { transition-property: var(--transition-property-colors); transition-timing-function: var(--tw-ease, ease); transition-duration: var(--tw-duration, 0s); - } - " + }" `) }) test('transition-behavior', async () => { expect(await run(['transition-discrete', 'transition-normal'])).toMatchInlineSnapshot(` - " - .transition-discrete { + ".transition-discrete { transition-behavior: allow-discrete; } .transition-normal { transition-behavior: normal; - } - " + }" `) expect(await run(['-transition-discrete', '-transition-normal'])).toEqual('') @@ -25261,8 +22949,7 @@ test('transition-behavior', async () => { test('delay', async () => { expect(await run(['delay-123', 'delay-200', 'delay-[300ms]'])).toMatchInlineSnapshot(` - " - .delay-123 { + ".delay-123 { transition-delay: .123s; } @@ -25272,8 +22959,7 @@ test('delay', async () => { .delay-\\[300ms\\] { transition-delay: .3s; - } - " + }" `) expect( await run([ @@ -25291,8 +22977,7 @@ test('delay', async () => { test('duration', async () => { expect(await run(['duration-123', 'duration-200', 'duration-[300ms]'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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-duration: initial; @@ -25318,8 +23003,7 @@ test('duration', async () => { @property --tw-duration { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -25336,8 +23020,7 @@ test('duration', async () => { test('ease', async () => { expect( - await run( - ['ease-in', 'ease-out', 'ease-[var(--value)]'], + await compileCss( css` @theme { --ease-in: cubic-bezier(0.4, 0, 1, 1); @@ -25345,10 +23028,10 @@ test('ease', async () => { } @tailwind utilities; `, + ['ease-in', 'ease-out', 'ease-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-ease: initial; @@ -25379,8 +23062,7 @@ test('ease', async () => { @property --tw-ease { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -25394,18 +23076,17 @@ test('ease', async () => { ).toEqual('') expect( - await run( - ['ease-linear'], + await compileCss( css` @theme { --ease-linear: steps(4); } @tailwind utilities; `, + ['ease-linear'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-ease: initial; @@ -25425,8 +23106,7 @@ test('ease', async () => { @property --tw-ease { syntax: "*"; inherits: false - } - " + }" `) }) @@ -25440,8 +23120,7 @@ test('will-change', async () => { 'will-change-[var(--value)]', ]), ).toMatchInlineSnapshot(` - " - .will-change-\\[var\\(--value\\)\\] { + ".will-change-\\[var\\(--value\\)\\] { will-change: var(--value); } @@ -25459,8 +23138,7 @@ test('will-change', async () => { .will-change-transform { will-change: transform; - } - " + }" `) expect( await run([ @@ -25493,8 +23171,7 @@ test('contain', async () => { 'contain-[unset]', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-contain-size: initial; @@ -25564,8 +23241,7 @@ test('contain', async () => { @property --tw-contain-style { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -25583,19 +23259,8 @@ test('contain', async () => { }) test('content', async () => { - expect( - await run( - ['content-slash', 'content-["hello_world"]'], - css` - @theme { - --content-slash: '/'; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - @layer properties { + expect(await run(['content-["hello_world"]'])).toMatchInlineSnapshot(` + "@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: ""; @@ -25603,26 +23268,16 @@ test('content', async () => { } } - :root, :host { - --content-slash: "/"; - } - .content-\\[\\"hello_world\\"\\] { --tw-content: "hello world"; content: var(--tw-content); } - .content-slash { - --tw-content: var(--content-slash); - content: var(--tw-content); - } - @property --tw-content { syntax: "*"; inherits: false; initial-value: ""; - } - " + }" `) expect(await run(['content', '-content-["hello_world"]', 'content-["hello_world"]/foo'])).toEqual( '', @@ -25632,15 +23287,13 @@ test('content', async () => { test('forced-color-adjust', async () => { expect(await run(['forced-color-adjust-none', 'forced-color-adjust-auto'])) .toMatchInlineSnapshot(` - " - .forced-color-adjust-auto { + ".forced-color-adjust-auto { forced-color-adjust: auto; } .forced-color-adjust-none { forced-color-adjust: none; - } - " + }" `) expect( await run([ @@ -25657,8 +23310,7 @@ test('forced-color-adjust', async () => { test('leading', async () => { expect( - await run( - ['leading-tight', 'leading-6', 'leading-[var(--value)]'], + await compileCss( css` @theme { --leading-tight: 1.25; @@ -25666,10 +23318,10 @@ test('leading', async () => { } @tailwind utilities; `, + ['leading-tight', 'leading-6', 'leading-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-leading: initial; @@ -25700,8 +23352,7 @@ test('leading', async () => { @property --tw-leading { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -25716,18 +23367,17 @@ test('leading', async () => { ).toEqual('') expect( - await run( - ['leading-none'], + await compileCss( css` @theme { --leading-none: 2; } @tailwind utilities; `, + ['leading-none'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-leading: initial; @@ -25747,15 +23397,13 @@ test('leading', async () => { @property --tw-leading { syntax: "*"; inherits: false - } - " + }" `) }) test('tracking', async () => { expect( - await run( - ['tracking-normal', 'tracking-wide', 'tracking-[var(--value)]', '-tracking-[var(--value)]'], + await compileCss( css` @theme { --tracking-normal: 0em; @@ -25763,10 +23411,10 @@ test('tracking', async () => { } @tailwind utilities; `, + ['tracking-normal', 'tracking-wide', 'tracking-[var(--value)]', '-tracking-[var(--value)]'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-tracking: initial; @@ -25802,8 +23450,7 @@ test('tracking', async () => { @property --tw-tracking { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -25818,8 +23465,7 @@ test('tracking', async () => { test('font-smoothing', async () => { expect(await run(['antialiased', 'subpixel-antialiased'])).toMatchInlineSnapshot(` - " - .antialiased { + ".antialiased { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -25827,8 +23473,7 @@ test('font-smoothing', async () => { .subpixel-antialiased { -webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto; - } - " + }" `) expect( await run([ @@ -25854,8 +23499,7 @@ test('font-variant-numeric', async () => { 'stacked-fractions', ]), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-ordinal: initial; @@ -25934,8 +23578,7 @@ test('font-variant-numeric', async () => { @property --tw-numeric-fraction { syntax: "*"; inherits: false - } - " + }" `) expect( await run([ @@ -25963,7 +23606,14 @@ test('font-variant-numeric', async () => { test('outline', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --outline-color-blue-500: #3b82f6; + } + @tailwind utilities; + `, [ 'outline', 'outline-hidden', @@ -26013,17 +23663,9 @@ test('outline', async () => { 'outline-[length:var(--my-width)]', 'outline-[percentage:var(--my-width)]', ], - css` - @theme { - --color-red-500: #ef4444; - --outline-color-blue-500: #3b82f6; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-outline-style: solid; @@ -26267,22 +23909,20 @@ test('outline', async () => { syntax: "*"; inherits: false; initial-value: solid; - } - " + }" `) expect( - await run( - ['outline'], + await compileCss( css` @theme { --default-outline-width: 2px; } @tailwind utilities; `, + ['outline'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-outline-style: solid; @@ -26299,8 +23939,7 @@ test('outline', async () => { syntax: "*"; inherits: false; initial-value: solid; - } - " + }" `) expect( await run([ @@ -26352,8 +23991,7 @@ test('outline-offset', async () => { '-outline-offset-[var(--value)]', ]), ).toMatchInlineSnapshot(` - " - .-outline-offset-4 { + ".-outline-offset-4 { outline-offset: calc(4px * -1); } @@ -26367,8 +24005,7 @@ test('outline-offset', async () => { .outline-offset-\\[var\\(--value\\)\\] { outline-offset: var(--value); - } - " + }" `) expect( await run([ @@ -26393,8 +24030,7 @@ test('opacity', async () => { 'opacity-[var(--value)]', ]), ).toMatchInlineSnapshot(` - " - .opacity-2\\.5 { + ".opacity-2\\.5 { opacity: .025; } @@ -26412,8 +24048,7 @@ test('opacity', async () => { .opacity-\\[var\\(--value\\)\\] { opacity: var(--value); - } - " + }" `) expect( await run([ @@ -26431,7 +24066,12 @@ test('opacity', async () => { test('underline-offset', async () => { expect( - await run( + await compileCss( + css` + @theme { + } + @tailwind utilities; + `, [ 'underline-offset-auto', 'underline-offset-4', @@ -26441,15 +24081,9 @@ test('underline-offset', async () => { 'underline-offset-[var(--value)]', '-underline-offset-[var(--value)]', ], - css` - @theme { - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - .-underline-offset-4 { + ".-underline-offset-4 { text-underline-offset: calc(4px * -1); } @@ -26475,8 +24109,7 @@ test('underline-offset', async () => { .underline-offset-auto { text-underline-offset: auto; - } - " + }" `) expect( await run([ @@ -26495,31 +24128,40 @@ test('underline-offset', async () => { ).toEqual('') expect( - await run( - ['underline-offset-auto'], + await compileCss( css` @theme { --text-underline-offset-auto: 4px; } @tailwind utilities; `, + ['underline-offset-auto'], ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --text-underline-offset-auto: 4px; } .underline-offset-auto { text-underline-offset: var(--text-underline-offset-auto); - } - " + }" `) }) test('text', async () => { expect( - await run( + await compileCss( + css` + @theme { + --spacing: 0.25rem; + --color-red-500: #ef4444; + --text-color-blue-500: #3b82f6; + --text-sm: 0.875rem; + --text-sm--line-height: 1.25rem; + --leading-snug: 1.375; + } + @tailwind utilities; + `, [ // color 'text-red-500', @@ -26549,18 +24191,15 @@ test('text', async () => { 'text-[color:var(--my-color)]/50', 'text-[color:var(--my-color)]/[0.5]', 'text-[color:var(--my-color)]/[50%]', - 'text-[--alpha(red/20%)]', // font-size / line-height / letter-spacing / font-weight 'text-sm', - 'text-sm/0', 'text-sm/6', 'text-sm/none', 'text-[10px]/none', 'text-sm/snug', 'text-sm/[4px]', 'text-[12px]', - 'text-[12px]/0', 'text-[12px]/6', 'text-[50%]', 'text-[50%]/6', @@ -26575,23 +24214,10 @@ test('text', async () => { 'text-[clamp(1rem,2rem,3rem)]', 'text-[clamp(1rem,var(--size),3rem)]', 'text-[clamp(1rem,var(--size),3rem)]/9', - 'text-[--spacing(2)]', ], - css` - @theme { - --spacing: 0.25rem; - --color-red-500: #ef4444; - --text-color-blue-500: #3b82f6; - --text-sm: 0.875rem; - --text-sm--line-height: 1.25rem; - --leading-snug: 1.375; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --spacing: .25rem; --color-red-500: #ef4444; --text-color-blue-500: #3b82f6; @@ -26605,11 +24231,6 @@ test('text', async () => { line-height: 1; } - .text-\\[12px\\]\\/0 { - font-size: 12px; - line-height: 0; - } - .text-\\[12px\\]\\/6 { font-size: 12px; line-height: calc(var(--spacing) * 6); @@ -26640,11 +24261,6 @@ test('text', async () => { line-height: var(--tw-leading, var(--text-sm--line-height)); } - .text-sm\\/0 { - font-size: var(--text-sm); - line-height: 0; - } - .text-sm\\/6 { font-size: var(--text-sm); line-height: calc(var(--spacing) * 6); @@ -26665,10 +24281,6 @@ test('text', async () => { line-height: var(--leading-snug); } - .text-\\[--spacing\\(2\\)\\] { - font-size: calc(var(--spacing) * 2); - } - .text-\\[12px\\] { font-size: 12px; } @@ -26709,10 +24321,6 @@ test('text', async () => { color: oklab(59.9824% -.067 -.124 / .5); } - .text-\\[--alpha\\(red\\/20\\%\\)\\] { - color: oklab(62.7955% .224 .125 / .2); - } - .text-\\[color\\:var\\(--my-color\\)\\], .text-\\[color\\:var\\(--my-color\\)\\]\\/50 { color: var(--my-color); } @@ -26877,11 +24485,16 @@ test('text', async () => { .text-transparent { color: #0000; - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme inline reference { + --text-sm: 0.875rem; + } + @tailwind utilities; + `, [ 'text', // color @@ -26907,19 +24520,21 @@ test('text', async () => { '-text-sm/[4px]', 'text-[10px]/foo', ], - css` - @theme inline reference { - --text-sm: 0.875rem; - } - @tailwind utilities; - `, ), ).toEqual('') }) test('text-shadow', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.1); + --text-shadow-sm: 0px 1px 2px rgb(0 0 0 / 0.06), 0px 2px 2px rgb(0 0 0 / 0.06); + } + @tailwind utilities; + `, [ // Shadows 'text-shadow-2xs', @@ -26932,7 +24547,6 @@ test('text-shadow', async () => { 'text-shadow-[shadow:var(--value)]', 'text-shadow-sm/25', - 'text-shadow-sm/12.5', 'text-shadow-[12px_12px_#0088cc]/25', 'text-shadow-[12px_12px_var(--value)]/25', 'text-shadow-[10px_10px]/25', @@ -26960,18 +24574,9 @@ test('text-shadow', async () => { 'text-shadow-[color:var(--value)]/[0.5]', 'text-shadow-[color:var(--value)]/[50%]', ], - css` - @theme { - --color-red-500: #ef4444; - --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.1); - --text-shadow-sm: 0px 1px 2px rgb(0 0 0 / 0.06), 0px 2px 2px rgb(0 0 0 / 0.06); - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-text-shadow-color: initial; @@ -27011,11 +24616,6 @@ test('text-shadow', async () => { text-shadow: 12px 12px var(--tw-text-shadow-color, oklab(59.9824% -.067 -.124 / .25)); } - .text-shadow-sm\\/12\\.5 { - --tw-text-shadow-alpha: 12.5%; - text-shadow: 0px 1px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .125)), 0px 2px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .125)); - } - .text-shadow-sm\\/25 { --tw-text-shadow-alpha: 25%; text-shadow: 0px 1px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .25)), 0px 2px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .25)); @@ -27262,46 +24862,43 @@ test('text-shadow', async () => { syntax: ""; inherits: false; initial-value: 100%; - } - " + }" `) expect( - await run( - [ - '-text-shadow-sm', - '-text-shadow-none', - '-text-shadow-red-500', - '-text-shadow-red-500/50', - '-text-shadow-red-500/[0.5]', - '-text-shadow-red-500/[50%]', - '-text-shadow-current', - '-text-shadow-current/50', - '-text-shadow-current/[0.5]', - '-text-shadow-current/[50%]', - '-text-shadow-inherit', - '-text-shadow-transparent', - '-text-shadow-[#0088cc]', - '-text-shadow-[#0088cc]/50', - '-text-shadow-[#0088cc]/[0.5]', - '-text-shadow-[#0088cc]/[50%]', - '-text-shadow-[var(--value)]', - 'text-shadow/foo', - 'text-shadow-sm/foo', - ], - css` - @theme { - --text-shadow: 0 1px 1px rgb(0 0 0 / 0.05); - --text-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06); - } - @tailwind utilities; - `, - ), + await run([ + '-shadow-xl', + '-shadow-none', + '-shadow-red-500', + '-shadow-red-500/50', + '-shadow-red-500/[0.5]', + '-shadow-red-500/[50%]', + '-shadow-current', + '-shadow-current/50', + '-shadow-current/[0.5]', + '-shadow-current/[50%]', + '-shadow-inherit', + '-shadow-transparent', + '-shadow-[#0088cc]', + '-shadow-[#0088cc]/50', + '-shadow-[#0088cc]/[0.5]', + '-shadow-[#0088cc]/[50%]', + '-shadow-[var(--value)]', + ]), ).toEqual('') }) test('shadow', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --box-shadow-color-blue-500: #3b82f6; + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + } + @tailwind utilities; + `, [ // Shadows 'shadow-sm', @@ -27314,7 +24911,6 @@ test('shadow', async () => { 'shadow-[shadow:var(--value)]', 'shadow-sm/25', - 'shadow-sm/12.5', 'shadow-[12px_12px_#0088cc]/25', 'shadow-[12px_12px_var(--value)]/25', 'shadow-[10px_10px]/25', @@ -27343,19 +24939,9 @@ test('shadow', async () => { 'shadow-[color:var(--value)]/[0.5]', 'shadow-[color:var(--value)]/[50%]', ], - css` - @theme { - --color-red-500: #ef4444; - --box-shadow-color-blue-500: #3b82f6; - --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-shadow: 0 0 #0000; @@ -27417,12 +25003,6 @@ test('shadow', async () => { box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .shadow-sm\\/12\\.5 { - --tw-shadow-alpha: 12.5%; - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, oklab(0% 0 0 / .125)), 0 1px 2px -1px var(--tw-shadow-color, oklab(0% 0 0 / .125)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-sm\\/25 { --tw-shadow-alpha: 25%; --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, oklab(0% 0 0 / .25)), 0 1px 2px -1px var(--tw-shadow-color, oklab(0% 0 0 / .25)); @@ -27755,48 +25335,42 @@ test('shadow', async () => { syntax: "*"; inherits: false; initial-value: 0 0 #0000; - } - " + }" `) expect( - await run( - [ - '-shadow-sm', - '-shadow-none', - '-shadow-red-500', - '-shadow-red-500/50', - '-shadow-red-500/[0.5]', - '-shadow-red-500/[50%]', - '-shadow-current', - '-shadow-current/50', - '-shadow-current/[0.5]', - '-shadow-current/[50%]', - '-shadow-inherit', - '-shadow-transparent', - '-shadow-[#0088cc]', - '-shadow-[#0088cc]/50', - '-shadow-[#0088cc]/[0.5]', - '-shadow-[#0088cc]/[50%]', - '-shadow-[var(--value)]', - 'shadow/foo', - 'shadow-sm/foo', - 'shadow-[10px_10px]/foo', - 'shadow-[#0088cc]/foo', - ], - css` - @theme { - --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); - --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - } - @tailwind utilities; - `, - ), + await run([ + '-shadow-xl', + '-shadow-none', + '-shadow-red-500', + '-shadow-red-500/50', + '-shadow-red-500/[0.5]', + '-shadow-red-500/[50%]', + '-shadow-current', + '-shadow-current/50', + '-shadow-current/[0.5]', + '-shadow-current/[50%]', + '-shadow-inherit', + '-shadow-transparent', + '-shadow-[#0088cc]', + '-shadow-[#0088cc]/50', + '-shadow-[#0088cc]/[0.5]', + '-shadow-[#0088cc]/[50%]', + '-shadow-[var(--value)]', + ]), ).toEqual('') }) test('inset-shadow', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --inset-shadow: inset 0 2px 4px rgb(0 0 0 / 0.05); + --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); + } + @tailwind utilities; + `, [ // Shadows 'inset-shadow', @@ -27810,7 +25384,6 @@ test('inset-shadow', async () => { 'inset-shadow-[12px_12px_#0088cc,12px_12px_var(--value,#0088cc)]', 'inset-shadow-sm/25', - 'inset-shadow-sm/12.5', 'inset-shadow-[12px_12px_#0088cc]/25', 'inset-shadow-[12px_12px_var(--value)]/25', 'inset-shadow-[10px_10px]/25', @@ -27839,18 +25412,9 @@ test('inset-shadow', async () => { 'inset-shadow-[color:var(--value)]/[0.5]', 'inset-shadow-[color:var(--value)]/[50%]', ], - css` - @theme { - --color-red-500: #ef4444; - --inset-shadow: inset 0 2px 4px rgb(0 0 0 / 0.05); - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-shadow: 0 0 #0000; @@ -27926,12 +25490,6 @@ test('inset-shadow', async () => { box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .inset-shadow-sm\\/12\\.5 { - --tw-inset-shadow-alpha: 12.5%; - --tw-inset-shadow: inset 0 1px 1px var(--tw-inset-shadow-color, oklab(0% 0 0 / .125)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .inset-shadow-sm\\/25 { --tw-inset-shadow-alpha: 25%; --tw-inset-shadow: inset 0 1px 1px var(--tw-inset-shadow-color, oklab(0% 0 0 / .25)); @@ -27969,7 +25527,7 @@ test('inset-shadow', async () => { } .inset-shadow-none { - --tw-inset-shadow: inset 0 0 #0000; + --tw-inset-shadow: 0 0 #0000; box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } @@ -28259,47 +25817,41 @@ test('inset-shadow', async () => { syntax: "*"; inherits: false; initial-value: 0 0 #0000; - } - " + }" `) expect( - await run( - [ - '-inset-shadow-sm', - '-inset-shadow-none', - '-inset-shadow-red-500', - '-inset-shadow-red-500/50', - '-inset-shadow-red-500/[0.5]', - '-inset-shadow-red-500/[50%]', - '-inset-shadow-current', - '-inset-shadow-current/50', - '-inset-shadow-current/[0.5]', - '-inset-shadow-current/[50%]', - '-inset-shadow-inherit', - '-inset-shadow-transparent', - '-inset-shadow-[#0088cc]', - '-inset-shadow-[#0088cc]/50', - '-inset-shadow-[#0088cc]/[0.5]', - '-inset-shadow-[#0088cc]/[50%]', - '-inset-shadow-[var(--value)]', - 'inset-shadow/foo', - 'inset-shadow-sm/foo', - 'inset-shadow-[10px_10px]/foo', - ], - css` - @theme { - --inset-shadow: inset 0 1px 3px 0 rgb(0 0 0 / 0.1); - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - } - @tailwind utilities; - `, - ), + await run([ + '-inset-shadow-sm', + '-inset-shadow-none', + '-inset-shadow-red-500', + '-inset-shadow-red-500/50', + '-inset-shadow-red-500/[0.5]', + '-inset-shadow-red-500/[50%]', + '-inset-shadow-current', + '-inset-shadow-current/50', + '-inset-shadow-current/[0.5]', + '-inset-shadow-current/[50%]', + '-inset-shadow-inherit', + '-inset-shadow-transparent', + '-inset-shadow-[#0088cc]', + '-inset-shadow-[#0088cc]/50', + '-inset-shadow-[#0088cc]/[0.5]', + '-inset-shadow-[#0088cc]/[50%]', + '-inset-shadow-[var(--value)]', + ]), ).toEqual('') }) test('ring', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --ring-color-blue-500: #3b82f6; + } + @tailwind utilities; + `, [ // ring color 'ring-inset', @@ -28339,17 +25891,9 @@ test('ring', async () => { 'ring-[12px]', 'ring-[length:var(--my-width)]', ], - css` - @theme { - --color-red-500: #ef4444; - --ring-color-blue-500: #3b82f6; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-shadow: 0 0 #0000; @@ -28406,7 +25950,7 @@ test('ring', async () => { } .ring-\\[length\\:var\\(--my-width\\)\\] { - --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(var(--my-width) + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(var(--my-width) + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } @@ -28665,22 +26209,20 @@ test('ring', async () => { syntax: "*"; inherits: false; initial-value: 0 0 #0000; - } - " + }" `) expect( - await run( - ['ring'], + await compileCss( css` @theme { --default-ring-width: 2px; } @tailwind utilities; `, + ['ring'], ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-shadow: 0 0 #0000; @@ -28783,8 +26325,7 @@ test('ring', async () => { syntax: "*"; inherits: false; initial-value: 0 0 #0000; - } - " + }" `) expect( await run([ @@ -28826,7 +26367,13 @@ test('ring', async () => { test('inset-ring', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + } + @tailwind utilities; + `, [ // ring color 'inset-ring-red-500', @@ -28864,16 +26411,9 @@ test('inset-ring', async () => { 'inset-ring-[12px]', 'inset-ring-[length:var(--my-width)]', ], - css` - @theme { - --color-red-500: #ef4444; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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-shadow: 0 0 #0000; @@ -29180,8 +26720,7 @@ test('inset-ring', async () => { syntax: "*"; inherits: false; initial-value: 0 0 #0000; - } - " + }" `) expect( await run([ @@ -29222,7 +26761,14 @@ test('inset-ring', async () => { test('ring-offset', async () => { expect( - await run( + await compileCss( + css` + @theme { + --color-red-500: #ef4444; + --ring-offset-color-blue-500: #3b82f6; + } + @tailwind utilities; + `, [ // ring color 'ring-offset-inset', @@ -29259,17 +26805,9 @@ test('ring-offset', async () => { 'ring-offset-[12px]', 'ring-offset-[length:var(--my-width)]', ], - css` - @theme { - --color-red-500: #ef4444; - --ring-offset-color-blue-500: #3b82f6; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root, :host { --color-red-500: #ef4444; --ring-offset-color-blue-500: #3b82f6; } @@ -29446,8 +26984,7 @@ test('ring-offset', async () => { .ring-offset-transparent { --tw-ring-offset-color: transparent; - } - " + }" `) expect( await run([ @@ -29497,8 +27034,7 @@ test('@container', async () => { '@container-size/sidebar', ]), ).toMatchInlineSnapshot(` - " - .\\@container-normal\\/sidebar { + ".\\@container-normal\\/sidebar { container: sidebar; } @@ -29520,8 +27056,7 @@ test('@container', async () => { .\\@container-size { container-type: size; - } - " + }" `) expect( await run([ @@ -29537,69 +27072,58 @@ test('@container', async () => { describe('spacing utilities', () => { test('`--spacing: initial` disables the spacing multiplier', async () => { - expect( - await run( - ['px-1', 'px-4'], - css` - @theme { - --spacing: initial; - --spacing-4: 1rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + let { build } = await compile(css` + @theme { + --spacing: initial; + --spacing-4: 1rem; + } + @tailwind utilities; + `) + let compiled = build(['px-1', 'px-4']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + ":root, :host { --spacing-4: 1rem; } .px-4 { padding-inline: var(--spacing-4); - } - " + }" `) }) test('`--spacing-*: initial` disables the spacing multiplier', async () => { - expect( - await run( - ['px-1', 'px-4'], - css` - @theme { - --spacing-*: initial; - --spacing-4: 1rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + let { build } = await compile(css` + @theme { + --spacing-*: initial; + --spacing-4: 1rem; + } + @tailwind utilities; + `) + let compiled = build(['px-1', 'px-4']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + ":root, :host { --spacing-4: 1rem; } .px-4 { padding-inline: var(--spacing-4); - } - " + }" `) }) test('only multiples of 0.25 with no trailing zeroes are supported with the spacing multiplier', async () => { - expect( - await run( - ['px-0.25', 'px-1.5', 'px-2.75', 'px-0.375', 'px-2.50', 'px-.75'], - css` - @theme { - --spacing: 4px; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + let { build } = await compile(css` + @theme { + --spacing: 4px; + } + @tailwind utilities; + `) + let compiled = build(['px-0.25', 'px-1.5', 'px-2.75', 'px-0.375', 'px-2.50', 'px-.75']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + ":root, :host { --spacing: 4px; } @@ -29613,40 +27137,34 @@ describe('spacing utilities', () => { .px-2\\.75 { padding-inline: calc(var(--spacing) * 2.75); - } - " + }" `) }) test('spacing utilities must have a value', async () => { - expect( - await run( - ['px'], - css` - @theme reference { - --spacing: 4px; - } - @tailwind utilities; - `, - ), - ).toEqual('') + let { build } = await compile(css` + @theme reference { + --spacing: 4px; + } + @tailwind utilities; + `) + let compiled = build(['px']) + + expect(optimizeCss(compiled).trim()).toEqual('') }) test('--spacing-* variables take precedence over --container-* variables', async () => { - expect( - await run( - ['w-sm', 'max-w-sm', 'min-w-sm', 'basis-sm'], - css` - @theme { - --spacing-sm: 8px; - --container-sm: 256px; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + let { build } = await compile(css` + @theme { + --spacing-sm: 8px; + --container-sm: 256px; + } + @tailwind utilities; + `) + let compiled = build(['w-sm', 'max-w-sm', 'min-w-sm', 'basis-sm']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + ":root, :host { --spacing-sm: 8px; } @@ -29664,77 +27182,31 @@ describe('spacing utilities', () => { .basis-sm { flex-basis: var(--spacing-sm); - } - " + }" `) }) }) describe('custom utilities', () => { - test.each([ - ['foo', true], // Simple name - ['foo-123', true], // Ending with a number is valid - ['foo-2.5', true], // Dots are valid when surrounded by numbers - ['-foo', true], // Simple name with negative sign - ['foo-bar', true], // With dashes - ['foo_bar', true], // With underscores - ['foo-50%', true], // Bare value with percentage - ['foo-1/2', true], // Bare value with fraction - ['foo-sm/8', true], // Bare value with number modifier - ['foo-4/snug', true], // Bare value with named modifier - ['foo_', true], // This is supported today, so let's not break it - ['foo/bar', true], // A slash to separate the modifier is valid. - - ['Foo', false], // Starting with uppercase letter is invalid - ['-Foo', false], // Starting with uppercase letter is invalid (negative) - ['foo-', false], // Should not end with a dash - ['foo-1/', false], // Invalid fraction/modifier - ['foo-p%', false], // Invalid percentage - ['foo.bar', false], // Dots are only valid when surrounded by numbers - ['foo-1..5', false], // Double dots are invalid - ['foo..bar', false], // Double dots are invalid definitely without numbers - ['foo/bar/baz', false], // Multiple slashes are invalid - ])('valid static utility name "%s" (%s)', (name, valid) => { - expect(isValidStaticUtilityName(name)).toBe(valid) - }) - - test.each([ - ['foo', false], // Simple name, missing '-*' suffix - ['foo-*', true], // Simple name - ['foo--*', true], // Root ending in `-` is valid (e.g. `border--*`) - ['-foo-*', true], // Simple name (negative) - ['foo-bar-*', true], // With dashes - ['foo_bar-*', true], // With underscores - ['Foo-*', false], // Starting with uppercase letter is invalid - ['-Foo-*', false], // Starting with uppercase letter is invalid - ['foo!-*', false], // Invalid special character - ['foo-[…]', false], // Invalid special character - ])('valid functional name "%s" (%s)', (name, valid) => { - expect(isValidFunctionalUtilityName(name)).toBe(valid) - }) - test('custom static utility', async () => { - expect( - await run( - ['text-trim', 'lg:text-trim'], - css` - @layer utilities { - @tailwind utilities; - } - - @theme reference { - --breakpoint-lg: 1024px; - } - - @utility text-trim { - text-box-trim: both; - text-box-edge: cap alphabetic; - } - `, - ), - ).toMatchInlineSnapshot(` - " + let { build } = await compile(css` @layer utilities { + @tailwind utilities; + } + + @theme reference { + --breakpoint-lg: 1024px; + } + + @utility text-trim { + text-box-trim: both; + text-box-edge: cap alphabetic; + } + `) + let compiled = build(['text-trim', 'lg:text-trim']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .text-trim { text-box-trim: both; text-box-edge: cap alphabetic; @@ -29746,13 +27218,12 @@ describe('custom utilities', () => { text-box-edge: cap alphabetic; } } - } - " + }" `) }) test('custom static utility emit CSS variables if the utility is used', async () => { - let input = css` + let { build } = await compile(css` @layer utilities { @tailwind utilities; } @@ -29764,19 +27235,16 @@ describe('custom utilities', () => { @utility foo { value: var(--example-foo); } - ` + `) + let compiled = build([]) // `foo` is not used yet: - expect(await compileCss(input)).toMatchInlineSnapshot(` - " - @layer utilities; - " - `) + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`"@layer utilities;"`) // `foo` is used, and the CSS variable is emitted: - expect(await run(['foo'], input)).toMatchInlineSnapshot(` - " - @layer utilities { + compiled = build(['foo']) + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo { value: var(--example-foo); } @@ -29784,153 +27252,133 @@ describe('custom utilities', () => { :root, :host { --example-foo: 123px; - } - " + }" `) }) test('custom static utility (negative)', async () => { - expect( - await run( - ['-example', 'lg:-example'], - css` - @layer utilities { - @tailwind utilities; - } - - @utility -example { - value: -1; - } - `, - ), - ).toMatchInlineSnapshot(` - " + let { build } = await compile(css` @layer utilities { + @tailwind utilities; + } + + @utility -example { + value: -1; + } + `) + let compiled = build(['-example', 'lg:-example']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .-example { value: -1; } - } - " + }" `) }) test('Multiple static utilities are merged', async () => { - expect( - await run( - ['really-round'], - css` - @layer utilities { - @tailwind utilities; - } - - @utility really-round { - --custom-prop: hi; - border-radius: 50rem; - } - - @utility really-round { - border-radius: 30rem; - } - `, - ), - ).toMatchInlineSnapshot(` - " + let { build } = await compile(css` @layer utilities { + @tailwind utilities; + } + + @utility really-round { + --custom-prop: hi; + border-radius: 50rem; + } + + @utility really-round { + border-radius: 30rem; + } + `) + let compiled = build(['really-round']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .really-round { --custom-prop: hi; border-radius: 30rem; } - } - " + }" `) }) test('custom utilities support some special characters', async () => { - expect( - await run( - ['push-1/2', 'push-50%'], - css` - @layer utilities { - @tailwind utilities; - } - - @utility push-1/2 { - right: 50%; - } - - @utility push-50% { - right: 50%; - } - `, - ), - ).toMatchInlineSnapshot(` - " + let { build } = await compile(css` @layer utilities { + @tailwind utilities; + } + + @utility push-1/2 { + right: 50%; + } + + @utility push-50% { + right: 50%; + } + `) + let compiled = build(['push-1/2', 'push-50%']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .push-1\\/2, .push-50\\% { right: 50%; } - } - " + }" `) }) test('can override specific versions of a functional utility with a static utility', async () => { - expect( - await run( - ['text-sm'], - css` - @layer utilities { - @tailwind utilities; - } - - @theme reference { - --text-sm: 0.875rem; - --text-sm--line-height: 1.25rem; - } - - @utility text-sm { - font-size: var(--text-sm, 0.8755rem); - line-height: var(--text-sm--line-height, 1.255rem); - text-rendering: optimizeLegibility; - } - `, - ), - ).toMatchInlineSnapshot(` - " + let { build } = await compile(css` @layer utilities { + @tailwind utilities; + } + + @theme reference { + --text-sm: 0.875rem; + --text-sm--line-height: 1.25rem; + } + + @utility text-sm { + font-size: var(--text-sm, 0.8755rem); + line-height: var(--text-sm--line-height, 1.255rem); + text-rendering: optimizeLegibility; + } + `) + let compiled = build(['text-sm']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .text-sm { font-size: var(--text-sm, .8755rem); line-height: var(--text-sm--line-height, 1.255rem); - text-rendering: optimizelegibility; + text-rendering: optimizeLegibility; font-size: var(--text-sm, .875rem); line-height: var(--tw-leading, var(--text-sm--line-height, 1.25rem)); } - } - " + }" `) }) test('can override the default value of a functional utility', async () => { - expect( - await run( - ['rounded', 'rounded-xl', 'rounded-[33px]'], - css` - @layer utilities { - @tailwind utilities; - } - - @theme reference { - --radius-xl: 16px; - } - - @utility rounded { - border-radius: 50rem; - } - `, - ), - ).toMatchInlineSnapshot(` - " + let { build } = await compile(css` @layer utilities { + @tailwind utilities; + } + + @theme reference { + --radius-xl: 16px; + } + + @utility rounded { + border-radius: 50rem; + } + `) + let compiled = build(['rounded', 'rounded-xl', 'rounded-[33px]']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .rounded { border-radius: 50rem; } @@ -29942,28 +27390,24 @@ describe('custom utilities', () => { .rounded-xl { border-radius: var(--radius-xl, 16px); } - } - " + }" `) }) test('custom utilities are sorted by used properties', async () => { - expect( - await run( - ['top-[100px]', 'push-left', 'right-[100px]', 'bottom-[100px]'], - css` - @layer utilities { - @tailwind utilities; - } - - @utility push-left { - right: 100%; - } - `, - ), - ).toMatchInlineSnapshot(` - " + let { build } = await compile(css` @layer utilities { + @tailwind utilities; + } + + @utility push-left { + right: 100%; + } + `) + let compiled = build(['top-[100px]', 'push-left', 'right-[100px]', 'bottom-[100px]']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .top-\\[100px\\] { top: 100px; } @@ -29979,8 +27423,7 @@ describe('custom utilities', () => { .bottom-\\[100px\\] { bottom: 100px; } - } - " + }" `) }) @@ -29991,7 +27434,7 @@ describe('custom utilities', () => { right: 100%; } `), - ).rejects.toThrow(/should be alphanumeric/) + ).rejects.toThrowError(/should be alphanumeric/) await expect(() => compile(css` @@ -29999,7 +27442,7 @@ describe('custom utilities', () => { right: 100%; } `), - ).rejects.toThrow(/should be alphanumeric/) + ).rejects.toThrowError(/should be alphanumeric/) await expect(() => compile(css` @@ -30007,13 +27450,12 @@ describe('custom utilities', () => { right: 100%; } `), - ).rejects.toThrow(/should be alphanumeric/) + ).rejects.toThrowError(/should be alphanumeric/) }) test('custom utilities work with `@apply`', async () => { expect( - await run( - ['foo', 'hover:foo', 'bar'], + await compileCss( css` @utility foo { @apply flex flex-col underline; @@ -30029,10 +27471,10 @@ describe('custom utilities', () => { @tailwind utilities; `, + ['foo', 'hover:foo', 'bar'], ), ).toMatchInlineSnapshot(` - " - .bar { + ".bar { z-index: 10; } @@ -30052,15 +27494,13 @@ describe('custom utilities', () => { text-decoration-line: underline; display: flex; } - } - " + }" `) }) test('referencing custom utilities in custom utilities via `@apply` should work', async () => { expect( - await run( - ['bar'], + await compileCss( css` @utility foo { @apply flex flex-col underline; @@ -30072,10 +27512,10 @@ describe('custom utilities', () => { @tailwind utilities; `, + ['bar'], ), ).toMatchInlineSnapshot(` - " - .bar { + ".bar { flex-wrap: wrap; } @@ -30085,15 +27525,13 @@ describe('custom utilities', () => { text-decoration-line: underline; display: flex; } - } - " + }" `) }) test('custom utilities with `@apply` causing circular dependencies should error', async () => { await expect(() => - run( - ['foo', 'bar'], + compileCss( css` @utility foo { @apply flex-wrap hover:bar; @@ -30105,6 +27543,7 @@ describe('custom utilities', () => { @tailwind utilities; `, + ['foo', 'bar'], ), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: You cannot \`@apply\` the \`hover:bar\` utility here because it creates a circular dependency.]`, @@ -30113,8 +27552,7 @@ describe('custom utilities', () => { test('custom utilities with `@apply` causing circular dependencies should error (deeply nesting)', async () => { await expect(() => - run( - ['foo', 'bar'], + compileCss( css` @utility foo { .bar { @@ -30136,6 +27574,7 @@ describe('custom utilities', () => { @tailwind utilities; `, + ['foo', 'bar'], ), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: You cannot \`@apply\` the \`hover:bar\` utility here because it creates a circular dependency.]`, @@ -30144,8 +27583,7 @@ describe('custom utilities', () => { test('custom utilities with `@apply` causing circular dependencies should error (multiple levels)', async () => { await expect(() => - run( - ['foo', 'bar'], + compileCss( css` body { @apply foo; @@ -30165,6 +27603,7 @@ describe('custom utilities', () => { @tailwind utilities; `, + ['foo', 'bar'], ), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: You cannot \`@apply\` the \`hover:bar\` utility here because it creates a circular dependency.]`, @@ -30172,225 +27611,148 @@ describe('custom utilities', () => { }) describe('functional utilities', () => { - test('functional utilities require a `--value(…)`', async () => { - let input = css` - @utility example-* { - --resolved-value: 4; - } - - @tailwind utilities; - ` - - expect(await run(['example', 'example-foo'], input)).toEqual('') - }) - - test('functional utilities must resolve at least one `--value(…)`', async () => { - let input = css` - @utility example-* { - --resolved-value: --value(integer); - } - - @tailwind utilities; - ` - - expect(await run(['example-1', 'example-2'], input)).toMatchInlineSnapshot(` - " - .example-1 { - --resolved-value: 1; - } - - .example-2 { - --resolved-value: 2; - } - " - `) - - expect(await run(['example', 'example-foo', 'example-2.5'], input)).toEqual('') - }) - test('resolving values from `@theme`', async () => { let input = css` @theme reference { - --example-1: 1; - --example-2: 2; - --example-4: 4; - --example-a: 8; + --tab-size-1: 1; + --tab-size-2: 2; + --tab-size-4: 4; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value(--example); + @utility tab-* { + tab-size: --value(--tab-size); } @tailwind utilities; ` - expect(await run(['example-1', 'example-2', 'example-4', 'example-a'], input)) + expect(await compileCss(input, ['tab-1', 'tab-2', 'tab-4', 'tab-github'])) .toMatchInlineSnapshot(` - " - .example-1 { - --resolved-value: var(--example-1, 1); + ".tab-1 { + tab-size: var(--tab-size-1, 1); } - .example-2 { - --resolved-value: var(--example-2, 2); + .tab-2 { + tab-size: var(--tab-size-2, 2); } - .example-4 { - --resolved-value: var(--example-4, 4); + .tab-4 { + tab-size: var(--tab-size-4, 4); } - .example-a { - --resolved-value: var(--example-a, 8); - } - " + .tab-github { + tab-size: var(--tab-size-github, 8); + }" `) - expect(await run(['example-3', 'example-gitlab'], input)).toEqual('') + expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('') }) - test('functional utility with double-dash separator', async () => { - let input = css` - @theme reference { - --color-border-0: #e5e7eb; - --color-border-1: #d1d5db; - --color-border-2: #9ca3af; - } - - @utility border--* { - border-color: --value(--color-border-*, [color]); - } - - @tailwind utilities; - ` - - expect(await run(['border--0', 'border--1', 'border--2'], input)).toMatchInlineSnapshot(` - " - .border--0 { - border-color: var(--color-border-0, #e5e7eb); - } - - .border--1 { - border-color: var(--color-border-1, #d1d5db); - } - - .border--2 { - border-color: var(--color-border-2, #9ca3af); - } - " - `) - expect(await run(['border--3'], input)).toEqual('') - }) - - test('resolving values from `@theme`, with `--example-*` syntax', async () => { + test('resolving values from `@theme`, with `--tab-size-*` syntax', async () => { let input = // Explicitly not using the css tagged template literal so that - // Prettier doesn't format the `value(--example-*)` as - // `value(--example- *)` + // Prettier doesn't format the `value(--tab-size-*)` as + // `value(--tab-size- *)` ` @theme reference { - --example-1: 1; - --example-2: 2; - --example-4: 4; - --example-a: 8; + --tab-size-1: 1; + --tab-size-2: 2; + --tab-size-4: 4; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value(--example-*); + @utility tab-* { + tab-size: --value(--tab-size-*); } @tailwind utilities; ` - expect(await run(['example-1', 'example-2', 'example-4', 'example-a'], input)) + expect(await compileCss(input, ['tab-1', 'tab-2', 'tab-4', 'tab-github'])) .toMatchInlineSnapshot(` - " - .example-1 { - --resolved-value: var(--example-1, 1); + ".tab-1 { + tab-size: var(--tab-size-1, 1); } - .example-2 { - --resolved-value: var(--example-2, 2); + .tab-2 { + tab-size: var(--tab-size-2, 2); } - .example-4 { - --resolved-value: var(--example-4, 4); + .tab-4 { + tab-size: var(--tab-size-4, 4); } - .example-a { - --resolved-value: var(--example-a, 8); - } - " + .tab-github { + tab-size: var(--tab-size-github, 8); + }" `) - expect(await run(['example-3', 'example-gitlab'], input)).toEqual('') + expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('') }) - test('resolving values from `@theme`, with `--example-\\*` syntax (prettier friendly)', async () => { + test('resolving values from `@theme`, with `--tab-size-\\*` syntax (prettier friendly)', async () => { let input = css` @theme reference { - --example-1: 1; - --example-2: 2; - --example-4: 4; - --example-a: 8; + --tab-size-1: 1; + --tab-size-2: 2; + --tab-size-4: 4; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value(--example-\*); + @utility tab-* { + tab-size: --value(--tab-size-\*); } @tailwind utilities; ` - expect(await run(['example-1', 'example-2', 'example-4', 'example-a'], input)) + expect(await compileCss(input, ['tab-1', 'tab-2', 'tab-4', 'tab-github'])) .toMatchInlineSnapshot(` - " - .example-1 { - --resolved-value: var(--example-1, 1); + ".tab-1 { + tab-size: var(--tab-size-1, 1); } - .example-2 { - --resolved-value: var(--example-2, 2); + .tab-2 { + tab-size: var(--tab-size-2, 2); } - .example-4 { - --resolved-value: var(--example-4, 4); + .tab-4 { + tab-size: var(--tab-size-4, 4); } - .example-a { - --resolved-value: var(--example-a, 8); - } - " + .tab-github { + tab-size: var(--tab-size-github, 8); + }" `) - expect(await run(['example-3', 'example-gitlab'], input)).toEqual('') + expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('') }) test('resolving bare values', async () => { let input = css` - @utility example-* { - --resolved-value: --value(integer); + @utility tab-* { + tab-size: --value(integer); } @tailwind utilities; ` - expect(await run(['example-1', 'example-76', 'example-971'], input)).toMatchInlineSnapshot(` - " - .example-1 { - --resolved-value: 1; + expect(await compileCss(input, ['tab-1', 'tab-76', 'tab-971'])).toMatchInlineSnapshot(` + ".tab-1 { + tab-size: 1; } - .example-76 { - --resolved-value: 76; + .tab-76 { + tab-size: 76; } - .example-971 { - --resolved-value: 971; - } - " + .tab-971 { + tab-size: 971; + }" `) - expect(await run(['example-foo'], input)).toEqual('') + expect(await compileCss(input, ['tab-foo'])).toEqual('') }) test('bare values with unsupported data types should result in a warning', async () => { - using spy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + let spy = vi.spyOn(console, 'warn').mockImplementation(() => {}) let input = css` @utility paint-* { paint: --value([color], color); @@ -30399,7 +27761,7 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['paint-#0088cc', 'paint-red'], input)).toEqual('') + expect(await compileCss(input, ['paint-#0088cc', 'paint-red'])).toMatchInlineSnapshot(`""`) expect(spy.mock.calls).toMatchInlineSnapshot(` [ [ @@ -30419,21 +27781,19 @@ describe('custom utilities', () => { test('resolve literal values', async () => { let input = css` - @utility example-* { - --resolved-value: --value('revert'); + @utility tab-* { + tab-size: --value('revert'); } @tailwind utilities; ` - expect(await run(['example-revert'], input)).toMatchInlineSnapshot(` - " - .example-revert { - --resolved-value: revert; - } - " + expect(await compileCss(input, ['tab-revert'])).toMatchInlineSnapshot(` + ".tab-revert { + tab-size: revert; + }" `) - expect(await run(['example-initial'], input)).toEqual('') + expect(await compileCss(input, ['tab-initial'])).toEqual('') }) test('resolving bare values with constraints for integer, percentage, and ratio', async () => { @@ -30447,10 +27807,9 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-1', 'example-0.5', 'example-20%', 'example-2/3'], input)) + expect(await compileCss(input, ['example-1', 'example-0.5', 'example-20%', 'example-2/3'])) .toMatchInlineSnapshot(` - " - .example-0\\.5 { + ".example-0\\.5 { --value-as-number: .5; } @@ -30464,266 +27823,226 @@ describe('custom utilities', () => { .example-20\\% { --value-as-percentage: 20%; - } - " + }" `) expect( - await run( - ['example-1.23', 'example-12.34%', 'example-1.2/3', 'example-1/2.3', 'example-1.2/3.4'], - input, - ), + await compileCss(input, [ + 'example-1.23', + 'example-12.34%', + 'example-1.2/3', + 'example-1/2.3', + 'example-1.2/3.4', + ]), ).toEqual('') }) test('resolving unsupported bare values', async () => { - using spy = vi.spyOn(console, 'warn').mockImplementation(() => {}) let input = css` - @utility example-* { - --resolved-value: --value(color); + @utility tab-* { + tab-size: --value(color); } @tailwind utilities; ` - expect(await run(['example-#0088cc', 'example-foo'], input)).toEqual('') - expect( - `\n${spy.mock.calls - .map((c) => c.join(' ')) - .join('\n') - .trim()}\n`, - ).toMatchInlineSnapshot(` - " - Unsupported bare value data type: "color". - Only valid data types are: "number", "integer", "ratio", "percentage". - - \`\`\`css - --value(color) - ^^^^^ - \`\`\` - " - `) + expect(await compileCss(input, ['tab-#0088cc', 'tab-foo'])).toEqual('') }) test('resolving arbitrary values', async () => { let input = css` - @utility example-* { - --resolved-value: --value([integer]); + @utility tab-* { + tab-size: --value([integer]); } @tailwind utilities; ` expect( - await run( - [ - 'example-[1]', - 'example-[76]', - 'example-[971]', - 'example-[integer:var(--my-value)]', - 'example-(integer:my-value)', - ], - input, - ), + await compileCss(input, [ + 'tab-[1]', + 'tab-[76]', + 'tab-[971]', + 'tab-[integer:var(--my-value)]', + 'tab-(integer:my-value)', + ]), ).toMatchInlineSnapshot(` - " - .example-\\[1\\] { - --resolved-value: 1; + ".tab-\\[1\\] { + tab-size: 1; } - .example-\\[76\\] { - --resolved-value: 76; + .tab-\\[76\\] { + tab-size: 76; } - .example-\\[971\\] { - --resolved-value: 971; + .tab-\\[971\\] { + tab-size: 971; } - .example-\\[integer\\:var\\(--my-value\\)\\] { - --resolved-value: var(--my-value); - } - " + .tab-\\[integer\\:var\\(--my-value\\)\\] { + tab-size: var(--my-value); + }" `) expect( - await run( - [ - 'example-[#0088cc]', - 'example-[1px]', - 'example-[var(--my-value)]', - 'example-(--my-value)', - 'example-[color:var(--my-value)]', - 'example-(color:--my-value)', - ], - input, - ), + await compileCss(input, [ + 'tab-[#0088cc]', + 'tab-[1px]', + 'tab-[var(--my-value)]', + 'tab-(--my-value)', + 'tab-[color:var(--my-value)]', + 'tab-(color:--my-value)', + ]), ).toEqual('') }) test('resolving any arbitrary values', async () => { let input = css` - @utility example-* { - --resolved-value: --value([*]); + @utility tab-* { + tab-size: --value([*]); } @tailwind utilities; ` expect( - await run( - [ - 'example-[1]', - 'example-[76]', - 'example-[971]', - 'example-[var(--my-value)]', - 'example-(--my-value)', - ], - input, - ), + await compileCss(input, [ + 'tab-[1]', + 'tab-[76]', + 'tab-[971]', + 'tab-[var(--my-value)]', + 'tab-(--my-value)', + ]), ).toMatchInlineSnapshot(` - " - .example-\\(--my-value\\) { - --resolved-value: var(--my-value); + ".tab-\\(--my-value\\) { + tab-size: var(--my-value); } - .example-\\[1\\] { - --resolved-value: 1; + .tab-\\[1\\] { + tab-size: 1; } - .example-\\[76\\] { - --resolved-value: 76; + .tab-\\[76\\] { + tab-size: 76; } - .example-\\[971\\] { - --resolved-value: 971; + .tab-\\[971\\] { + tab-size: 971; } - .example-\\[var\\(--my-value\\)\\] { - --resolved-value: var(--my-value); - } - " + .tab-\\[var\\(--my-value\\)\\] { + tab-size: var(--my-value); + }" `) }) test('resolving any arbitrary values (without space)', async () => { let input = ` - @utility example-* { - --resolved-value: --value([*]); + @utility tab-* { + tab-size: --value([*]); } @tailwind utilities; ` expect( - await run( - [ - 'example-[1]', - 'example-[76]', - 'example-[971]', - 'example-[var(--my-value)]', - 'example-(--my-value)', - ], - input, - ), + await compileCss(input, [ + 'tab-[1]', + 'tab-[76]', + 'tab-[971]', + 'tab-[var(--my-value)]', + 'tab-(--my-value)', + ]), ).toMatchInlineSnapshot(` - " - .example-\\(--my-value\\) { - --resolved-value: var(--my-value); + ".tab-\\(--my-value\\) { + tab-size: var(--my-value); } - .example-\\[1\\] { - --resolved-value: 1; + .tab-\\[1\\] { + tab-size: 1; } - .example-\\[76\\] { - --resolved-value: 76; + .tab-\\[76\\] { + tab-size: 76; } - .example-\\[971\\] { - --resolved-value: 971; + .tab-\\[971\\] { + tab-size: 971; } - .example-\\[var\\(--my-value\\)\\] { - --resolved-value: var(--my-value); - } - " + .tab-\\[var\\(--my-value\\)\\] { + tab-size: var(--my-value); + }" `) }) test('resolving any arbitrary values (with escaped `*`)', async () => { let input = css` - @utility example-* { - --resolved-value: --value([\*]); + @utility tab-* { + tab-size: --value([\*]); } @tailwind utilities; ` expect( - await run( - [ - 'example-[1]', - 'example-[76]', - 'example-[971]', - 'example-[var(--my-value)]', - 'example-(--my-value)', - ], - input, - ), + await compileCss(input, [ + 'tab-[1]', + 'tab-[76]', + 'tab-[971]', + 'tab-[var(--my-value)]', + 'tab-(--my-value)', + ]), ).toMatchInlineSnapshot(` - " - .example-\\(--my-value\\) { - --resolved-value: var(--my-value); + ".tab-\\(--my-value\\) { + tab-size: var(--my-value); } - .example-\\[1\\] { - --resolved-value: 1; + .tab-\\[1\\] { + tab-size: 1; } - .example-\\[76\\] { - --resolved-value: 76; + .tab-\\[76\\] { + tab-size: 76; } - .example-\\[971\\] { - --resolved-value: 971; + .tab-\\[971\\] { + tab-size: 971; } - .example-\\[var\\(--my-value\\)\\] { - --resolved-value: var(--my-value); - } - " + .tab-\\[var\\(--my-value\\)\\] { + tab-size: var(--my-value); + }" `) }) test('resolving theme, bare and arbitrary values all at once', async () => { let input = css` @theme reference { - --example-a: 8; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value([integer]); - --resolved-value: --value(integer); - --resolved-value: --value(--example); + @utility tab-* { + tab-size: --value([integer]); + tab-size: --value(integer); + tab-size: --value(--tab-size); } @tailwind utilities; ` - expect(await run(['example-a', 'example-76', 'example-[123]'], input)).toMatchInlineSnapshot(` - " - .example-76 { - --resolved-value: 76; + expect(await compileCss(input, ['tab-github', 'tab-76', 'tab-[123]'])).toMatchInlineSnapshot(` + ".tab-76 { + tab-size: 76; } - .example-\\[123\\] { - --resolved-value: 123; + .tab-\\[123\\] { + tab-size: 123; } - .example-a { - --resolved-value: var(--example-a, 8); - } - " + .tab-github { + tab-size: var(--tab-size-github, 8); + }" `) - expect(await run(['example-[#0088cc]', 'example-[1px]'], input)).toEqual('') + expect(await compileCss(input, ['tab-[#0088cc]', 'tab-[1px]'])).toEqual('') }) test('in combination with calc to produce different data types of values', async () => { @@ -30733,64 +28052,87 @@ describe('custom utilities', () => { } @utility example-* { - --resolved-value: --value([percentage]); - --resolved-value: calc(--value(integer) * 1%); - --resolved-value: --value(--example); + --value: --value([percentage]); + --value: calc(--value(integer) * 1%); + --value: --value(--example); } @tailwind utilities; ` - expect(await run(['example-full', 'example-12', 'example-[20%]'], input)) + expect(await compileCss(input, ['example-full', 'example-12', 'example-[20%]'])) .toMatchInlineSnapshot(` - " - .example-12 { - --resolved-value: calc(12 * 1%); + ".example-12 { + --value: calc(12 * 1%); } .example-\\[20\\%\\] { - --resolved-value: 20%; + --value: 20%; } .example-full { - --resolved-value: var(--example-full, 100%); - } - " + --value: var(--example-full, 100%); + }" `) - expect(await run(['example-half', 'example-[#0088cc]'], input)).toEqual('') + expect(await compileCss(input, ['example-half', 'example-[#0088cc]'])).toEqual('') }) test('shorthand if resulting values are of the same type', async () => { let input = css` @theme reference { + --tab-size-github: 8; --example-full: 100%; } + @utility tab-* { + tab-size: --value(--tab-size, integer, [integer]); + } + @utility example-* { - --resolved-value: calc(--value(integer) * 1%); - --resolved-value: --value(--example, [percentage]); + --value: calc(--value(integer) * 1%); + --value: --value(--example, [percentage]); } @tailwind utilities; ` - expect(await run(['example-37', 'example-[50%]', 'example-full'], input)) - .toMatchInlineSnapshot(` - " - .example-37 { - --resolved-value: calc(37 * 1%); - } + expect( + await compileCss(input, [ + 'tab-github', + 'tab-76', + 'tab-[123]', + 'example-37', + 'example-[50%]', + 'example-full', + ]), + ).toMatchInlineSnapshot(` + ".example-37 { + --value: calc(37 * 1%); + } - .example-\\[50\\%\\] { - --resolved-value: 50%; - } + .example-\\[50\\%\\] { + --value: 50%; + } - .example-full { - --resolved-value: var(--example-full, 100%); - } - " - `) - expect(await run(['example-foo', 'example-[13px]'], input)).toEqual('') + .example-full { + --value: var(--example-full, 100%); + } + + .tab-76 { + tab-size: 76; + } + + .tab-\\[123\\] { + tab-size: 123; + } + + .tab-github { + tab-size: var(--tab-size-github, 8); + }" + `) + expect( + await compileCss(input, ['tab-[#0088cc]', 'tab-[1px]', 'example-foo', 'example-[13px]']), + ).toEqual('') }) test('negative values', async () => { @@ -30800,75 +28142,66 @@ describe('custom utilities', () => { } @utility example-* { - --resolved-value: --value(--example, [percentage], [length]); + --value: --value(--example, [percentage], [length]); } @utility -example-* { - --resolved-value: calc(--value(--example, [percentage], [length]) * -1); + --value: calc(--value(--example, [percentage], [length]) * -1); } @tailwind utilities; ` expect( - await run( - [ - 'example-full', - '-example-full', - 'example-[10px]', - '-example-[10px]', - 'example-[20%]', - '-example-[20%]', - ], - input, - ), + await compileCss(input, [ + 'example-full', + '-example-full', + 'example-[10px]', + '-example-[10px]', + 'example-[20%]', + '-example-[20%]', + ]), ).toMatchInlineSnapshot(` - " - .-example-\\[10px\\] { - --resolved-value: calc(10px * -1); + ".-example-\\[10px\\] { + --value: calc(10px * -1); } .-example-\\[20\\%\\] { - --resolved-value: calc(20% * -1); + --value: calc(20% * -1); } .-example-full { - --resolved-value: calc(var(--example-full, 100%) * -1); + --value: calc(var(--example-full, 100%) * -1); } .example-\\[10px\\] { - --resolved-value: 10px; + --value: 10px; } .example-\\[20\\%\\] { - --resolved-value: 20%; + --value: 20%; } .example-full { - --resolved-value: var(--example-full, 100%); - } - " + --value: var(--example-full, 100%); + }" `) - expect(await run(['example-10'], input)).toEqual('') + expect(await compileCss(input, ['example-10'])).toEqual('') }) test('using the same value multiple times', async () => { let input = css` @utility example-* { - --resolved-value: calc(var(--spacing) * --value(number)) - calc(var(--spacing) * --value(number)); + --value: calc(var(--spacing) * --value(number)) calc(var(--spacing) * --value(number)); } @tailwind utilities; ` - expect(await run(['example-12'], input)).toMatchInlineSnapshot(` - " - .example-12 { - --resolved-value: calc(var(--spacing) * 12) - calc(var(--spacing) * 12); - } - " + expect(await compileCss(input, ['example-12'])).toMatchInlineSnapshot(` + ".example-12 { + --value: calc(var(--spacing) * 12) calc(var(--spacing) * 12); + }" `) }) @@ -30885,16 +28218,14 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-12'], input)).toMatchInlineSnapshot(` - " - :root, :host { + expect(await compileCss(input, ['example-12'])).toMatchInlineSnapshot(` + ":root, :host { --spacing: 4px; } .example-12 { margin: calc(var(--spacing) * 12); - } - " + }" `) }) @@ -30911,179 +28242,10 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-12'], input)).toMatchInlineSnapshot(` - " - .example-12 { + expect(await compileCss(input, ['example-12'])).toMatchInlineSnapshot(` + ".example-12 { margin: 48px; - } - " - `) - }) - - test('functional utilities can use `--default(…)` in `--value(…)`', async () => { - let input = css` - @utility example-* { - --resolved-value: --value(integer, --default(4)); - } - - @tailwind utilities; - ` - - expect(await run(['example', 'example-123'], input)).toMatchInlineSnapshot(` - " - .example { - --resolved-value: 4; - } - - .example-123 { - --resolved-value: 123; - } - " - `) - - expect(await run(['example-foo'], input)).toEqual('') - }) - - test('functional utilities can use `--default(…)` in complex expressions', async () => { - let input = css` - @utility example-* { - --resolved-value: calc(--value(integer, --default(4)) * 2); - } - - @tailwind utilities; - ` - - expect(await run(['example', 'example-123'], input)).toMatchInlineSnapshot(` - " - .example { - --resolved-value: calc(4 * 2); - } - - .example-123 { - --resolved-value: calc(123 * 2); - } - " - `) - - expect(await run(['example-foo'], input)).toEqual('') - }) - - test('functional utilities can use `--default(…)` with `--modifier(…)`', async () => { - let input = css` - @utility example-* { - --resolved-value: --value(integer, --default(4)); - --resolved-modifier: --modifier(integer); - } - - @tailwind utilities; - ` - - expect(await run(['example', 'example/25'], input)).toMatchInlineSnapshot(` - " - .example\\/25 { - --resolved-value: 4; - --resolved-modifier: 25; - } - - .example { - --resolved-value: 4; - } - " - `) - - expect(await run(['example/foo'], input)).toEqual('') - }) - - test('functional utilities can use `--default(…)` in `--modifier(…)`', async () => { - let input = css` - @utility example-* { - --resolved-value: --value(integer); - --resolved-modifier: --modifier(integer, --default(1)); - } - - @tailwind utilities; - ` - - expect(await run(['example-123', 'example-123/25'], input)).toMatchInlineSnapshot(` - " - .example-123 { - --resolved-value: 123; - --resolved-modifier: 1; - } - - .example-123\\/25 { - --resolved-value: 123; - --resolved-modifier: 25; - } - " - `) - - expect(await run(['example-123/foo'], input)).toEqual('') - }) - - test('functional utilities can use `--default(…)` in `--value(…)` and `--modifier(…)`', async () => { - let input = css` - @utility example-* { - --resolved-value: --value(integer, --default(12)); - --resolved-modifier: --modifier(integer, --default(34)); - } - - @tailwind utilities; - ` - - expect(await run(['example', 'example/1', 'example-1', 'example-1/1'], input)) - .toMatchInlineSnapshot(` - " - .example { - --resolved-value: 12; - --resolved-modifier: 34; - } - - .example-1 { - --resolved-value: 1; - --resolved-modifier: 34; - } - - .example-1\\/1 { - --resolved-value: 1; - --resolved-modifier: 1; - } - - .example\\/1 { - --resolved-value: 12; - --resolved-modifier: 1; - } - " - `) - - expect(await run(['example-123/foo'], input)).toEqual('') - }) - - test('the value of `--default(…)` is emitted as-is', async () => { - let input = css` - @theme reference { - --text-box-trim-trim-both: trim-both; - --text-box-edge-cap: cap alphabetic; - } - - @utility trim-* { - text-box: --value(--text-box-trim-*) - --modifier(--text-box-edge-*, --default(box alphabetic)); - } - - @tailwind utilities; - ` - - expect(await run(['trim-trim-both', 'trim-trim-both/cap'], input)).toMatchInlineSnapshot(` - " - .trim-trim-both { - text-box: var(--text-box-trim-trim-both, trim-both) box alphabetic; - } - - .trim-trim-both\\/cap { - text-box: var(--text-box-trim-trim-both, trim-both) var(--text-box-edge-cap, cap alphabetic); - } - " + }" `) }) @@ -31095,65 +28257,62 @@ describe('custom utilities', () => { } @utility example-* { - --resolved-value: --value(--value, [length]); - --resolved-modifier: --modifier(--modifier, [length]); - --resolved-modifier-with-calc: calc(--modifier(--modifier, [length]) * 2); - --resolved-modifier-literals: --modifier('literal', 'literal-2'); + --value: --value(--value, [length]); + --modifier: --modifier(--modifier, [length]); + --modifier-with-calc: calc(--modifier(--modifier, [length]) * 2); + --modifier-literals: --modifier('literal', 'literal-2'); } @tailwind utilities; ` expect( - await run( - [ - 'example-sm', - 'example-sm/7', - 'example-[12px]', - 'example-[12px]/[16px]', - 'example-sm/literal', - 'example-sm/literal-2', - ], - input, - ), + await compileCss(input, [ + 'example-sm', + 'example-sm/7', + 'example-[12px]', + 'example-[12px]/[16px]', + 'example-sm/literal', + 'example-sm/literal-2', + ]), ).toMatchInlineSnapshot(` - " - .example-\\[12px\\]\\/\\[16px\\] { - --resolved-value: 12px; - --resolved-modifier: 16px; - --resolved-modifier-with-calc: calc(16px * 2); + ".example-\\[12px\\]\\/\\[16px\\] { + --value: 12px; + --modifier: 16px; + --modifier-with-calc: calc(16px * 2); } .example-sm\\/7 { - --resolved-value: var(--value-sm, 14px); - --resolved-modifier: var(--modifier-7, 28px); - --resolved-modifier-with-calc: calc(var(--modifier-7, 28px) * 2); + --value: var(--value-sm, 14px); + --modifier: var(--modifier-7, 28px); + --modifier-with-calc: calc(var(--modifier-7, 28px) * 2); } .example-sm\\/literal { - --resolved-value: var(--value-sm, 14px); - --resolved-modifier-literals: literal; + --value: var(--value-sm, 14px); + --modifier-literals: literal; } .example-sm\\/literal-2 { - --resolved-value: var(--value-sm, 14px); - --resolved-modifier-literals: literal-2; + --value: var(--value-sm, 14px); + --modifier-literals: literal-2; } .example-\\[12px\\] { - --resolved-value: 12px; + --value: 12px; } .example-sm { - --resolved-value: var(--value-sm, 14px); - } - " + --value: var(--value-sm, 14px); + }" `) expect( - await run( - ['example-foo', 'example-foo/[12px]', 'example-foo/12', 'example-sm/unknown-literal'], - input, - ), + await compileCss(input, [ + 'example-foo', + 'example-foo/[12px]', + 'example-foo/12', + 'example-sm/unknown-literal', + ]), ).toEqual('') }) @@ -31164,29 +28323,27 @@ describe('custom utilities', () => { } @utility example-* { - --resolved-value: --value(--example, ratio, [ratio]); + --value: --value(--example, ratio, [ratio]); } @tailwind utilities; ` - expect(await run(['example-video', 'example-1/1', 'example-[7/9]'], input)) + expect(await compileCss(input, ['example-video', 'example-1/1', 'example-[7/9]'])) .toMatchInlineSnapshot(` - " - .example-1\\/1 { - --resolved-value: 1 / 1; + ".example-1\\/1 { + --value: 1 / 1; } .example-\\[7\\/9\\] { - --resolved-value: 7/9; + --value: 7 / 9; } .example-video { - --resolved-value: var(--example-video, 16 / 9); - } - " + --value: var(--example-video, 16 / 9); + }" `) - expect(await run(['example-foo'], input)).toEqual('') + expect(await compileCss(input, ['example-foo'])).toEqual('') }) test('resolve theme values with sub-namespace (--text- * --line-height)', async () => { @@ -31205,9 +28362,8 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-xs', 'example-xs/6'], input)).toMatchInlineSnapshot(` - " - .example-xs\\/6 { + expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(` + ".example-xs\\/6 { font-size: var(--text-xs, .75rem); line-height: var(--text-xs--line-height, calc(1 / .75)); line-height: 6; @@ -31216,10 +28372,9 @@ describe('custom utilities', () => { .example-xs { font-size: var(--text-xs, .75rem); line-height: var(--text-xs--line-height, calc(1 / .75)); - } - " + }" `) - expect(await run(['example-foo', 'example-xs/foo'], input)).toEqual('') + expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('') }) test('resolve theme values with sub-namespace (--text-\\* --line-height)', async () => { @@ -31238,9 +28393,8 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-xs', 'example-xs/6'], input)).toMatchInlineSnapshot(` - " - .example-xs\\/6 { + expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(` + ".example-xs\\/6 { font-size: var(--text-xs, .75rem); line-height: var(--text-xs--line-height, calc(1 / .75)); line-height: 6; @@ -31249,10 +28403,9 @@ describe('custom utilities', () => { .example-xs { font-size: var(--text-xs, .75rem); line-height: var(--text-xs--line-height, calc(1 / .75)); - } - " + }" `) - expect(await run(['example-foo', 'example-xs/foo'], input)).toEqual('') + expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('') }) test('resolve theme values with sub-namespace (--value(--text --line-height))', async () => { @@ -31271,9 +28424,8 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-xs', 'example-xs/6'], input)).toMatchInlineSnapshot(` - " - .example-xs\\/6 { + expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(` + ".example-xs\\/6 { font-size: var(--text-xs, .75rem); line-height: var(--text-xs--line-height, calc(1 / .75)); line-height: 6; @@ -31282,10 +28434,9 @@ describe('custom utilities', () => { .example-xs { font-size: var(--text-xs, .75rem); line-height: var(--text-xs--line-height, calc(1 / .75)); - } - " + }" `) - expect(await run(['example-foo', 'example-xs/foo'], input)).toEqual('') + expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('') }) test('resolve theme values with sub-namespace (--value(--text-*--line-height))', async () => { @@ -31304,9 +28455,8 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-xs', 'example-xs/6'], input)).toMatchInlineSnapshot(` - " - .example-xs\\/6 { + expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(` + ".example-xs\\/6 { font-size: var(--text-xs, .75rem); line-height: var(--text-xs--line-height, calc(1 / .75)); line-height: 6; @@ -31315,10 +28465,9 @@ describe('custom utilities', () => { .example-xs { font-size: var(--text-xs, .75rem); line-height: var(--text-xs--line-height, calc(1 / .75)); - } - " + }" `) - expect(await run(['example-foo', 'example-xs/foo'], input)).toEqual('') + expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('') }) test('variables used in `@utility` will not be emitted if the utility is not used', async () => { @@ -31336,12 +28485,10 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['flex'], input)).toMatchInlineSnapshot(` - " - .flex { + expect(await compileCss(input, ['flex'])).toMatchInlineSnapshot(` + ".flex { display: flex; - } - " + }" `) }) @@ -31360,9 +28507,8 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-foo'], input)).toMatchInlineSnapshot(` - " - :root, :host { + expect(await compileCss(input, ['example-foo'])).toMatchInlineSnapshot(` + ":root, :host { --example-foo: red; --color-red-500: red; } @@ -31370,8 +28516,7 @@ describe('custom utilities', () => { .example-foo { color: var(--color-red-500); background-color: var(--example-foo); - } - " + }" `) }) @@ -31397,15 +28542,13 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['mask-r-20%'], input)).toMatchInlineSnapshot(` - " - .mask-r-20\\% { + expect(await compileCss(input, ['mask-r-20%'])).toMatchInlineSnapshot(` + ".mask-r-20\\% { --mask-right: linear-gradient(to left, transparent, black 20%); -webkit-mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic); -webkit-mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic); mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic); - } - " + }" `) }) }) @@ -31413,92 +28556,84 @@ describe('custom utilities', () => { test('resolve value based on `@theme`', async () => { let input = css` @theme { - --example-a: 8; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value(--example); + @utility tab-* { + tab-size: --value(--tab-size); } @tailwind utilities; ` - expect(await run(['example-a'], input)).toMatchInlineSnapshot(` - " - :root, :host { - --example-a: 8; + expect(await compileCss(input, ['tab-github'])).toMatchInlineSnapshot(` + ":root, :host { + --tab-size-github: 8; } - .example-a { - --resolved-value: var(--example-a); - } - " + .tab-github { + tab-size: var(--tab-size-github); + }" `) }) test('resolve value based on `@theme reference`', async () => { let input = css` @theme reference { - --example-a: 8; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value(--example); + @utility tab-* { + tab-size: --value(--tab-size); } @tailwind utilities; ` - expect(await run(['example-a'], input)).toMatchInlineSnapshot(` - " - .example-a { - --resolved-value: var(--example-a, 8); - } - " + expect(await compileCss(input, ['tab-github'])).toMatchInlineSnapshot(` + ".tab-github { + tab-size: var(--tab-size-github, 8); + }" `) }) test('resolve value based on `@theme inline`', async () => { let input = css` @theme inline { - --example-a: 8; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value(--example); + @utility tab-* { + tab-size: --value(--tab-size); } @tailwind utilities; ` - expect(await run(['example-a'], input)).toMatchInlineSnapshot(` - " - .example-a { - --resolved-value: 8; - } - " + expect(await compileCss(input, ['tab-github'])).toMatchInlineSnapshot(` + ".tab-github { + tab-size: 8; + }" `) }) test('resolve value based on `@theme inline reference`', async () => { let input = css` @theme inline reference { - --example-a: 8; + --tab-size-github: 8; } - @utility example-* { - --resolved-value: --value(--example); + @utility tab-* { + tab-size: --value(--tab-size); } @tailwind utilities; ` - expect(await run(['example-a'], input)).toMatchInlineSnapshot(` - " - .example-a { - --resolved-value: 8; - } - " + expect(await compileCss(input, ['tab-github'])).toMatchInlineSnapshot(` + ".tab-github { + tab-size: 8; + }" `) }) @@ -31520,13 +28655,11 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-xs'], input)).toMatchInlineSnapshot(` - " - .example-xs { + expect(await compileCss(input, ['example-xs'])).toMatchInlineSnapshot(` + ".example-xs { font-size: var(--text-xs, .75rem); line-height: 1.33333; - } - " + }" `) }) @@ -31548,49 +28681,11 @@ describe('custom utilities', () => { @tailwind utilities; ` - expect(await run(['example-xs'], input)).toMatchInlineSnapshot(` - " - .example-xs { + expect(await compileCss(input, ['example-xs'])).toMatchInlineSnapshot(` + ".example-xs { font-size: .75rem; line-height: var(--text-xs--line-height, calc(1 / .75)); - } - " - `) - }) - - test('multiple @utility definitions with the same name but different value types', async () => { - let input = css` - @theme { - --color-red-500: #ef4444; - --spacing: 0.25rem; - } - - @utility foo-* { - color: --value(--color-*); - } - - @utility foo-* { - font-size: --spacing(--value(number)); - } - - @tailwind utilities; - ` - - expect(await run(['foo-red-500', 'foo-123'], input)).toMatchInlineSnapshot(` - " - :root, :host { - --color-red-500: #ef4444; - --spacing: .25rem; - } - - .foo-123 { - font-size: calc(var(--spacing) * 123); - } - - .foo-red-500 { - color: var(--color-red-500); - } - " + }" `) }) }) diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 477559d7e..521cf974d 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -12,25 +12,25 @@ import { } from './ast' import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate' import type { DesignSystem } from './design-system' +import { enableContainerSizeUtility } from './feature-flags' import type { Theme, ThemeKey } from './theme' import { compareBreakpoints } from './utils/compare-breakpoints' import { DefaultMap } from './utils/default-map' -import { dimensions } from './utils/dimensions' -import { unescape } from './utils/escape' import { inferDataType, - isLength, isPositiveInteger, isStrictPositiveInteger, isValidOpacityValue, isValidSpacingMultiplier, } from './utils/infer-data-type' -import { addWhitespaceAroundMathOperators } from './utils/math-operators' import { replaceShadowColors } from './utils/replace-shadow-colors' import { segment } from './utils/segment' import * as ValueParser from './value-parser' import { walk, WalkAction } from './walk' +const IS_VALID_STATIC_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*$/ +const IS_VALID_FUNCTIONAL_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*-\*$/ + const DEFAULT_SPACING_SUGGESTIONS = [ '0', '0.5', @@ -193,7 +193,7 @@ export function withAlpha(value: string, alpha: string): string { } /** - * Replace the opacity in a color using relative color syntax. + * Apply opacity to a color using `color-mix`. */ export function replaceAlpha(value: string, alpha: string): string { // Convert numeric values (like `0.5`) to percentages (like `50%`) so they @@ -393,8 +393,6 @@ export function createUtilities(theme: Theme) { * user's theme. */ function functionalUtility(classRoot: string, desc: UtilityDescription) { - if (desc.staticValues) desc.staticValues = Object.assign(Object.create(null), desc.staticValues) - function handleFunctionalUtility({ negative }: { negative: boolean }) { return (candidate: Extract) => { let value: string | null = null @@ -421,19 +419,12 @@ export function createUtilities(theme: Theme) { desc.themeKeys ?? [], ) - // If the theme value resolved without consuming the modifier, then - // the modifier would be silently ignored and `w-4` and `w-4/foo` - // would generate the same CSS. - // - // The modifier would be consumed if it's a fraction, e.g.: `w-1/2` - if (value !== null && candidate.modifier && !candidate.value.fraction) return - // Automatically handle things like `w-1/2` without requiring `1/2` to // exist as a theme value. if (value === null && desc.supportsFractions && candidate.value.fraction) { let [lhs, rhs] = segment(candidate.value.fraction, '/') if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) return - value = `calc(${lhs} / ${rhs} * 100%)` + value = `calc(${candidate.value.fraction} * 100%)` } // If there is still no value but the utility supports bare values, @@ -459,10 +450,7 @@ export function createUtilities(theme: Theme) { if (value === null) return // Negate the value if the candidate has a negative prefix. - return desc.handle( - negative ? addWhitespaceAroundMathOperators(`calc(${value} * -1)`) : value, - dataType, - ) + return desc.handle(negative ? `calc(${value} * -1)` : value, dataType) } } @@ -526,7 +514,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: desc.themeKeys, - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, ]) @@ -560,14 +547,14 @@ export function createUtilities(theme: Theme) { if (!multiplier) return null if (!isValidSpacingMultiplier(value)) return null - return `--spacing(${value})` + return `calc(${multiplier} * ${value})` }, handleNegativeBareValue: ({ value }) => { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null if (!isValidSpacingMultiplier(value)) return null - return `--spacing(-${value})` + return `calc(${multiplier} * -${value})` }, handle, staticValues, @@ -640,10 +627,8 @@ export function createUtilities(theme: Theme) { ['inset', 'inset'], ['inset-x', 'inset-inline'], ['inset-y', 'inset-block'], - ['inset-s', 'inset-inline-start'], - ['inset-e', 'inset-inline-end'], - ['inset-bs', 'inset-block-start'], - ['inset-be', 'inset-block-end'], + ['start', 'inset-inline-start'], + ['end', 'inset-inline-end'], ['top', 'top'], ['right', 'right'], ['bottom', 'bottom'], @@ -906,8 +891,6 @@ export function createUtilities(theme: Theme) { ['my', 'margin-block'], ['ms', 'margin-inline-start'], ['me', 'margin-inline-end'], - ['mbs', 'margin-block-start'], - ['mbe', 'margin-block-end'], ['mt', 'margin-top'], ['mr', 'margin-right'], ['mb', 'margin-bottom'], @@ -997,7 +980,7 @@ export function createUtilities(theme: Theme) { handleBareValue: ({ fraction }) => { if (fraction === null) return null let [lhs, rhs] = segment(fraction, '/') - if (!isValidSpacingMultiplier(lhs) || !isValidSpacingMultiplier(rhs)) return null + if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) return null return fraction }, handle: (value) => [decl('aspect-ratio', value)], @@ -1087,82 +1070,6 @@ export function createUtilities(theme: Theme) { }) } - /** - * @css `inline-size` - * @css `min-inline-size` - * @css `max-inline-size` - * @css `block-size` - * @css `min-block-size` - * @css `max-block-size` - */ - for (let [key, value] of [ - ['full', '100%'], - ['min', 'min-content'], - ['max', 'max-content'], - ['fit', 'fit-content'], - ]) { - staticUtility(`inline-${key}`, [['inline-size', value]]) - staticUtility(`block-${key}`, [['block-size', value]]) - staticUtility(`min-inline-${key}`, [['min-inline-size', value]]) - staticUtility(`min-block-${key}`, [['min-block-size', value]]) - staticUtility(`max-inline-${key}`, [['max-inline-size', value]]) - staticUtility(`max-block-${key}`, [['max-block-size', value]]) - } - - // inline-size viewport units (like width) - for (let [key, value] of [ - ['svw', '100svw'], - ['lvw', '100lvw'], - ['dvw', '100dvw'], - ]) { - staticUtility(`inline-${key}`, [['inline-size', value]]) - staticUtility(`min-inline-${key}`, [['min-inline-size', value]]) - staticUtility(`max-inline-${key}`, [['max-inline-size', value]]) - } - - // block-size viewport units (like height) - for (let [key, value] of [ - ['svh', '100svh'], - ['lvh', '100lvh'], - ['dvh', '100dvh'], - ]) { - staticUtility(`block-${key}`, [['block-size', value]]) - staticUtility(`min-block-${key}`, [['min-block-size', value]]) - staticUtility(`max-block-${key}`, [['max-block-size', value]]) - } - - staticUtility(`inline-auto`, [['inline-size', 'auto']]) - staticUtility(`block-auto`, [['block-size', 'auto']]) - staticUtility(`min-inline-auto`, [['min-inline-size', 'auto']]) - staticUtility(`min-block-auto`, [['min-block-size', 'auto']]) - - staticUtility(`block-lh`, [['block-size', '1lh']]) - staticUtility(`min-block-lh`, [['min-block-size', '1lh']]) - staticUtility(`max-block-lh`, [['max-block-size', '1lh']]) - - staticUtility(`inline-screen`, [['inline-size', '100vw']]) - staticUtility(`min-inline-screen`, [['min-inline-size', '100vw']]) - staticUtility(`max-inline-screen`, [['max-inline-size', '100vw']]) - staticUtility(`block-screen`, [['block-size', '100vh']]) - staticUtility(`min-block-screen`, [['min-block-size', '100vh']]) - staticUtility(`max-block-screen`, [['max-block-size', '100vh']]) - - staticUtility(`max-inline-none`, [['max-inline-size', 'none']]) - staticUtility(`max-block-none`, [['max-block-size', 'none']]) - - for (let [name, namespaces, property] of [ - ['inline', ['--spacing', '--container'], 'inline-size'], - ['min-inline', ['--spacing', '--container'], 'min-inline-size'], - ['max-inline', ['--spacing', '--container'], 'max-inline-size'], - ['block', ['--spacing'], 'block-size'], - ['min-block', ['--spacing'], 'min-block-size'], - ['max-block', ['--spacing'], 'max-block-size'], - ] as [string, ThemeKey[], string][]) { - spacingUtility(name, namespaces, (value) => [decl(property, value)], { - supportsFractions: true, - }) - } - utilities.static('container', () => { let breakpoints = [...theme.namespace('--breakpoint').values()] breakpoints.sort((a, z) => compareBreakpoints(a, z, 'asc')) @@ -1568,11 +1475,11 @@ export function createUtilities(theme: Theme) { { let transformValue = [ - 'var(--tw-rotate-x, )', - 'var(--tw-rotate-y, )', - 'var(--tw-rotate-z, )', - 'var(--tw-skew-x, )', - 'var(--tw-skew-y, )', + 'var(--tw-rotate-x,)', + 'var(--tw-rotate-y,)', + 'var(--tw-rotate-z,)', + 'var(--tw-skew-x,)', + 'var(--tw-skew-y,)', ].join(' ') let transformProperties = () => @@ -1715,21 +1622,6 @@ export function createUtilities(theme: Theme) { staticUtility('transform-none', [['transform', 'none']]) } - /** - * @css `zoom` - */ - functionalUtility('zoom', { - handleBareValue: ({ value }) => { - if (!isPositiveInteger(value)) return null - return `${value}%` - }, - handle: (value) => [decl('zoom', value)], - }) - - suggest('zoom', () => [ - { values: ['50', '75', '90', '95', '100', '105', '110', '125', '150', '200'] }, - ]) - /** * @css `transform-style` */ @@ -1814,7 +1706,7 @@ export function createUtilities(theme: Theme) { staticUtility(`touch-pan-${value}`, [ touchProperties, ['--tw-pan-x', `pan-${value}`], - ['touch-action', 'var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, )'], + ['touch-action', 'var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)'], ]) } @@ -1822,14 +1714,14 @@ export function createUtilities(theme: Theme) { staticUtility(`touch-pan-${value}`, [ touchProperties, ['--tw-pan-y', `pan-${value}`], - ['touch-action', 'var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, )'], + ['touch-action', 'var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)'], ]) } staticUtility('touch-pinch-zoom', [ touchProperties, ['--tw-pinch-zoom', `pinch-zoom`], - ['touch-action', 'var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, )'], + ['touch-action', 'var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)'], ]) /** @@ -1885,8 +1777,6 @@ export function createUtilities(theme: Theme) { ['scroll-my', 'scroll-margin-block'], ['scroll-ms', 'scroll-margin-inline-start'], ['scroll-me', 'scroll-margin-inline-end'], - ['scroll-mbs', 'scroll-margin-block-start'], - ['scroll-mbe', 'scroll-margin-block-end'], ['scroll-mt', 'scroll-margin-top'], ['scroll-mr', 'scroll-margin-right'], ['scroll-mb', 'scroll-margin-bottom'], @@ -1911,8 +1801,6 @@ export function createUtilities(theme: Theme) { ['scroll-py', 'scroll-padding-block'], ['scroll-ps', 'scroll-padding-inline-start'], ['scroll-pe', 'scroll-padding-inline-end'], - ['scroll-pbs', 'scroll-padding-block-start'], - ['scroll-pbe', 'scroll-padding-block-end'], ['scroll-pt', 'scroll-padding-top'], ['scroll-pr', 'scroll-padding-right'], ['scroll-pb', 'scroll-padding-bottom'], @@ -1993,11 +1881,6 @@ export function createUtilities(theme: Theme) { functionalUtility('auto-cols', { themeKeys: ['--grid-auto-columns'], - handleBareValue: ({ value }) => { - if (!theme.resolve(null, ['--spacing'])) return null - if (!isValidSpacingMultiplier(value)) return null - return `--spacing(${value})` - }, handle: (value) => [decl('grid-auto-columns', value)], staticValues: { auto: [decl('grid-auto-columns', 'auto')], @@ -2009,11 +1892,6 @@ export function createUtilities(theme: Theme) { functionalUtility('auto-rows', { themeKeys: ['--grid-auto-rows'], - handleBareValue: ({ value }) => { - if (!theme.resolve(null, ['--spacing'])) return null - if (!isValidSpacingMultiplier(value)) return null - return `--spacing(${value})` - }, handle: (value) => [decl('grid-auto-rows', value)], staticValues: { auto: [decl('grid-auto-rows', 'auto')], @@ -2139,65 +2017,31 @@ export function createUtilities(theme: Theme) { spacingUtility( 'space-x', ['--space', '--spacing'], - (value) => { - let zero = (() => { - if (value === '--spacing(0)') return true - if (value === '--spacing(-0)') return true + (value) => [ + atRoot([property('--tw-space-x-reverse', '0')]), - let parsed = dimensions.get(value) - if (parsed && parsed[0] === 0 && (parsed[1] === null || isLength(value))) { - return true - } - - return false - })() - - return [ - atRoot([property('--tw-space-x-reverse', '0')]), - - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'row-gap'), - decl('--tw-space-x-reverse', '0'), - decl('margin-inline-start', zero ? '0' : `calc(${value} * var(--tw-space-x-reverse))`), - decl( - 'margin-inline-end', - zero ? '0' : `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`, - ), - ]), - ] - }, + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'row-gap'), + decl('--tw-space-x-reverse', '0'), + decl('margin-inline-start', `calc(${value} * var(--tw-space-x-reverse))`), + decl('margin-inline-end', `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`), + ]), + ], { supportsNegative: true }, ) spacingUtility( 'space-y', ['--space', '--spacing'], - (value) => { - let zero = (() => { - if (value === '--spacing(0)') return true - if (value === '--spacing(-0)') return true - - let parsed = dimensions.get(value) - if (parsed && parsed[0] === 0 && (parsed[1] === null || isLength(value))) { - return true - } - - return false - })() - - return [ - atRoot([property('--tw-space-y-reverse', '0')]), - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'column-gap'), - decl('--tw-space-y-reverse', '0'), - decl('margin-block-start', zero ? '0' : `calc(${value} * var(--tw-space-y-reverse))`), - decl( - 'margin-block-end', - zero ? '0' : `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`, - ), - ]), - ] - }, + (value) => [ + atRoot([property('--tw-space-y-reverse', '0')]), + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'column-gap'), + decl('--tw-space-y-reverse', '0'), + decl('margin-block-start', `calc(${value} * var(--tw-space-y-reverse))`), + decl('margin-block-end', `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`), + ]), + ], { supportsNegative: true }, ) @@ -2281,41 +2125,6 @@ export function createUtilities(theme: Theme) { staticUtility('scroll-auto', [['scroll-behavior', 'auto']]) staticUtility('scroll-smooth', [['scroll-behavior', 'smooth']]) - staticUtility('scrollbar-auto', [['scrollbar-width', 'auto']]) - staticUtility('scrollbar-thin', [['scrollbar-width', 'thin']]) - staticUtility('scrollbar-none', [['scrollbar-width', 'none']]) - - { - let scrollbarColorProperties = () => { - return atRoot([ - property('--tw-scrollbar-thumb', '#0000', ''), - property('--tw-scrollbar-track', '#0000', ''), - ]) - } - - colorUtility('scrollbar-thumb', { - themeKeys: ['--color'], - handle: (value) => [ - scrollbarColorProperties(), - decl('--tw-scrollbar-thumb', value), - decl('scrollbar-color', 'var(--tw-scrollbar-thumb) var(--tw-scrollbar-track)'), - ], - }) - - colorUtility('scrollbar-track', { - themeKeys: ['--color'], - handle: (value) => [ - scrollbarColorProperties(), - decl('--tw-scrollbar-track', value), - decl('scrollbar-color', 'var(--tw-scrollbar-thumb) var(--tw-scrollbar-track)'), - ], - }) - } - - staticUtility('scrollbar-gutter-auto', [['scrollbar-gutter', 'auto']]) - staticUtility('scrollbar-gutter-stable', [['scrollbar-gutter', 'stable']]) - staticUtility('scrollbar-gutter-both', [['scrollbar-gutter', 'stable both-edges']]) - staticUtility('truncate', [ ['overflow', 'hidden'], ['text-overflow', 'ellipsis'], @@ -2345,16 +2154,6 @@ export function createUtilities(theme: Theme) { staticUtility('whitespace-pre-wrap', [['white-space', 'pre-wrap']]) staticUtility('whitespace-break-spaces', [['white-space', 'break-spaces']]) - functionalUtility('tab', { - handleBareValue: ({ value }) => { - if (!isPositiveInteger(value)) return null - return value - }, - handle: (value) => [decl('tab-size', value)], - }) - - suggest('tab', () => [{ values: ['2', '4', '8'] }]) - staticUtility('text-wrap', [['text-wrap', 'wrap']]) staticUtility('text-nowrap', [['text-wrap', 'nowrap']]) staticUtility('text-balance', [['text-wrap', 'balance']]) @@ -2498,7 +2297,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--border-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), hasDefaultValue: true, }, @@ -2549,22 +2347,6 @@ export function createUtilities(theme: Theme) { color: (value) => [decl('border-inline-end-color', value)], }) - borderSideUtility('border-bs', { - width: (value) => [ - decl('border-block-start-style', 'var(--tw-border-style)'), - decl('border-block-start-width', value), - ], - color: (value) => [decl('border-block-start-color', value)], - }) - - borderSideUtility('border-be', { - width: (value) => [ - decl('border-block-end-style', 'var(--tw-border-style)'), - decl('border-block-end-width', value), - ], - color: (value) => [decl('border-block-end-color', value)], - }) - borderSideUtility('border-t', { width: (value) => [ decl('border-top-style', 'var(--tw-border-style)'), @@ -2969,7 +2751,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--background-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -3049,7 +2830,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--background-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -3298,7 +3078,7 @@ export function createUtilities(theme: Theme) { if (!multiplier) return if (!isValidSpacingMultiplier(candidate.value.value)) return - return desc.position(`--spacing(${candidate.value.value})`) + return desc.position(`calc(${multiplier} * ${candidate.value.value})`) } case 'percentage': { @@ -3317,7 +3097,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--background-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -3341,7 +3120,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--background-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, ]) @@ -3475,19 +3253,13 @@ export function createUtilities(theme: Theme) { defaultValue: null, supportsNegative: true, supportsFractions: false, - handleBareValue({ value }) { - if (!isPositiveInteger(value)) return null - let valueAsNumber = Number(value) - if (valueAsNumber === 0) return '0deg' - if (valueAsNumber === 1) return '1deg' - return `calc(1deg * ${value})` + handleBareValue(value) { + if (!isPositiveInteger(value.value)) return null + return `calc(1deg * ${value.value})` }, - handleNegativeBareValue({ value }) { - if (!isPositiveInteger(value)) return null - let valueAsNumber = Number(value) - if (valueAsNumber === 0) return '0deg' - if (valueAsNumber === 1) return '-1deg' - return `calc(1deg * -${value})` + handleNegativeBareValue(value) { + if (!isPositiveInteger(value.value)) return null + return `calc(1deg * -${value.value})` }, handle: (value) => [ maskPropertiesGradient(), @@ -3699,19 +3471,13 @@ export function createUtilities(theme: Theme) { defaultValue: null, supportsNegative: true, supportsFractions: false, - handleBareValue({ value }) { - if (!isPositiveInteger(value)) return null - let valueAsNumber = Number(value) - if (valueAsNumber === 0) return '0deg' - if (valueAsNumber === 1) return '1deg' - return `calc(1deg * ${value})` + handleBareValue(value) { + if (!isPositiveInteger(value.value)) return null + return `calc(1deg * ${value.value})` }, - handleNegativeBareValue({ value }) { - if (!isPositiveInteger(value)) return null - let valueAsNumber = Number(value) - if (valueAsNumber === 0) return '0deg' - if (valueAsNumber === 1) return '-1deg' - return `calc(1deg * -${value})` + handleNegativeBareValue(value) { + if (!isPositiveInteger(value.value)) return null + return `calc(1deg * -${value.value})` }, handle: (value) => [ maskPropertiesGradient(), @@ -3856,7 +3622,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--fill', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, ]) @@ -3895,10 +3660,6 @@ export function createUtilities(theme: Theme) { } { - // A modifier is only valid as the opacity of a stroke color. The value - // did not resolve to a color, so a modifier would be silently ignored. - if (candidate.modifier) return - let value = theme.resolve(candidate.value.value, ['--stroke-width']) if (value) { return [decl('stroke-width', value)] @@ -3912,7 +3673,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--stroke', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -3949,8 +3709,6 @@ export function createUtilities(theme: Theme) { ['py', 'padding-block'], ['ps', 'padding-inline-start'], ['pe', 'padding-inline-end'], - ['pbs', 'padding-block-start'], - ['pbe', 'padding-block-end'], ['pt', 'padding-top'], ['pr', 'padding-right'], ['pb', 'padding-bottom'], @@ -4052,14 +3810,6 @@ export function createUtilities(theme: Theme) { }, ]) - /** - * @css `font-feature-settings` - */ - functionalUtility('font-features', { - themeKeys: [], - handle: (value) => [decl('font-feature-settings', value)], - }) - staticUtility('uppercase', [['text-transform', 'uppercase']]) staticUtility('lowercase', [['text-transform', 'lowercase']]) staticUtility('capitalize', [['text-transform', 'capitalize']]) @@ -4100,7 +3850,7 @@ export function createUtilities(theme: Theme) { ]) colorUtility('placeholder', { - themeKeys: ['--placeholder-color', '--color'], + themeKeys: ['--background-color', '--color'], handle: (value) => [ styleRule('&::placeholder', [decl('--tw-sort', 'placeholder-color'), decl('color', value)]), ], @@ -4170,7 +3920,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--text-decoration-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -4189,27 +3938,27 @@ export function createUtilities(theme: Theme) { { let cssFilterValue = [ - 'var(--tw-blur, )', - 'var(--tw-brightness, )', - 'var(--tw-contrast, )', - 'var(--tw-grayscale, )', - 'var(--tw-hue-rotate, )', - 'var(--tw-invert, )', - 'var(--tw-saturate, )', - 'var(--tw-sepia, )', - 'var(--tw-drop-shadow, )', + 'var(--tw-blur,)', + 'var(--tw-brightness,)', + 'var(--tw-contrast,)', + 'var(--tw-grayscale,)', + 'var(--tw-hue-rotate,)', + 'var(--tw-invert,)', + 'var(--tw-saturate,)', + 'var(--tw-sepia,)', + 'var(--tw-drop-shadow,)', ].join(' ') let cssBackdropFilterValue = [ - 'var(--tw-backdrop-blur, )', - 'var(--tw-backdrop-brightness, )', - 'var(--tw-backdrop-contrast, )', - 'var(--tw-backdrop-grayscale, )', - 'var(--tw-backdrop-hue-rotate, )', - 'var(--tw-backdrop-invert, )', - 'var(--tw-backdrop-opacity, )', - 'var(--tw-backdrop-saturate, )', - 'var(--tw-backdrop-sepia, )', + 'var(--tw-backdrop-blur,)', + 'var(--tw-backdrop-brightness,)', + 'var(--tw-backdrop-contrast,)', + 'var(--tw-backdrop-grayscale,)', + 'var(--tw-backdrop-hue-rotate,)', + 'var(--tw-backdrop-invert,)', + 'var(--tw-backdrop-opacity,)', + 'var(--tw-backdrop-saturate,)', + 'var(--tw-backdrop-sepia,)', ].join(' ') let filterProperties = () => { @@ -4629,7 +4378,7 @@ export function createUtilities(theme: Theme) { if (candidate.modifier.kind === 'arbitrary') { alpha = candidate.modifier.value } else { - if (isValidOpacityValue(candidate.modifier.value)) { + if (isPositiveInteger(candidate.modifier.value)) { alpha = `${candidate.modifier.value}%` } } @@ -4639,7 +4388,6 @@ export function createUtilities(theme: Theme) { let value = theme.get(['--drop-shadow']) let resolved = theme.resolve(null, ['--drop-shadow']) if (value === null || resolved === null) return - if (candidate.modifier && !alpha) return return [ filterProperties(), @@ -4760,7 +4508,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--drop-shadow-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -4941,9 +4688,7 @@ export function createUtilities(theme: Theme) { ['content', 'none'], ]) functionalUtility('content', { - // BC: We only read from the `--content` theme key for compatibility reasons. It's recommended - // to use the utility with arbitrary values instead. - themeKeys: ['--content'], + themeKeys: [], handle: (value) => [ atRoot([property('--tw-content', '""')]), decl('--tw-content', value), @@ -4953,7 +4698,7 @@ export function createUtilities(theme: Theme) { { let cssContainValue = - 'var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, )' + 'var(--tw-contain-size,) var(--tw-contain-layout,) var(--tw-contain-paint,) var(--tw-contain-style,)' let cssContainProperties = () => { return atRoot([ property('--tw-contain-size'), @@ -5047,7 +4792,7 @@ export function createUtilities(theme: Theme) { { let cssFontVariantNumericValue = - 'var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, )' + 'var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)' let fontVariantNumericProperties = () => { return atRoot([ property('--tw-ordinal'), @@ -5218,7 +4963,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--outline-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), hasDefaultValue: true, }, @@ -5308,7 +5052,7 @@ export function createUtilities(theme: Theme) { if (!modifier && isValidSpacingMultiplier(candidate.modifier.value)) { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null - modifier = `--spacing(${candidate.modifier.value})` + modifier = `calc(${multiplier} * ${candidate.modifier.value})` } // Shorthand for `leading-none` @@ -5361,7 +5105,7 @@ export function createUtilities(theme: Theme) { if (!modifier && isValidSpacingMultiplier(candidate.modifier.value)) { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null - modifier = `--spacing(${candidate.modifier.value})` + modifier = `calc(${multiplier} * ${candidate.modifier.value})` } // Shorthand for `leading-none` @@ -5409,7 +5153,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--text-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -5439,7 +5182,7 @@ export function createUtilities(theme: Theme) { if (candidate.modifier.kind === 'arbitrary') { alpha = candidate.modifier.value } else { - if (isValidOpacityValue(candidate.modifier.value)) { + if (isPositiveInteger(candidate.modifier.value)) { alpha = `${candidate.modifier.value}%` } } @@ -5448,7 +5191,6 @@ export function createUtilities(theme: Theme) { if (!candidate.value) { let value = theme.get(['--text-shadow']) if (value === null) return - if (candidate.modifier && !alpha) return return [ textShadowProperties(), @@ -5476,8 +5218,6 @@ export function createUtilities(theme: Theme) { ] } default: { - if (candidate.modifier && !alpha) return - return [ textShadowProperties(), decl('--tw-text-shadow-alpha', alpha), @@ -5506,8 +5246,6 @@ export function createUtilities(theme: Theme) { { let value = theme.get([`--text-shadow-${candidate.value.value}`]) if (value) { - if (candidate.modifier && !alpha) return - return [ textShadowProperties(), decl('--tw-text-shadow-alpha', alpha), @@ -5537,7 +5275,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--text-shadow-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -5590,7 +5327,7 @@ export function createUtilities(theme: Theme) { if (candidate.modifier.kind === 'arbitrary') { alpha = candidate.modifier.value } else { - if (isValidOpacityValue(candidate.modifier.value)) { + if (isPositiveInteger(candidate.modifier.value)) { alpha = `${candidate.modifier.value}%` } } @@ -5599,7 +5336,6 @@ export function createUtilities(theme: Theme) { if (!candidate.value) { let value = theme.get(['--shadow']) if (value === null) return - if (candidate.modifier && !alpha) return return [ boxShadowProperties(), @@ -5629,8 +5365,6 @@ export function createUtilities(theme: Theme) { ] } default: { - if (candidate.modifier && !alpha) return - return [ boxShadowProperties(), decl('--tw-shadow-alpha', alpha), @@ -5664,8 +5398,6 @@ export function createUtilities(theme: Theme) { { let value = theme.get([`--shadow-${candidate.value.value}`]) if (value) { - if (candidate.modifier && !alpha) return - return [ boxShadowProperties(), decl('--tw-shadow-alpha', alpha), @@ -5696,7 +5428,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--box-shadow-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -5721,7 +5452,7 @@ export function createUtilities(theme: Theme) { if (candidate.modifier.kind === 'arbitrary') { alpha = candidate.modifier.value } else { - if (isValidOpacityValue(candidate.modifier.value)) { + if (isPositiveInteger(candidate.modifier.value)) { alpha = `${candidate.modifier.value}%` } } @@ -5730,7 +5461,6 @@ export function createUtilities(theme: Theme) { if (!candidate.value) { let value = theme.get(['--inset-shadow']) if (value === null) return - if (candidate.modifier && !alpha) return return [ boxShadowProperties(), @@ -5760,8 +5490,6 @@ export function createUtilities(theme: Theme) { ] } default: { - if (candidate.modifier && !alpha) return - return [ boxShadowProperties(), decl('--tw-inset-shadow-alpha', alpha), @@ -5770,7 +5498,7 @@ export function createUtilities(theme: Theme) { value, alpha, (color) => `var(--tw-inset-shadow-color, ${color})`, - 'inset', + 'inset ', ), decl('box-shadow', cssBoxShadowValue), ] @@ -5783,7 +5511,7 @@ export function createUtilities(theme: Theme) { if (candidate.modifier) return return [ boxShadowProperties(), - decl('--tw-inset-shadow', `inset ${nullShadow}`), + decl('--tw-inset-shadow', nullShadow), decl('box-shadow', cssBoxShadowValue), ] @@ -5797,8 +5525,6 @@ export function createUtilities(theme: Theme) { let value = theme.get([`--inset-shadow-${candidate.value.value}`]) if (value) { - if (candidate.modifier && !alpha) return - return [ boxShadowProperties(), decl('--tw-inset-shadow-alpha', alpha), @@ -5829,7 +5555,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--box-shadow-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -5846,7 +5571,7 @@ export function createUtilities(theme: Theme) { let defaultRingColor = theme.get(['--default-ring-color']) ?? 'currentcolor' function ringShadowValue(value: string) { - return `var(--tw-ring-inset, ) 0 0 0 calc(${value} + var(--tw-ring-offset-width)) var(--tw-ring-color, ${defaultRingColor})` + return `var(--tw-ring-inset,) 0 0 0 calc(${value} + var(--tw-ring-offset-width)) var(--tw-ring-color, ${defaultRingColor})` } utilities.functional('ring', (candidate) => { if (!candidate.value) { @@ -5910,7 +5635,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--ring-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -5984,7 +5708,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--ring-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -5995,7 +5718,7 @@ export function createUtilities(theme: Theme) { ]) let ringOffsetShadowValue = - 'var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)' + 'var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)' utilities.functional('ring-offset', (candidate) => { if (!candidate.value) return @@ -6052,7 +5775,6 @@ export function createUtilities(theme: Theme) { { values: ['current', 'inherit', 'transparent'], valueThemeKeys: ['--ring-offset-color', '--color'], - modifierThemeKeys: ['--opacity'], modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), }, { @@ -6069,7 +5791,11 @@ export function createUtilities(theme: Theme) { value = candidate.value.value } else if (candidate.value.kind === 'named' && candidate.value.value === 'normal') { value = 'normal' - } else if (candidate.value.kind === 'named' && candidate.value.value === 'size') { + } else if ( + enableContainerSizeUtility && + candidate.value.kind === 'named' && + candidate.value.value === 'size' + ) { value = 'size' } @@ -6104,22 +5830,16 @@ export const BARE_VALUE_DATA_TYPES = [ ] export function createCssUtility(node: AtRule) { - // Allow escaped characters in the name for compatibility with formatters and - // other parsers, to ensure valid CSS syntax. E.g.: `@utility foo-1\/2`. - // - // Note: the actual utility will be `foo-1/2` - let name = unescape(node.params) + let name = node.params // Functional utilities. E.g.: `tab-size-*` - if (isValidFunctionalUtilityName(name)) { + if (IS_VALID_FUNCTIONAL_UTILITY_NAME.test(name)) { // API: // // - `--value('literal')` resolves a literal named value // - `--value(number)` resolves a bare value of type number // - `--value([number])` resolves an arbitrary value of type number // - `--value(--color)` resolves a theme value in the `color` namespace - // - `--value(--default(4))` resolves to a default value when only the - // root of the functional utility was used. // - `--value(number, [number])` resolves a bare value of type number or an // arbitrary value of type number in order. // @@ -6220,15 +5940,6 @@ export function createCssUtility(node: AtRule) { let args = segment(ValueParser.toCss(fn.nodes), ',') for (let [idx, arg] of args.entries()) { - arg = arg.trim() - - // The value of `--default(…)` is emitted as-is, so it should not be - // normalized. - if (arg.startsWith('--default(')) { - args[idx] = arg - continue - } - // Transform escaped `\\*` -> `*` arg = arg.replace(/\\\*/g, '*') @@ -6242,7 +5953,7 @@ export function createCssUtility(node: AtRule) { arg = arg.replace(/(-\*){2,}/g, '-*') // Ensure trailing `-*` exists if `-*` isn't present yet - if (arg[0] === '-' && arg[1] === '-' && !arg.includes('(') && !arg.includes('-*')) { + if (arg[0] === '-' && arg[1] === '-' && !arg.includes('-*')) { arg += '-*' } @@ -6308,8 +6019,9 @@ export function createCssUtility(node: AtRule) { let value = candidate.value let modifier = candidate.modifier - // Functional CSS utilities must resolve at least one `--value(…)`. - // Use `--default(…)` inside `--value(…)` for the omitted-value case. + // A value is required for functional utilities, if you want to accept + // just `tab-size`, you'd have to use a static utility. + if (value === null) return // Whether `--value(…)` was used let usedValueFn = false @@ -6350,14 +6062,14 @@ export function createCssUtility(node: AtRule) { let shouldRemoveDeclaration = false let valueAst = ValueParser.parse(node.value) - walk(valueAst, (fnNode) => { - if (fnNode.kind !== 'function') return + walk(valueAst, (valueNode) => { + if (valueNode.kind !== 'function') return // Value function, e.g.: `--value(integer)` - if (fnNode.value === '--value') { + if (valueNode.value === '--value') { usedValueFn = true - let resolved = resolveValueFunction(value, fnNode, designSystem) + let resolved = resolveValueFunction(value, valueNode, designSystem) if (resolved) { resolvedValueFn = true if (resolved.ratio) { @@ -6369,21 +6081,30 @@ export function createCssUtility(node: AtRule) { } // Drop the declaration in case we couldn't resolve the value + usedValueFn ||= false shouldRemoveDeclaration = true return WalkAction.Stop } // Modifier function, e.g.: `--modifier(integer)` - else if (fnNode.value === '--modifier') { + else if (valueNode.value === '--modifier') { + // If there is no modifier present in the candidate, then the + // declaration can be removed. + if (modifier === null) { + shouldRemoveDeclaration = true + return WalkAction.Stop + } + usedModifierFn = true - let resolved = resolveValueFunction(modifier, fnNode, designSystem) - if (resolved) { + let replacement = resolveValueFunction(modifier, valueNode, designSystem) + if (replacement) { resolvedModifierFn = true - return WalkAction.ReplaceSkip(resolved.nodes) + return WalkAction.ReplaceSkip(replacement.nodes) } // Drop the declaration in case we couldn't resolve the value + usedModifierFn ||= false shouldRemoveDeclaration = true return WalkAction.Stop } @@ -6396,12 +6117,11 @@ export function createCssUtility(node: AtRule) { node.value = ValueParser.toCss(valueAst) }) - // Functional CSS utilities require `--value(…)`, and one of those - // branches must resolve for the candidate to be valid. - if (!usedValueFn || !resolvedValueFn) return null + // Used `--value(…)` but nothing resolved + if (usedValueFn && !resolvedValueFn) return null // Used `--modifier(…)` but nothing resolved - if (usedModifierFn && !resolvedModifierFn && modifier !== null) return null + if (usedModifierFn && !resolvedModifierFn) return null // Resolved `--value(ratio)` and `--modifier(…)`, which is invalid if (resolvedRatioValue && resolvedModifierFn) return null @@ -6462,7 +6182,7 @@ export function createCssUtility(node: AtRule) { } } - if (isValidStaticUtilityName(name)) { + if (IS_VALID_STATIC_UTILITY_NAME.test(name)) { return (designSystem: DesignSystem) => { designSystem.utilities.static(name, () => node.nodes.map(cloneAstNode)) } @@ -6472,23 +6192,13 @@ export function createCssUtility(node: AtRule) { } function resolveValueFunction( - value: + value: NonNullable< | Extract['value'] - | Extract['modifier'], + | Extract['modifier'] + >, fn: ValueParser.ValueFunctionNode, designSystem: DesignSystem, ): { nodes: ValueParser.ValueAstNode[]; ratio?: boolean } | undefined { - // No value provided, we can try `--default(…)` - if (value === null) { - for (let arg of fn.nodes) { - // Resolve default value, e.g.: `--default(…)` - if (arg.kind === 'function' && arg.value === '--default') { - return { nodes: arg.nodes } - } - } - return - } - for (let arg of fn.nodes) { // Resolve literal value, e.g.: `--modifier('closest-side')` if ( @@ -6558,7 +6268,7 @@ function resolveValueFunction( // Ratio must be a valid fraction, e.g.: / if (type === 'ratio') { - let [lhs, rhs] = segment(resolved, '/').map(Number) + let [lhs, rhs] = segment(resolved, '/') if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) continue } @@ -6573,12 +6283,7 @@ function resolveValueFunction( continue } - if (type === 'ratio') { - let [lhs, rhs] = segment(resolved, '/') - return { nodes: ValueParser.parse(`${lhs.trim()} / ${rhs.trim()}`), ratio: true } - } - - return { nodes: ValueParser.parse(resolved), ratio: false } + return { nodes: ValueParser.parse(resolved), ratio: type === 'ratio' } } // Arbitrary value, e.g.: `--value([integer])` @@ -6657,8 +6362,8 @@ function alphaReplacedShadowProperties( function applyPrefix(x: string) { if (!prefix) return x return segment(x, ',') - .map((value) => prefix.trim() + ' ' + value.trim()) - .join(', ') + .map((value) => prefix + value) + .join(',') } if (requiresFallback) { @@ -6721,143 +6426,3 @@ function alphaReplacedDropShadowProperties( return [decl(property, prefix + replacedValue)] } } - -const UTILITY_ROOT = /^-?[a-z][a-zA-Z0-9_-]*/ - -const PERCENT = 37 -const SLASH = 47 -const DOT = 46 -const LOWER_A = 97 -const LOWER_Z = 122 -const UPPER_A = 65 -const UPPER_Z = 90 -const ZERO = 48 -const NINE = 57 -const UNDERSCORE = 95 -const DASH = 45 - -export function isValidStaticUtilityName(name: string): boolean { - let match = UTILITY_ROOT.exec(name) - if (match === null) return false // Invalid root - - let root = match[0] - let value = name.slice(root.length) - - // Root should not end in `-` if there is no value - // - // `tab-size-` - // --------- Root - if (value.length === 0 && root.endsWith('-')) { - return false - } - - // No remaining value is valid - // - // `tab-size` - // -------- Root - if (value.length === 0) { - return true - } - - // Any valid (static) utility should be valid including: - // - Bare values with `.`: `p-1.5` - // - Bare values with `%`: `w-50%` - // - With an embedded modifier: `text-xs/8` - - let seenSlash = false - for (let i = 0; i < value.length; i++) { - let charCode = value.charCodeAt(i) - switch (charCode) { - case PERCENT: { - // A percentage is only valid at the end of the value - if (i !== value.length - 1) return false - - // A percent is only valid when preceded by a digit. E.g.: `w-%` is invalid - let previousChar = value[i - 1] || root[root.length - 1] || '' - let previousCharCode = previousChar.charCodeAt(0) - if (previousCharCode < ZERO || previousCharCode > NINE) return false - break - } - - case SLASH: { - // A slash must be followed by at least 1 character. E.g.: `foo/` is invalid - if (i === value.length - 1) return false - - // A slash can only appear once. E.g.: `foo/bar/baz` is invalid - if (seenSlash) return false - seenSlash = true - break - } - - case DOT: { - // Dots are only allowed between digits. E.g.: `p-1.a` is invalid - let previousChar = value[i - 1] || root[root.length - 1] || '' - let previousCharCode = previousChar.charCodeAt(0) - if (previousCharCode < ZERO || previousCharCode > NINE) return false - - let nextChar = value[i + 1] || '' - let nextCharCode = nextChar.charCodeAt(0) - if (nextCharCode < ZERO || nextCharCode > NINE) return false - break - } - - // Allowed special characters - case UNDERSCORE: - case DASH: { - continue - } - - default: { - if ( - (charCode >= LOWER_A && charCode <= LOWER_Z) || // Allow a-z - (charCode >= UPPER_A && charCode <= UPPER_Z) || // Allow A-Z - (charCode >= ZERO && charCode <= NINE) // Allow 0-9 - ) { - continue - } - - // Everything else is invalid - return false - } - } - } - - return true -} - -export function isValidFunctionalUtilityName(name: string): boolean { - if (!name.endsWith('-*')) return false // Missing '-*' suffix - name = name.slice(0, -2) - - let match = UTILITY_ROOT.exec(name) - if (match === null) return false // Invalid root - - let root = match[0] - let value = name.slice(root.length) - - // No remaining value is valid - // - // `tab-size-*` - // -------- Root - // -- Suffix - // - // Backwards compatibility: a root ending in `-` was valid and correctly - // scanned by Oxide. This means that custom utilities can result in candidates - // such as `foo--bar`. - // - // We might want to revisit this for Tailwind CSS v5, but for now we have to - // make it backwards compatible. - // - // PR: https://github.com/tailwindlabs/tailwindcss/pull/19696 - // - if (value.length === 0) { - return true - } - - // But if there is a value remaining, it's invalid. - // - // E.g.: `tab-size-[…]-*` - // - // If we allow more characters, we can extend the validation here - return false -} diff --git a/packages/tailwindcss/src/utils/compare-breakpoints.ts b/packages/tailwindcss/src/utils/compare-breakpoints.ts index 4eb562764..08e97998e 100644 --- a/packages/tailwindcss/src/utils/compare-breakpoints.ts +++ b/packages/tailwindcss/src/utils/compare-breakpoints.ts @@ -25,7 +25,7 @@ export function compareBreakpoints(a: string, z: string, direction: 'asc' | 'des // Compare by bucket name (aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) || // If bucket names are the same, compare by value - (direction === 'asc' ? parseFloat(a) - parseFloat(z) : parseFloat(z) - parseFloat(a)) + (direction === 'asc' ? parseInt(a) - parseInt(z) : parseInt(z) - parseInt(a)) // If the groups are the same, and the contents are not numbers, the // `order` will result in `NaN`. In this case, we want to make sorting diff --git a/packages/tailwindcss/src/utils/escape.test.ts b/packages/tailwindcss/src/utils/escape.test.ts index f4c8e1192..ff7715b9d 100644 --- a/packages/tailwindcss/src/utils/escape.test.ts +++ b/packages/tailwindcss/src/utils/escape.test.ts @@ -11,14 +11,4 @@ describe('unescape', () => { test('removes backslashes', () => { expect(unescape(String.raw`red-1\/2`)).toMatchInlineSnapshot(`"red-1/2"`) }) - - test('replaces out-of-range escaped code points', () => { - expect( - unescape( - String.raw`--Coding-Projects-CharacterMapper-Master-Workspace\d8819554-4725-4235-9d22-2d0ed572e924`, - ), - ).toMatchInlineSnapshot( - `"--Coding-Projects-CharacterMapper-Master-Workspace�54-4725-4235-9d22-2d0ed572e924"`, - ) - }) }) diff --git a/packages/tailwindcss/src/utils/escape.ts b/packages/tailwindcss/src/utils/escape.ts index c430e4ad4..cbcfa62a0 100644 --- a/packages/tailwindcss/src/utils/escape.ts +++ b/packages/tailwindcss/src/utils/escape.ts @@ -74,24 +74,8 @@ export function escape(value: string) { export function unescape(escaped: string) { return escaped.replace(/\\([\dA-Fa-f]{1,6}[\t\n\f\r ]?|[\S\s])/g, (match) => { - if (match.length <= 2) { - return match[1] - } - - let codePoint = Number.parseInt(match.slice(1).trim(), 16) - - if ( - // Invalid codepoint: https://infra.spec.whatwg.org/#code-point - codePoint === 0x0000 || - codePoint > 0x10ffff || - // Is surrogate: https://infra.spec.whatwg.org/#leading-surrogate - // - A leading surrogate is a code point that is in the range U+D800 to U+DBFF, inclusive. - // - A trailing surrogate is a code point that is in the range U+DC00 to U+DFFF, inclusive. - (codePoint >= 0xd800 && codePoint <= 0xdfff) - ) { - return '\uFFFD' - } - - return String.fromCodePoint(codePoint) + return match.length > 2 + ? String.fromCodePoint(Number.parseInt(match.slice(1).trim(), 16)) + : match[1] }) } diff --git a/packages/tailwindcss/src/utils/infer-data-type.ts b/packages/tailwindcss/src/utils/infer-data-type.ts index 3317572f3..587bae785 100644 --- a/packages/tailwindcss/src/utils/infer-data-type.ts +++ b/packages/tailwindcss/src/utils/infer-data-type.ts @@ -184,7 +184,7 @@ function isPercentage(value: string): boolean { /* -------------------------------------------------------------------------- */ -const IS_FRACTION = new RegExp(`^${HAS_NUMBER.source}\\s*/\\s*${HAS_NUMBER.source}$`) +const IS_FRACTION = new RegExp(`^${HAS_NUMBER.source}\s*/\s*${HAS_NUMBER.source}$`) function isFraction(value: string): boolean { return IS_FRACTION.test(value) || hasMathFn(value) @@ -232,10 +232,9 @@ const LENGTH_UNITS = [ ] const IS_LENGTH = new RegExp(`^${HAS_NUMBER.source}(${LENGTH_UNITS.join('|')})$`) -const IS_LENGTH_FN = /^(--spacing)\(/i export function isLength(value: string): boolean { - return IS_LENGTH.test(value) || IS_LENGTH_FN.test(value) || hasMathFn(value) + return IS_LENGTH.test(value) || hasMathFn(value) } /* -------------------------------------------------------------------------- */ diff --git a/packages/tailwindcss/src/utils/is-color.ts b/packages/tailwindcss/src/utils/is-color.ts index de7c4cbd4..e3501aa86 100644 --- a/packages/tailwindcss/src/utils/is-color.ts +++ b/packages/tailwindcss/src/utils/is-color.ts @@ -195,14 +195,10 @@ const NAMED_COLORS = new Set([ 'accentcolortext', ]) -const IS_COLOR_FN = /^(rgba?|hsla?|hwb|color|(ok)?(lab|lch)|light-dark|color-mix|--alpha)\(/i +const IS_COLOR_FN = /^(rgba?|hsla?|hwb|color|(ok)?(lab|lch)|light-dark|color-mix)\(/i export function isColor(value: string): boolean { return ( value.charCodeAt(0) === HASH || IS_COLOR_FN.test(value) || NAMED_COLORS.has(value.toLowerCase()) ) } - -export function isNamedColor(value: string): boolean { - return NAMED_COLORS.has(value.toLowerCase()) -} diff --git a/packages/tailwindcss/src/utils/replace-shadow-colors.test.ts b/packages/tailwindcss/src/utils/replace-shadow-colors.test.ts index 701e80f05..6303e5a8d 100644 --- a/packages/tailwindcss/src/utils/replace-shadow-colors.test.ts +++ b/packages/tailwindcss/src/utils/replace-shadow-colors.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { cartesian } from '../cartesian' import { replaceAlpha } from '../utilities' import { replaceShadowColors } from './replace-shadow-colors' @@ -41,59 +40,6 @@ describe('without replacer', () => { expect(parsed).toMatchInlineSnapshot(`"1px 2px 3px 4px var(--tw-shadow-color, currentcolor)"`) }) - it('should find the color regardless of its position', () => { - for (let [x, y, blur, spread, color] of cartesian( - ['calc(var(--spacing) * 1)', '1', '--spacing(1)'], // x - ['calc(var(--spacing) * 2)', '2', '--spacing(2)'], // y - ['calc(var(--spacing) * 3)', '3', '--spacing(3)'], // blur - ['calc(var(--spacing) * 4)', '4', '--spacing(4)'], // spread - ['black', 'rgb(0, 0, 0)', '#000', '--alpha(var(--color) / 50%)', 'var(--uknown-color)'], // color - )) { - let expectedColor = `var(--tw-shadow-color, ${color})` - - { - let input = `${x} ${color} ${y} ${blur} ${spread}` - expect(replaceShadowColors(input, replacer)).toEqual(input.replace(color, expectedColor)) - } - - { - let input = `${x} ${y} ${color} ${blur} ${spread}` - expect(replaceShadowColors(input, replacer)).toEqual(input.replace(color, expectedColor)) - } - - { - let input = `${x} ${y} ${blur} ${color} ${spread}` - expect(replaceShadowColors(input, replacer)).toEqual(input.replace(color, expectedColor)) - } - } - }) - - // When using `var(…)`, we don't know the types of the used variables, but we - // might be able to find the color itself. - it.each([ - 'black', // Named color - '#000', // Hex color - 'rgb(0, 0, 0)', // Color functions - '--alpha(var(--color) / 50%)', // Known custom functions - ])('should find the color (%s)', (color) => { - let expectedColor = `var(--tw-shadow-color, ${color})` - - { - let input = `var(--x) var(--y) ${color}` - expect(replaceShadowColors(input, replacer)).toEqual(input.replace(color, expectedColor)) - } - - { - let input = `var(--x) var(--y) var(--blur) ${color}` - expect(replaceShadowColors(input, replacer)).toEqual(input.replace(color, expectedColor)) - } - - { - let input = `var(--x) var(--y) var(--blur) var(--spread) ${color}` - expect(replaceShadowColors(input, replacer)).toEqual(input.replace(color, expectedColor)) - } - }) - it('should handle multiple shadows', () => { let parsed = replaceShadowColors( ['var(--my-shadow)', '1px 1px var(--my-color)', '0 0 1px var(--my-color)'].join(', '), diff --git a/packages/tailwindcss/src/utils/replace-shadow-colors.ts b/packages/tailwindcss/src/utils/replace-shadow-colors.ts index 5f9e0264c..e51f9c2a0 100644 --- a/packages/tailwindcss/src/utils/replace-shadow-colors.ts +++ b/packages/tailwindcss/src/utils/replace-shadow-colors.ts @@ -1,134 +1,47 @@ -import * as ValueParser from '../value-parser' -import { walk, WalkAction } from '../walk' -import { isNamedColor } from './is-color' import { segment } from './segment' const KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset']) -const LENGTH_FUNCTIONS = new Set(['calc', 'clamp', 'max', 'min', '--spacing']) -const COLOR_FUNCTIONS = new Set([ - 'color', - 'color-mix', - 'contrast-color', - 'device-cmyk', - 'hsl', - 'hsla', - 'hwb', - 'lab', - 'lch', - 'light-dark', - 'oklab', - 'oklch', - 'rgb', - 'rgba', - '--alpha', -]) -const LENGTH = /^-?(\d+|\.\d+)(.*?)$/ +const LENGTH = /^-?(\d+|\.\d+)(.*?)$/g export function replaceShadowColors(input: string, replacement: (color: string) => string) { - function replaceAst(node: ValueParser.ValueAstNode): ValueParser.ValueAstNode[] { - let color = ValueParser.toCss([node]) - let updatedColor = replacement(color) - let ast = ValueParser.parse(updatedColor) - return ast - } - let shadows = segment(input, ',').map((shadow) => { shadow = shadow.trim() - let ast = ValueParser.parse(shadow) + let parts = segment(shadow, ' ').filter((part) => part.trim() !== '') + let color = null + let offsetX = null + let offsetY = null - let unknown: ValueParser.ValueAstNode | null = null - let unknowns = 0 - let lengths = 0 - let replaced = false - - walk(ast, (node) => { - switch (node.kind) { - case 'word': { - // Skip known keywords - if (KEYWORDS.has(node.value.toLowerCase())) { - return WalkAction.Continue - } - - // Must be a length - if (LENGTH.test(node.value.toLowerCase())) { - lengths++ - return WalkAction.Continue - } - - // Must be a color - if (node.value[0] === '#' || isNamedColor(node.value)) { - replaced = true - return WalkAction.ReplaceStop(replaceAst(node)) - } - - // We're not sure yet - unknown = node - unknowns++ - break + for (let part of parts) { + if (KEYWORDS.has(part)) { + continue + } else if (LENGTH.test(part)) { + if (offsetX === null) { + offsetX = part + } else if (offsetY === null) { + offsetY = part } - case 'function': { - // Must be a color - if (COLOR_FUNCTIONS.has(node.value.toLowerCase())) { - replaced = true - return WalkAction.ReplaceStop(replaceAst(node)) - } - - // Must be a length - if (LENGTH_FUNCTIONS.has(node.value.toLowerCase())) { - lengths++ - return WalkAction.Skip - } - - // We're not sure yet - unknown = node - unknowns++ - - // We're not interested in the arguments of the function - return WalkAction.Skip - } - - // Ignore separators - case 'separator': - return WalkAction.Continue - - default: - node satisfies never + // Reset index, since the regex is stateful. + LENGTH.lastIndex = 0 + } else if (color === null) { + color = part } - }) - - // We definitely found a color, nothing else to do - if (replaced) { - return ValueParser.toCss(ast) } // If the x and y offsets were not detected, the shadow is either invalid or // using a variable to represent more than one field in the shadow value, so // we can't know what to replace. - if (lengths < 2) { - return shadow - } + if (offsetX === null || offsetY === null) return shadow + let replacementColor = replacement(color ?? 'currentcolor') + + if (color !== null) { + // If a color was found, replace the color. + return shadow.replace(color, replacementColor) + } // If no color was found, assume the shadow is relying on the browser // default shadow color and append the replacement color. - if (unknowns === 0) { - return `${shadow} ${replacement('currentcolor')}` - } - - // A single left-over, we assume that this is the color - if (unknowns === 1) { - walk(ast, (node) => { - if (node === unknown) { - replaced = true - return WalkAction.ReplaceStop(replaceAst(node)) - } - - // Keep the walk top-level only, no need to go into functions - return WalkAction.Skip - }) - } - - return replaced ? ValueParser.toCss(ast) : shadow + return `${shadow} ${replacementColor}` }) return shadows.join(', ') diff --git a/packages/tailwindcss/src/utils/variables.ts b/packages/tailwindcss/src/utils/variables.ts index b72987565..57ac4c3a5 100644 --- a/packages/tailwindcss/src/utils/variables.ts +++ b/packages/tailwindcss/src/utils/variables.ts @@ -1,8 +1,7 @@ import * as ValueParser from '../value-parser' import { walk, WalkAction } from '../walk' -import { DefaultMap } from './default-map' -let extractUsedVariablesCache = new DefaultMap((raw) => { +export function extractUsedVariables(raw: string): string[] { let variables: string[] = [] walk(ValueParser.parse(raw), (node) => { if (node.kind !== 'function' || node.value !== 'var') return @@ -16,8 +15,4 @@ let extractUsedVariablesCache = new DefaultMap((raw) => { return WalkAction.Skip }) return variables -}) - -export function extractUsedVariables(raw: string): string[] { - return extractUsedVariablesCache.get(raw) } diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 37f9ebbea..a2540e698 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1,58 +1,50 @@ +import dedent from 'dedent' import { expect, test } from 'vitest' import createPlugin from './plugin' -import { run } from './test-utils/run' +import { compileCss, run } from './test-utils/run' import { Compounds, compoundsForSelectors } from './variants' const css = String.raw test('*', async () => { expect(await run(['*:flex'])).toMatchInlineSnapshot(` - " - :is(.\\*\\:flex > *) { + ":is(.\\*\\:flex > *) { display: flex; - } - " + }" `) expect(await run(['*/foo:flex'])).toEqual('') }) test('**', async () => { expect(await run(['**:flex'])).toMatchInlineSnapshot(` - " - :is(.\\*\\*\\:flex *) { + ":is(.\\*\\*\\:flex *) { display: flex; - } - " + }" `) expect(await run(['**/foo:flex'])).toEqual('') }) test('first-letter', async () => { expect(await run(['first-letter:flex'])).toMatchInlineSnapshot(` - " - .first-letter\\:flex:first-letter { + ".first-letter\\:flex:first-letter { display: flex; - } - " + }" `) expect(await run(['first-letter/foo:flex'])).toEqual('') }) test('first-line', async () => { expect(await run(['first-line:flex'])).toMatchInlineSnapshot(` - " - .first-line\\:flex:first-line { + ".first-line\\:flex:first-line { display: flex; - } - " + }" `) expect(await run(['first-line/foo:flex'])).toEqual('') }) test('marker', async () => { expect(await run(['marker:flex'])).toMatchInlineSnapshot(` - " - .marker\\:flex ::marker { + ".marker\\:flex ::marker { display: flex; } @@ -66,75 +58,63 @@ test('marker', async () => { .marker\\:flex::-webkit-details-marker { display: flex; - } - " + }" `) expect(await run(['marker/foo:flex'])).toEqual('') }) test('selection', async () => { expect(await run(['selection:flex'])).toMatchInlineSnapshot(` - " - .selection\\:flex ::selection { + ".selection\\:flex ::selection { display: flex; } .selection\\:flex::selection { display: flex; - } - " + }" `) expect(await run(['selection/foo:flex'])).toEqual('') }) test('file', async () => { expect(await run(['file:flex'])).toMatchInlineSnapshot(` - " - .file\\:flex::file-selector-button { + ".file\\:flex::file-selector-button { display: flex; - } - " + }" `) expect(await run(['file/foo:flex'])).toEqual('') }) test('placeholder', async () => { expect(await run(['placeholder:flex'])).toMatchInlineSnapshot(` - " - .placeholder\\:flex::placeholder { + ".placeholder\\:flex::placeholder { display: flex; - } - " + }" `) expect(await run(['placeholder/foo:flex'])).toEqual('') }) test('backdrop', async () => { expect(await run(['backdrop:flex'])).toMatchInlineSnapshot(` - " - .backdrop\\:flex::backdrop { + ".backdrop\\:flex::backdrop { display: flex; - } - " + }" `) expect(await run(['backdrop/foo:flex'])).toEqual('') }) test('details-content', async () => { expect(await run(['details-content:flex'])).toMatchInlineSnapshot(` - " - .details-content\\:flex::details-content { + ".details-content\\:flex::details-content { display: flex; - } - " + }" `) expect(await run(['details-content/foo:flex'])).toEqual('') }) test('before', async () => { expect(await run(['before:flex'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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: ""; @@ -151,16 +131,14 @@ test('before', async () => { syntax: "*"; inherits: false; initial-value: ""; - } - " + }" `) expect(await run(['before/foo:flex'])).toEqual('') }) test('after', async () => { expect(await run(['after:flex'])).toMatchInlineSnapshot(` - " - @layer properties { + "@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: ""; @@ -177,63 +155,52 @@ test('after', async () => { syntax: "*"; inherits: false; initial-value: ""; - } - " + }" `) expect(await run(['after/foo:flex'])).toEqual('') }) test('first', async () => { expect(await run(['first:flex', 'group-first:flex', 'peer-first:flex'])).toMatchInlineSnapshot(` - " - :is(:where(.group):first-child .group-first\\:flex), :is(:where(.peer):first-child ~ .peer-first\\:flex), .first\\:flex:first-child { + ".group-first\\:flex:is(:where(.group):first-child *), .peer-first\\:flex:is(:where(.peer):first-child ~ *), .first\\:flex:first-child { display: flex; - } - " + }" `) expect(await run(['first/foo:flex'])).toEqual('') }) test('last', async () => { expect(await run(['last:flex', 'group-last:flex', 'peer-last:flex'])).toMatchInlineSnapshot(` - " - :is(:where(.group):last-child .group-last\\:flex), :is(:where(.peer):last-child ~ .peer-last\\:flex), .last\\:flex:last-child { + ".group-last\\:flex:is(:where(.group):last-child *), .peer-last\\:flex:is(:where(.peer):last-child ~ *), .last\\:flex:last-child { display: flex; - } - " + }" `) expect(await run(['last/foo:flex'])).toEqual('') }) test('only', async () => { expect(await run(['only:flex', 'group-only:flex', 'peer-only:flex'])).toMatchInlineSnapshot(` - " - :is(:where(.group):only-child .group-only\\:flex), :is(:where(.peer):only-child ~ .peer-only\\:flex), .only\\:flex:only-child { + ".group-only\\:flex:is(:where(.group):only-child *), .peer-only\\:flex:is(:where(.peer):only-child ~ *), .only\\:flex:only-child { display: flex; - } - " + }" `) expect(await run(['only/foo:flex'])).toEqual('') }) test('odd', async () => { expect(await run(['odd:flex', 'group-odd:flex', 'peer-odd:flex'])).toMatchInlineSnapshot(` - " - :is(:where(.group):nth-child(odd) .group-odd\\:flex), :is(:where(.peer):nth-child(odd) ~ .peer-odd\\:flex), .odd\\:flex:nth-child(odd) { + ".group-odd\\:flex:is(:where(.group):nth-child(odd) *), .peer-odd\\:flex:is(:where(.peer):nth-child(odd) ~ *), .odd\\:flex:nth-child(odd) { display: flex; - } - " + }" `) expect(await run(['odd/foo:flex'])).toEqual('') }) test('even', async () => { expect(await run(['even:flex', 'group-even:flex', 'peer-even:flex'])).toMatchInlineSnapshot(` - " - :is(:where(.group):nth-child(2n) .group-even\\:flex), :is(:where(.peer):nth-child(2n) ~ .peer-even\\:flex), .even\\:flex:nth-child(2n) { + ".group-even\\:flex:is(:where(.group):nth-child(2n) *), .peer-even\\:flex:is(:where(.peer):nth-child(2n) ~ *), .even\\:flex:nth-child(2n) { display: flex; - } - " + }" `) expect(await run(['even/foo:flex'])).toEqual('') }) @@ -241,11 +208,9 @@ test('even', async () => { test('first-of-type', async () => { expect(await run(['first-of-type:flex', 'group-first-of-type:flex', 'peer-first-of-type:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):first-of-type .group-first-of-type\\:flex), :is(:where(.peer):first-of-type ~ .peer-first-of-type\\:flex), .first-of-type\\:flex:first-of-type { + ".group-first-of-type\\:flex:is(:where(.group):first-of-type *), .peer-first-of-type\\:flex:is(:where(.peer):first-of-type ~ *), .first-of-type\\:flex:first-of-type { display: flex; - } - " + }" `) expect(await run(['first-of-type/foo:flex'])).toEqual('') }) @@ -253,11 +218,9 @@ test('first-of-type', async () => { test('last-of-type', async () => { expect(await run(['last-of-type:flex', 'group-last-of-type:flex', 'peer-last-of-type:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):last-of-type .group-last-of-type\\:flex), :is(:where(.peer):last-of-type ~ .peer-last-of-type\\:flex), .last-of-type\\:flex:last-of-type { + ".group-last-of-type\\:flex:is(:where(.group):last-of-type *), .peer-last-of-type\\:flex:is(:where(.peer):last-of-type ~ *), .last-of-type\\:flex:last-of-type { display: flex; - } - " + }" `) expect(await run(['last-of-type/foo:flex'])).toEqual('') }) @@ -265,11 +228,9 @@ test('last-of-type', async () => { test('only-of-type', async () => { expect(await run(['only-of-type:flex', 'group-only-of-type:flex', 'peer-only-of-type:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):only-of-type .group-only-of-type\\:flex), :is(:where(.peer):only-of-type ~ .peer-only-of-type\\:flex), .only-of-type\\:flex:only-of-type { + ".group-only-of-type\\:flex:is(:where(.group):only-of-type *), .peer-only-of-type\\:flex:is(:where(.peer):only-of-type ~ *), .only-of-type\\:flex:only-of-type { display: flex; - } - " + }" `) expect(await run(['only-of-type/foo:flex'])).toEqual('') }) @@ -277,11 +238,9 @@ test('only-of-type', async () => { test('visited', async () => { expect(await run(['visited:flex', 'group-visited:flex', 'peer-visited:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):visited .group-visited\\:flex), :is(:where(.peer):visited ~ .peer-visited\\:flex), .visited\\:flex:visited { + ".group-visited\\:flex:is(:where(.group):visited *), .peer-visited\\:flex:is(:where(.peer):visited ~ *), .visited\\:flex:visited { display: flex; - } - " + }" `) expect(await run(['visited/foo:flex'])).toEqual('') }) @@ -289,11 +248,9 @@ test('visited', async () => { test('target', async () => { expect(await run(['target:flex', 'group-target:flex', 'peer-target:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):target .group-target\\:flex), :is(:where(.peer):target ~ .peer-target\\:flex), .target\\:flex:target { + ".group-target\\:flex:is(:where(.group):target *), .peer-target\\:flex:is(:where(.peer):target ~ *), .target\\:flex:target { display: flex; - } - " + }" `) expect(await run(['target/foo:flex'])).toEqual('') }) @@ -301,11 +258,9 @@ test('target', async () => { test('open', async () => { expect(await run(['open:flex', 'group-open:flex', 'peer-open:flex', 'not-open:flex'])) .toMatchInlineSnapshot(` - " - .not-open\\:flex:not(:is([open], :popover-open, :open)), :is(:where(.group):is([open], :popover-open, :open) .group-open\\:flex), :is(:where(.peer):is([open], :popover-open, :open) ~ .peer-open\\:flex), .open\\:flex:is([open], :popover-open, :open) { + ".not-open\\:flex:not(:is([open], :popover-open, :open)), .group-open\\:flex:is(:where(.group):is([open], :popover-open, :open) *), .peer-open\\:flex:is(:where(.peer):is([open], :popover-open, :open) ~ *), .open\\:flex:is([open], :popover-open, :open) { display: flex; - } - " + }" `) expect(await run(['open/foo:flex'])).toEqual('') }) @@ -313,11 +268,9 @@ test('open', async () => { test('default', async () => { expect(await run(['default:flex', 'group-default:flex', 'peer-default:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):default .group-default\\:flex), :is(:where(.peer):default ~ .peer-default\\:flex), .default\\:flex:default { + ".group-default\\:flex:is(:where(.group):default *), .peer-default\\:flex:is(:where(.peer):default ~ *), .default\\:flex:default { display: flex; - } - " + }" `) expect(await run(['default/foo:flex'])).toEqual('') }) @@ -325,11 +278,9 @@ test('default', async () => { test('checked', async () => { expect(await run(['checked:flex', 'group-checked:flex', 'peer-checked:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):checked .group-checked\\:flex), :is(:where(.peer):checked ~ .peer-checked\\:flex), .checked\\:flex:checked { + ".group-checked\\:flex:is(:where(.group):checked *), .peer-checked\\:flex:is(:where(.peer):checked ~ *), .checked\\:flex:checked { display: flex; - } - " + }" `) expect(await run(['checked/foo:flex'])).toEqual('') }) @@ -337,11 +288,9 @@ test('checked', async () => { test('indeterminate', async () => { expect(await run(['indeterminate:flex', 'group-indeterminate:flex', 'peer-indeterminate:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):indeterminate .group-indeterminate\\:flex), :is(:where(.peer):indeterminate ~ .peer-indeterminate\\:flex), .indeterminate\\:flex:indeterminate { + ".group-indeterminate\\:flex:is(:where(.group):indeterminate *), .peer-indeterminate\\:flex:is(:where(.peer):indeterminate ~ *), .indeterminate\\:flex:indeterminate { display: flex; - } - " + }" `) expect(await run(['indeterminate/foo:flex'])).toEqual('') }) @@ -354,11 +303,9 @@ test('placeholder-shown', async () => { 'peer-placeholder-shown:flex', ]), ).toMatchInlineSnapshot(` - " - :is(:where(.group):placeholder-shown .group-placeholder-shown\\:flex), :is(:where(.peer):placeholder-shown ~ .peer-placeholder-shown\\:flex), .placeholder-shown\\:flex:placeholder-shown { + ".group-placeholder-shown\\:flex:is(:where(.group):placeholder-shown *), .peer-placeholder-shown\\:flex:is(:where(.peer):placeholder-shown ~ *), .placeholder-shown\\:flex:placeholder-shown { display: flex; - } - " + }" `) expect(await run(['placeholder-shown/foo:flex'])).toEqual('') }) @@ -366,15 +313,9 @@ test('placeholder-shown', async () => { test('autofill', async () => { expect(await run(['autofill:flex', 'group-autofill:flex', 'peer-autofill:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):autofill .group-autofill\\:flex), :is(:where(.peer):autofill ~ .peer-autofill\\:flex) { + ".group-autofill\\:flex:is(:where(.group):autofill *), .peer-autofill\\:flex:is(:where(.peer):autofill ~ *), .autofill\\:flex:autofill { display: flex; - } - - .autofill\\:flex:autofill { - display: flex; - } - " + }" `) expect(await run(['autofill/foo:flex'])).toEqual('') }) @@ -382,11 +323,9 @@ test('autofill', async () => { test('optional', async () => { expect(await run(['optional:flex', 'group-optional:flex', 'peer-optional:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):optional .group-optional\\:flex), :is(:where(.peer):optional ~ .peer-optional\\:flex), .optional\\:flex:optional { + ".group-optional\\:flex:is(:where(.group):optional *), .peer-optional\\:flex:is(:where(.peer):optional ~ *), .optional\\:flex:optional { display: flex; - } - " + }" `) expect(await run(['optional/foo:flex'])).toEqual('') }) @@ -394,22 +333,18 @@ test('optional', async () => { test('required', async () => { expect(await run(['required:flex', 'group-required:flex', 'peer-required:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):required .group-required\\:flex), :is(:where(.peer):required ~ .peer-required\\:flex), .required\\:flex:required { + ".group-required\\:flex:is(:where(.group):required *), .peer-required\\:flex:is(:where(.peer):required ~ *), .required\\:flex:required { display: flex; - } - " + }" `) expect(await run(['required/foo:flex'])).toEqual('') }) test('valid', async () => { expect(await run(['valid:flex', 'group-valid:flex', 'peer-valid:flex'])).toMatchInlineSnapshot(` - " - :is(:where(.group):valid .group-valid\\:flex), :is(:where(.peer):valid ~ .peer-valid\\:flex), .valid\\:flex:valid { + ".group-valid\\:flex:is(:where(.group):valid *), .peer-valid\\:flex:is(:where(.peer):valid ~ *), .valid\\:flex:valid { display: flex; - } - " + }" `) expect(await run(['valid/foo:flex'])).toEqual('') }) @@ -417,11 +352,9 @@ test('valid', async () => { test('invalid', async () => { expect(await run(['invalid:flex', 'group-invalid:flex', 'peer-invalid:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):invalid .group-invalid\\:flex), :is(:where(.peer):invalid ~ .peer-invalid\\:flex), .invalid\\:flex:invalid { + ".group-invalid\\:flex:is(:where(.group):invalid *), .peer-invalid\\:flex:is(:where(.peer):invalid ~ *), .invalid\\:flex:invalid { display: flex; - } - " + }" `) expect(await run(['invalid/foo:flex'])).toEqual('') }) @@ -429,15 +362,13 @@ test('invalid', async () => { test('user-valid', async () => { expect(await run(['user-valid:flex', 'group-user-valid:flex', 'peer-user-valid:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):user-valid .group-user-valid\\:flex), :is(:where(.peer):user-valid ~ .peer-user-valid\\:flex) { + ".group-user-valid\\:flex:is(:where(.group):user-valid *), .peer-user-valid\\:flex:is(:where(.peer):user-valid ~ *) { display: flex; } .user-valid\\:flex:user-valid { display: flex; - } - " + }" `) expect(await run(['user-valid/foo:flex'])).toEqual('') }) @@ -445,27 +376,23 @@ test('user-valid', async () => { test('user-invalid', async () => { expect(await run(['user-invalid:flex', 'group-user-invalid:flex', 'peer-user-invalid:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):user-invalid .group-user-invalid\\:flex), :is(:where(.peer):user-invalid ~ .peer-user-invalid\\:flex) { + ".group-user-invalid\\:flex:is(:where(.group):user-invalid *), .peer-user-invalid\\:flex:is(:where(.peer):user-invalid ~ *) { display: flex; } .user-invalid\\:flex:user-invalid { display: flex; - } - " + }" `) - expect(await run(['user-invalid/foo:flex'])).toEqual('') + expect(await run(['invalid/foo:flex'])).toEqual('') }) test('in-range', async () => { expect(await run(['in-range:flex', 'group-in-range:flex', 'peer-in-range:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):in-range .group-in-range\\:flex), :is(:where(.peer):in-range ~ .peer-in-range\\:flex), .in-range\\:flex:in-range { + ".group-in-range\\:flex:is(:where(.group):in-range *), .peer-in-range\\:flex:is(:where(.peer):in-range ~ *), .in-range\\:flex:in-range { display: flex; - } - " + }" `) expect(await run(['in-range/foo:flex'])).toEqual('') }) @@ -473,11 +400,9 @@ test('in-range', async () => { test('out-of-range', async () => { expect(await run(['out-of-range:flex', 'group-out-of-range:flex', 'peer-out-of-range:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):out-of-range .group-out-of-range\\:flex), :is(:where(.peer):out-of-range ~ .peer-out-of-range\\:flex), .out-of-range\\:flex:out-of-range { + ".group-out-of-range\\:flex:is(:where(.group):out-of-range *), .peer-out-of-range\\:flex:is(:where(.peer):out-of-range ~ *), .out-of-range\\:flex:out-of-range { display: flex; - } - " + }" `) expect(await run(['out-of-range/foo:flex'])).toEqual('') }) @@ -485,22 +410,18 @@ test('out-of-range', async () => { test('read-only', async () => { expect(await run(['read-only:flex', 'group-read-only:flex', 'peer-read-only:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):read-only .group-read-only\\:flex), :is(:where(.peer):read-only ~ .peer-read-only\\:flex), .read-only\\:flex:read-only { + ".group-read-only\\:flex:is(:where(.group):read-only *), .peer-read-only\\:flex:is(:where(.peer):read-only ~ *), .read-only\\:flex:read-only { display: flex; - } - " + }" `) expect(await run(['read-only/foo:flex'])).toEqual('') }) test('empty', async () => { expect(await run(['empty:flex', 'group-empty:flex', 'peer-empty:flex'])).toMatchInlineSnapshot(` - " - :is(:where(.group):empty .group-empty\\:flex), :is(:where(.peer):empty ~ .peer-empty\\:flex), .empty\\:flex:empty { + ".group-empty\\:flex:is(:where(.group):empty *), .peer-empty\\:flex:is(:where(.peer):empty ~ *), .empty\\:flex:empty { display: flex; - } - " + }" `) expect(await run(['empty/foo:flex'])).toEqual('') }) @@ -508,89 +429,64 @@ test('empty', async () => { test('focus-within', async () => { expect(await run(['focus-within:flex', 'group-focus-within:flex', 'peer-focus-within:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):focus-within .group-focus-within\\:flex), :is(:where(.peer):focus-within ~ .peer-focus-within\\:flex), .focus-within\\:flex:focus-within { + ".group-focus-within\\:flex:is(:where(.group):focus-within *), .peer-focus-within\\:flex:is(:where(.peer):focus-within ~ *), .focus-within\\:flex:focus-within { display: flex; - } - " + }" `) expect(await run(['focus-within/foo:flex'])).toEqual('') }) test('hover', async () => { expect(await run(['hover:flex', 'group-hover:flex', 'peer-hover:flex'])).toMatchInlineSnapshot(` - " - @media (hover: hover) { - :is(:where(.group):hover .group-hover\\:flex), :is(:where(.peer):hover ~ .peer-hover\\:flex), .hover\\:flex:hover { + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *), .peer-hover\\:flex:is(:where(.peer):hover ~ *), .hover\\:flex:hover { display: flex; } - } - " + }" `) expect(await run(['hover/foo:flex'])).toEqual('') }) test('focus', async () => { expect(await run(['focus:flex', 'group-focus:flex', 'peer-focus:flex'])).toMatchInlineSnapshot(` - " - :is(:where(.group):focus .group-focus\\:flex), :is(:where(.peer):focus ~ .peer-focus\\:flex), .focus\\:flex:focus { + ".group-focus\\:flex:is(:where(.group):focus *), .peer-focus\\:flex:is(:where(.peer):focus ~ *), .focus\\:flex:focus { display: flex; - } - " + }" `) expect(await run(['focus/foo:flex'])).toEqual('') }) test('group-hover group-focus sorting', async () => { expect(await run(['group-hover:flex', 'group-focus:flex'])).toMatchInlineSnapshot(` - " - @media (hover: hover) { - :is(:where(.group):hover .group-hover\\:flex) { + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { display: flex; } } - :is(:where(.group):focus .group-focus\\:flex) { + .group-focus\\:flex:is(:where(.group):focus *) { display: flex; - } - " + }" `) expect(await run(['group-focus:flex', 'group-hover:flex'])).toMatchInlineSnapshot(` - " - @media (hover: hover) { - :is(:where(.group):hover .group-hover\\:flex) { + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { display: flex; } } - :is(:where(.group):focus .group-focus\\:flex) { + .group-focus\\:flex:is(:where(.group):focus *) { display: flex; - } - " + }" `) }) -test('stacking group and peer variants does not duplicate the target selector', async () => { - let candidate = Array.from( - { length: 12 }, - (_, i) => `${i % 2 === 0 ? 'group' : 'peer'}-data-active/name${i}`, - ).join(':') - - let output = await run([`${candidate}:flex`]) - expect(output).toContain('display: flex') - // Repeating `&` in each variant makes the generated selector grow - // exponentially. This many variants should still produce a small rule. - expect(output.length).toBeLessThan(2_000) -}) - test('focus-visible', async () => { expect(await run(['focus-visible:flex', 'group-focus-visible:flex', 'peer-focus-visible:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):focus-visible .group-focus-visible\\:flex), :is(:where(.peer):focus-visible ~ .peer-focus-visible\\:flex), .focus-visible\\:flex:focus-visible { + ".group-focus-visible\\:flex:is(:where(.group):focus-visible *), .peer-focus-visible\\:flex:is(:where(.peer):focus-visible ~ *), .focus-visible\\:flex:focus-visible { display: flex; - } - " + }" `) expect(await run(['focus-visible/foo:flex'])).toEqual('') }) @@ -598,11 +494,9 @@ test('focus-visible', async () => { test('active', async () => { expect(await run(['active:flex', 'group-active:flex', 'peer-active:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):active .group-active\\:flex), :is(:where(.peer):active ~ .peer-active\\:flex), .active\\:flex:active { + ".group-active\\:flex:is(:where(.group):active *), .peer-active\\:flex:is(:where(.peer):active ~ *), .active\\:flex:active { display: flex; - } - " + }" `) expect(await run(['active/foo:flex'])).toEqual('') }) @@ -610,11 +504,9 @@ test('active', async () => { test('enabled', async () => { expect(await run(['enabled:flex', 'group-enabled:flex', 'peer-enabled:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):enabled .group-enabled\\:flex), :is(:where(.peer):enabled ~ .peer-enabled\\:flex), .enabled\\:flex:enabled { + ".group-enabled\\:flex:is(:where(.group):enabled *), .peer-enabled\\:flex:is(:where(.peer):enabled ~ *), .enabled\\:flex:enabled { display: flex; - } - " + }" `) expect(await run(['enabled/foo:flex'])).toEqual('') }) @@ -622,22 +514,18 @@ test('enabled', async () => { test('disabled', async () => { expect(await run(['disabled:flex', 'group-disabled:flex', 'peer-disabled:flex'])) .toMatchInlineSnapshot(` - " - :is(:where(.group):disabled .group-disabled\\:flex), :is(:where(.peer):disabled ~ .peer-disabled\\:flex), .disabled\\:flex:disabled { + ".group-disabled\\:flex:is(:where(.group):disabled *), .peer-disabled\\:flex:is(:where(.peer):disabled ~ *), .disabled\\:flex:disabled { display: flex; - } - " + }" `) expect(await run(['disabled/foo:flex'])).toEqual('') }) test('inert', async () => { expect(await run(['inert:flex', 'group-inert:flex', 'peer-inert:flex'])).toMatchInlineSnapshot(` - " - :is(:where(.group):is([inert], [inert] *) .group-inert\\:flex), :is(:where(.peer):is([inert], [inert] *) ~ .peer-inert\\:flex), .inert\\:flex:is([inert], [inert] *) { + ".group-inert\\:flex:is(:where(.group):is([inert], [inert] *) *), .peer-inert\\:flex:is(:where(.peer):is([inert], [inert] *) ~ *), .inert\\:flex:is([inert], [inert] *) { display: flex; - } - " + }" `) expect(await run(['inert/foo:flex'])).toEqual('') }) @@ -652,46 +540,36 @@ test('group-[...]', async () => { 'group-[&:hover]:group-[&_p]:flex', ]), ).toMatchInlineSnapshot(` - " - :is(:where(.group) p .group-\\[\\&_p\\]\\:flex), :is(:where(.group) p :is(:where(.group):hover .group-\\[\\&\\:hover\\]\\:group-\\[\\&_p\\]\\:flex)) { + ".group-\\[\\&_p\\]\\:flex:is(:where(.group) p *), .group-\\[\\&\\:hover\\]\\:group-\\[\\&_p\\]\\:flex:is(:where(.group):hover *):is(:where(.group) p *) { display: flex; } @media (hover: hover) { - :is(:where(.group) p .group-\\[\\&_p\\]\\:hover\\:flex):hover, :is(:where(.group) p .hover\\:group-\\[\\&_p\\]\\:flex:hover) { + .group-\\[\\&_p\\]\\:hover\\:flex:is(:where(.group) p *):hover, .hover\\:group-\\[\\&_p\\]\\:flex:hover:is(:where(.group) p *) { display: flex; } @media (hover: hover) { - :is(:where(.group) p .hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover):hover { + .hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover:is(:where(.group) p *):hover { display: flex; } } - } - " + }" `) expect( - await run([ - 'group-[]:flex', - 'group-hover/[]:flex', - 'group-[@media_foo]:flex', - 'group-[>img]:flex', - ]), + await compileCss( + css` + @tailwind utilities; + `, + ['group-[]:flex', 'group-hover/[]:flex', 'group-[@media_foo]:flex', 'group-[>img]:flex'], + ), ).toEqual('') }) test('group-*', async () => { expect( - await run( - [ - 'group-hover:flex', - 'group-focus:flex', - 'group-hocus:flex', - - 'group-hover:group-focus:flex', - 'group-focus:group-hover:flex', - ], + await compileCss( css` @custom-variant hocus { &:hover, @@ -701,34 +579,39 @@ test('group-*', async () => { } @tailwind utilities; `, + [ + 'group-hover:flex', + 'group-focus:flex', + 'group-hocus:flex', + + 'group-hover:group-focus:flex', + 'group-focus:group-hover:flex', + ], ), ).toMatchInlineSnapshot(` - " - @media (hover: hover) { - :is(:where(.group):hover .group-hover\\:flex) { + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { display: flex; } } - :is(:where(.group):focus .group-focus\\:flex) { + .group-focus\\:flex:is(:where(.group):focus *) { display: flex; } @media (hover: hover) { - :is(:where(.group):hover :is(:where(.group):focus .group-focus\\:group-hover\\:flex)), :is(:where(.group):focus :is(:where(.group):hover .group-hover\\:group-focus\\:flex)) { + .group-focus\\:group-hover\\:flex:is(:where(.group):focus *):is(:where(.group):hover *), .group-hover\\:group-focus\\:flex:is(:where(.group):hover *):is(:where(.group):focus *) { display: flex; } } - :is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) { + .group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) { display: flex; - } - " + }" `) expect( - await run( - ['group-custom-at-rule:flex', 'group-nested-selectors:flex'], + await compileCss( css` @custom-variant custom-at-rule (@media foo); @custom-variant nested-selectors { @@ -740,6 +623,7 @@ test('group-*', async () => { } @tailwind utilities; `, + ['group-custom-at-rule:flex', 'group-nested-selectors:flex'], ), ).toEqual('') }) @@ -754,34 +638,30 @@ test('peer-[...]', async () => { 'peer-[&:hover]:peer-[&_p]:flex', ]), ).toMatchInlineSnapshot(` - " - :is(:where(.peer) p ~ .peer-\\[\\&_p\\]\\:flex), :is(:where(.peer) p ~ :is(:where(.peer):hover ~ .peer-\\[\\&\\:hover\\]\\:peer-\\[\\&_p\\]\\:flex)) { + ".peer-\\[\\&_p\\]\\:flex:is(:where(.peer) p ~ *), .peer-\\[\\&\\:hover\\]\\:peer-\\[\\&_p\\]\\:flex:is(:where(.peer):hover ~ *):is(:where(.peer) p ~ *) { display: flex; } @media (hover: hover) { - :is(:where(.peer) p ~ .hover\\:peer-\\[\\&_p\\]\\:flex:hover), :is(:where(.peer) p ~ .peer-\\[\\&_p\\]\\:hover\\:flex):hover, :is(:where(.peer) p ~ .hover\\:peer-\\[\\&_p\\]\\:focus\\:flex:hover):focus { + .hover\\:peer-\\[\\&_p\\]\\:flex:hover:is(:where(.peer) p ~ *), .peer-\\[\\&_p\\]\\:hover\\:flex:is(:where(.peer) p ~ *):hover, .hover\\:peer-\\[\\&_p\\]\\:focus\\:flex:hover:is(:where(.peer) p ~ *):focus { display: flex; } - } - " + }" `) expect( - await run(['peer-[]:flex', 'peer-hover/[]:flex', 'peer-[@media_foo]:flex', 'peer-[>img]:flex']), + await compileCss( + css` + @tailwind utilities; + `, + ['peer-[]:flex', 'peer-hover/[]:flex', 'peer-[@media_foo]:flex', 'peer-[>img]:flex'], + ), ).toEqual('') }) test('peer-*', async () => { expect( - await run( - [ - 'peer-hover:flex', - 'peer-focus:flex', - 'peer-hocus:flex', - 'peer-hover:peer-focus:flex', - 'peer-focus:peer-hover:flex', - ], + await compileCss( css` @custom-variant hocus { &:hover, @@ -791,34 +671,38 @@ test('peer-*', async () => { } @tailwind utilities; `, + [ + 'peer-hover:flex', + 'peer-focus:flex', + 'peer-hocus:flex', + 'peer-hover:peer-focus:flex', + 'peer-focus:peer-hover:flex', + ], ), ).toMatchInlineSnapshot(` - " - @media (hover: hover) { - :is(:where(.peer):hover ~ .peer-hover\\:flex) { + "@media (hover: hover) { + .peer-hover\\:flex:is(:where(.peer):hover ~ *) { display: flex; } } - :is(:where(.peer):focus ~ .peer-focus\\:flex) { + .peer-focus\\:flex:is(:where(.peer):focus ~ *) { display: flex; } @media (hover: hover) { - :is(:where(.peer):hover ~ :is(:where(.peer):focus ~ .peer-focus\\:peer-hover\\:flex)), :is(:where(.peer):focus ~ :is(:where(.peer):hover ~ .peer-hover\\:peer-focus\\:flex)) { + .peer-focus\\:peer-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.peer):hover ~ *), .peer-hover\\:peer-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.peer):focus ~ *) { display: flex; } } - :is(:is(:where(.peer):hover, :where(.peer):focus) ~ .peer-hocus\\:flex) { + .peer-hocus\\:flex:is(:is(:where(.peer):hover, :where(.peer):focus) ~ *) { display: flex; - } - " + }" `) expect( - await run( - ['peer-custom-at-rule:flex', 'peer-nested-selectors:flex'], + await compileCss( css` @custom-variant custom-at-rule (@media foo); @custom-variant nested-selectors { @@ -830,101 +714,87 @@ test('peer-*', async () => { } @tailwind utilities; `, + ['peer-custom-at-rule:flex', 'peer-nested-selectors:flex'], ), ).toEqual('') }) test('ltr', async () => { expect(await run(['ltr:flex'])).toMatchInlineSnapshot(` - " - .ltr\\:flex:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *) { + ".ltr\\:flex:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *) { display: flex; - } - " + }" `) expect(await run(['ltr/foo:flex'])).toEqual('') }) test('rtl', async () => { expect(await run(['rtl:flex'])).toMatchInlineSnapshot(` - " - .rtl\\:flex:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) { + ".rtl\\:flex:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) { display: flex; - } - " + }" `) expect(await run(['rtl/foo:flex'])).toEqual('') }) test('motion-safe', async () => { expect(await run(['motion-safe:flex'])).toMatchInlineSnapshot(` - " - @media (prefers-reduced-motion: no-preference) { + "@media (prefers-reduced-motion: no-preference) { .motion-safe\\:flex { display: flex; } - } - " + }" `) expect(await run(['motion-safe/foo:flex'])).toEqual('') }) test('motion-reduce', async () => { expect(await run(['motion-reduce:flex'])).toMatchInlineSnapshot(` - " - @media (prefers-reduced-motion: reduce) { + "@media (prefers-reduced-motion: reduce) { .motion-reduce\\:flex { display: flex; } - } - " + }" `) expect(await run(['motion-reduce/foo:flex'])).toEqual('') }) test('dark', async () => { expect(await run(['dark:flex'])).toMatchInlineSnapshot(` - " - @media (prefers-color-scheme: dark) { + "@media (prefers-color-scheme: dark) { .dark\\:flex { display: flex; } - } - " + }" `) expect(await run(['dark/foo:flex'])).toEqual('') }) test('starting', async () => { expect(await run(['starting:opacity-0'])).toMatchInlineSnapshot(` - " - @starting-style { + "@starting-style { .starting\\:opacity-0 { opacity: 0; } - } - " + }" `) expect(await run(['starting/foo:flex'])).toEqual('') }) test('print', async () => { expect(await run(['print:flex'])).toMatchInlineSnapshot(` - " - @media print { + "@media print { .print\\:flex { display: flex; } - } - " + }" `) expect(await run(['print/foo:flex'])).toEqual('') }) test('default breakpoints', async () => { expect( - await run( - ['sm:flex', 'md:flex', 'lg:flex', 'xl:flex', '2xl:flex'], + await compileCss( css` @theme { /* Breakpoints */ @@ -936,10 +806,10 @@ test('default breakpoints', async () => { } @tailwind utilities; `, + ['sm:flex', 'md:flex', 'lg:flex', 'xl:flex', '2xl:flex'], ), ).toMatchInlineSnapshot(` - " - @media (min-width: 640px) { + "@media (min-width: 640px) { .sm\\:flex { display: flex; } @@ -967,12 +837,10 @@ test('default breakpoints', async () => { .\\32 xl\\:flex { display: flex; } - } - " + }" `) expect( - await run( - ['sm/foo:flex', 'md/foo:flex', 'lg/foo:flex', 'xl/foo:flex', '2xl/foo:flex'], + await compileCss( css` @theme reference { /* Breakpoints */ @@ -984,36 +852,34 @@ test('default breakpoints', async () => { } @tailwind utilities; `, + ['sm/foo:flex', 'md/foo:flex', 'lg/foo:flex', 'xl/foo:flex', '2xl/foo:flex'], ), ).toEqual('') }) test('custom breakpoint', async () => { expect( - await run( - ['10xl:flex'], + await compileCss( css` @theme { --breakpoint-10xl: 5000px; } @tailwind utilities; `, + ['10xl:flex'], ), ).toMatchInlineSnapshot(` - " - @media (min-width: 5000px) { + "@media (min-width: 5000px) { .\\31 0xl\\:flex { display: flex; } - } - " + }" `) }) test('max-*', async () => { expect( - await run( - ['max-lg:flex', 'max-sm:flex', 'max-md:flex'], + await compileCss( css` @theme { /* Explicitly ordered in a strange way */ @@ -1023,10 +889,10 @@ test('max-*', async () => { } @tailwind utilities; `, + ['max-lg:flex', 'max-sm:flex', 'max-md:flex'], ), ).toMatchInlineSnapshot(` - " - @media not all and (min-width: 1024px) { + "@media not all and (min-width: 1024px) { .max-lg\\:flex { display: flex; } @@ -1042,12 +908,10 @@ test('max-*', async () => { .max-sm\\:flex { display: flex; } - } - " + }" `) expect( - await run( - ['max-lg/foo:flex', 'max-sm/foo:flex', 'max-md/foo:flex'], + await compileCss( css` @theme reference { /* Explicitly ordered in a strange way */ @@ -1057,14 +921,14 @@ test('max-*', async () => { } @tailwind utilities; `, + ['max-lg/foo:flex', 'max-sm/foo:flex', 'max-md/foo:flex'], ), ).toEqual('') }) test('min-*', async () => { expect( - await run( - ['min-lg:flex', 'min-sm:flex', 'min-md:flex'], + await compileCss( css` @theme { /* Explicitly ordered in a strange way */ @@ -1074,10 +938,10 @@ test('min-*', async () => { } @tailwind utilities; `, + ['min-lg:flex', 'min-sm:flex', 'min-md:flex'], ), ).toMatchInlineSnapshot(` - " - @media (min-width: 640px) { + "@media (min-width: 640px) { .min-sm\\:flex { display: flex; } @@ -1093,12 +957,10 @@ test('min-*', async () => { .min-lg\\:flex { display: flex; } - } - " + }" `) expect( - await run( - ['min-lg/foo:flex', 'min-sm/foo:flex', 'min-md/foo:flex'], + await compileCss( css` @theme reference { /* Explicitly ordered in a strange way */ @@ -1108,14 +970,14 @@ test('min-*', async () => { } @tailwind utilities; `, + ['min-lg/foo:flex', 'min-sm/foo:flex', 'min-md/foo:flex'], ), ).toEqual('') }) test('sorting stacked min-* and max-* variants', async () => { expect( - await run( - ['min-sm:max-lg:flex', 'min-sm:max-xl:flex', 'min-md:max-lg:flex', 'min-xs:max-sm:flex'], + await compileCss( css` @theme { /* Explicitly ordered in a strange way */ @@ -1127,10 +989,10 @@ test('sorting stacked min-* and max-* variants', async () => { } @tailwind utilities; `, + ['min-sm:max-lg:flex', 'min-sm:max-xl:flex', 'min-md:max-lg:flex', 'min-xs:max-sm:flex'], ), ).toMatchInlineSnapshot(` - " - @media (min-width: 280px) { + "@media (min-width: 280px) { @media not all and (min-width: 640px) { .min-xs\\:max-sm\\:flex { display: flex; @@ -1158,15 +1020,13 @@ test('sorting stacked min-* and max-* variants', async () => { display: flex; } } - } - " + }" `) }) test('stacked min-* and max-* variants should come after unprefixed variants', async () => { expect( - await run( - ['sm:flex', 'min-sm:max-lg:flex', 'md:flex', 'min-md:max-lg:flex'], + await compileCss( css` @theme { /* Explicitly ordered in a strange way */ @@ -1176,10 +1036,10 @@ test('stacked min-* and max-* variants should come after unprefixed variants', a } @tailwind utilities; `, + ['sm:flex', 'min-sm:max-lg:flex', 'md:flex', 'min-md:max-lg:flex'], ), ).toMatchInlineSnapshot(` - " - @media (min-width: 640px) { + "@media (min-width: 640px) { .sm\\:flex { display: flex; } @@ -1201,14 +1061,22 @@ test('stacked min-* and max-* variants should come after unprefixed variants', a display: flex; } } - } - " + }" `) }) test('min, max and unprefixed breakpoints', async () => { expect( - await run( + await compileCss( + css` + @theme { + /* Explicitly ordered in a strange way */ + --breakpoint-sm: 640px; + --breakpoint-lg: 1024px; + --breakpoint-md: 768px; + } + @tailwind utilities; + `, [ 'max-lg-sm-potato:flex', 'min-lg-sm-potato:flex', @@ -1225,19 +1093,9 @@ test('min, max and unprefixed breakpoints', async () => { 'sm:flex', 'lg:flex', ], - css` - @theme { - /* Explicitly ordered in a strange way */ - --breakpoint-sm: 640px; - --breakpoint-lg: 1024px; - --breakpoint-md: 768px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @media not all and (min-width: 1024px) { + "@media not all and (min-width: 1024px) { .max-lg\\:flex { display: flex; } @@ -1283,8 +1141,7 @@ test('min, max and unprefixed breakpoints', async () => { .lg\\:flex, .min-lg\\:flex { display: flex; } - } - " + }" `) }) @@ -1313,8 +1170,7 @@ test('sorting `min` and `max` should sort by unit, then by value, then alphabeti 'max-[12vh]:flex', ]), ).toMatchInlineSnapshot(` - " - @media not all and (min-width: calc(1000px + 12em)) { + "@media not all and (min-width: calc(1000px + 12em)) { .max-\\[calc\\(1000px\\+12em\\)\\]\\:flex { display: flex; } @@ -1432,82 +1288,7 @@ test('sorting `min` and `max` should sort by unit, then by value, then alphabeti .min-\\[12vh\\]\\:flex { display: flex; } - } - " - `) -}) - -test('sorting `min` and `max` should compare decimal values numerically', async () => { - expect( - await run([ - 'min-[40.5rem]:flex', - 'min-[40.25rem]:flex', - 'max-[40.25rem]:flex', - 'max-[40.5rem]:flex', - ]), - ).toMatchInlineSnapshot(` - " - @media not all and (min-width: 40.5rem) { - .max-\\[40\\.5rem\\]\\:flex { - display: flex; - } - } - - @media not all and (min-width: 40.25rem) { - .max-\\[40\\.25rem\\]\\:flex { - display: flex; - } - } - - @media (min-width: 40.25rem) { - .min-\\[40\\.25rem\\]\\:flex { - display: flex; - } - } - - @media (min-width: 40.5rem) { - .min-\\[40\\.5rem\\]\\:flex { - display: flex; - } - } - " - `) -}) - -test('sorting `@min` and `@max` should compare decimal values numerically', async () => { - expect( - await run([ - '@min-[40.5rem]:flex', - '@min-[40.25rem]:flex', - '@max-[40.25rem]:flex', - '@max-[40.5rem]:flex', - ]), - ).toMatchInlineSnapshot(` - " - @container not (min-width: 40.5rem) { - .\\@max-\\[40\\.5rem\\]\\:flex { - display: flex; - } - } - - @container not (min-width: 40.25rem) { - .\\@max-\\[40\\.25rem\\]\\:flex { - display: flex; - } - } - - @container (min-width: 40.25rem) { - .\\@min-\\[40\\.25rem\\]\\:flex { - display: flex; - } - } - - @container (min-width: 40.5rem) { - .\\@min-\\[40\\.5rem\\]\\:flex { - display: flex; - } - } - " + }" `) }) @@ -1522,44 +1303,20 @@ test('supports', async () => { 'supports-[font-tech(color-COLRv1)]:flex', 'supports-[var(--test)]:flex', 'supports-[--test]:flex', - - // Only top-level and/or/not should have spaces around them. We should - // ignore and/or/not inside of `selector(…)` - 'supports-[selector(a:not(.foo))]:flex', - 'supports-[selector(a:is(.and,.or))]:flex', - 'supports-[(display:grid)or(display:flex)]:grid', - 'supports-[not(display:grid)]:flex', - 'supports-[foo-not(display:grid)]:flex', - 'supports-[selector([data-foo="("])or(display:grid)]:flex', - 'supports-[selector(.foo\\(bar)or(display:grid)]:flex', - 'supports-[((display:grid)or(display:flex))]:grid', ]), ).toMatchInlineSnapshot(` - " - @supports (gap: var(--tw)) { + "@supports (gap: var(--tw)) { .supports-gap\\:grid { display: grid; } } - @supports (display: grid) or (display: flex) { - .supports-\\[\\(\\(display\\:grid\\)or\\(display\\:flex\\)\\)\\]\\:grid { - display: grid; - } - } - @supports (display: grid) and font-format(opentype) { .supports-\\[\\(display\\:grid\\)_and_font-format\\(opentype\\)\\]\\:grid { display: grid; } } - @supports (display: grid) or (display: flex) { - .supports-\\[\\(display\\:grid\\)or\\(display\\:flex\\)\\]\\:grid { - display: grid; - } - } - @supports (--test: var(--tw)) { .supports-\\[--test\\]\\:flex { display: flex; @@ -1584,54 +1341,17 @@ test('supports', async () => { } } - @supports foo-not(display:grid) { - .supports-\\[foo-not\\(display\\:grid\\)\\]\\:flex { - display: flex; - } - } - - @supports not (display: grid) { - .supports-\\[not\\(display\\:grid\\)\\]\\:flex { - display: flex; - } - } - - @supports selector(.foo\\(bar) or (display: grid) { - .supports-\\[selector\\(\\.foo\\\\\\(bar\\)or\\(display\\:grid\\)\\]\\:flex { - display: flex; - } - } - @supports selector(A > B) { .supports-\\[selector\\(A_\\>_B\\)\\]\\:flex { display: flex; } } - @supports selector([data-foo="("]) or (display: grid) { - .supports-\\[selector\\(\\[data-foo\\=\\"\\(\\"\\]\\)or\\(display\\:grid\\)\\]\\:flex { - display: flex; - } - } - - @supports selector(a:is(.and,.or)) { - .supports-\\[selector\\(a\\:is\\(\\.and\\,\\.or\\)\\)\\]\\:flex { - display: flex; - } - } - - @supports selector(a:not(.foo)) { - .supports-\\[selector\\(a\\:not\\(\\.foo\\)\\)\\]\\:flex { - display: flex; - } - } - @supports var(--test) { .supports-\\[var\\(--test\\)\\]\\:flex { display: flex; } - } - " + }" `) expect( await run([ @@ -1648,7 +1368,30 @@ test('supports', async () => { test('not', async () => { expect( - await run( + await compileCss( + css` + @custom-variant hocus { + &:hover, + &:focus { + @slot; + } + } + + @custom-variant device-hocus { + @media (hover: hover) { + &:hover, + &:focus { + @slot; + } + } + } + + @theme { + --breakpoint-sm: 640px; + } + + @tailwind utilities; + `, [ 'not-[:checked]:flex', 'not-[@media_print]:flex', @@ -1730,41 +1473,9 @@ test('not', async () => { 'not-max-sm:flex', 'not-max-[130px]:flex', ], - css` - @custom-variant hocus { - &:hover, - &:focus { - @slot; - } - } - - @custom-variant device-hocus { - @media (hover: hover) { - &:hover, - &:focus { - @slot; - } - } - } - - @theme { - --breakpoint-sm: 640px; - } - - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - .not-first\\:flex:not(:first-child), .not-last\\:flex:not(:last-child), .not-only\\:flex:not(:only-child), .not-odd\\:flex:not(:nth-child(odd)), .not-even\\:flex:not(:nth-child(2n)), .not-first-of-type\\:flex:not(:first-of-type), .not-last-of-type\\:flex:not(:last-of-type), .not-only-of-type\\:flex:not(:only-of-type), .not-visited\\:flex:not(:visited), .not-target\\:flex:not(:target), .not-open\\:flex:not(:is([open], :popover-open, :open)), .not-default\\:flex:not(:default), .not-checked\\:flex:not(:checked), .not-indeterminate\\:flex:not(:indeterminate), .not-placeholder-shown\\:flex:not(:placeholder-shown) { - display: flex; - } - - .not-autofill\\:flex:not(:autofill) { - display: flex; - } - - .not-optional\\:flex:not(:optional), .not-required\\:flex:not(:required), .not-valid\\:flex:not(:valid), .not-invalid\\:flex:not(:invalid), .not-in-range\\:flex:not(:in-range), .not-out-of-range\\:flex:not(:out-of-range), .not-read-only\\:flex:not(:read-only), .not-empty\\:flex:not(:empty), .not-focus-within\\:flex:not(:focus-within), .not-hover\\:flex:not(:hover) { + ".not-first\\:flex:not(:first-child), .not-last\\:flex:not(:last-child), .not-only\\:flex:not(:only-child), .not-odd\\:flex:not(:nth-child(odd)), .not-even\\:flex:not(:nth-child(2n)), .not-first-of-type\\:flex:not(:first-of-type), .not-last-of-type\\:flex:not(:last-of-type), .not-only-of-type\\:flex:not(:only-of-type), .not-visited\\:flex:not(:visited), .not-target\\:flex:not(:target), .not-open\\:flex:not(:is([open], :popover-open, :open)), .not-default\\:flex:not(:default), .not-checked\\:flex:not(:checked), .not-indeterminate\\:flex:not(:indeterminate), .not-placeholder-shown\\:flex:not(:placeholder-shown), .not-autofill\\:flex:not(:autofill), .not-optional\\:flex:not(:optional), .not-required\\:flex:not(:required), .not-valid\\:flex:not(:valid), .not-invalid\\:flex:not(:invalid), .not-in-range\\:flex:not(:in-range), .not-out-of-range\\:flex:not(:out-of-range), .not-read-only\\:flex:not(:read-only), .not-empty\\:flex:not(:empty), .not-focus-within\\:flex:not(:focus-within), .not-hover\\:flex:not(:hover) { display: flex; } @@ -1910,116 +1621,13 @@ test('not', async () => { } } - :is(:where(.group):not(:checked) .group-not-checked\\:flex), :is(:where(.group):not(:hover, :focus) .group-not-hocus\\:flex), :is(:where(.group\\/parent-name):not(:hover, :focus) .group-not-hocus\\/parent-name\\:flex), :is(:where(.group):not(:checked) .group-not-\\[\\:checked\\]\\:flex), :is(:where(.group\\/parent-name):not(:checked) .group-not-\\[\\:checked\\]\\/parent-name\\:flex), :is(:where(.peer):not(:checked) ~ .peer-not-checked\\:flex), :is(:where(.peer):not(:hover, :focus) ~ .peer-not-hocus\\:flex), :is(:where(.peer\\/sibling-name):not(:hover, :focus) ~ .peer-not-hocus\\/sibling-name\\:flex), :is(:where(.peer):not(:checked) ~ .peer-not-\\[\\:checked\\]\\:flex), :is(:where(.peer\\/sibling-name):not(:checked) ~ .peer-not-\\[\\:checked\\]\\/sibling-name\\:flex) { + .group-not-checked\\:flex:is(:where(.group):not(:checked) *), .group-not-hocus\\:flex:is(:where(.group):not(:hover, :focus) *), .group-not-hocus\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:hover, :focus) *), .group-not-\\[\\:checked\\]\\:flex:is(:where(.group):not(:checked) *), .group-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:checked) *), .peer-not-checked\\:flex:is(:where(.peer):not(:checked) ~ *), .peer-not-hocus\\:flex:is(:where(.peer):not(:hover, :focus) ~ *), .peer-not-hocus\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):not(:hover, :focus) ~ *), .peer-not-\\[\\:checked\\]\\:flex:is(:where(.peer):not(:checked) ~ *), .peer-not-\\[\\:checked\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):not(:checked) ~ *) { display: flex; - } - " - `) - - // https://github.com/tailwindlabs/tailwindcss/issues/20058 - expect( - await run( - ['not-has-a:flex', 'not-has-b:flex', 'not-has-c:flex', 'not-has-d:flex'], - css` - @custom-variant has-a { - @container style(--a) { - @slot; - } - } - - /* Already negated case */ - @custom-variant has-b { - @container not style(--b) { - @slot; - } - } - - /* Named @container */ - @custom-variant has-c { - @container foo style(--c) { - @slot; - } - } - - /* Named @container, that's already negated case */ - @custom-variant has-d { - @container bar not style(--d) { - @slot; - } - } - - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - @container not style(--a) { - .not-has-a\\:flex { - display: flex; - } - } - - @container style(--b) { - .not-has-b\\:flex { - display: flex; - } - } - - @container foo not style(--c) { - .not-has-c\\:flex { - display: flex; - } - } - - @container bar style(--d) { - .not-has-d\\:flex { - display: flex; - } - } - " + }" `) expect( - await run( - [ - 'not-[>img]:flex', - 'not-[+img]:flex', - 'not-[~img]:flex', - 'not-[:checked]/foo:flex', - 'not-[@media_screen,print]:flex', - 'not-[@media_not_screen,print]:flex', - 'not-[@media_not_screen,not_print]:flex', - - 'not-nested-at-rules:flex', - 'not-nested-style-rules:flex', - 'not-multiple-media-conditions:flex', - 'not-starting:flex', - - 'not-parallel-style-rules:flex', - 'not-parallel-at-rules:flex', - 'not-parallel-mixed-rules:flex', - - // The following built-in variants don't have not-* versions because - // there is no sensible negative version of them. - - // These just don't make sense as not-* - 'not-force:flex', - 'not-*:flex', - - // These contain pseudo-elements - 'not-first-letter:flex', - 'not-first-line:flex', - 'not-marker:flex', - 'not-selection:flex', - 'not-file:flex', - 'not-placeholder:flex', - 'not-backdrop:flex', - 'not-before:flex', - 'not-after:flex', - - // This is not a conditional at rule - 'not-starting:flex', - ], + await compileCss( css` @custom-variant nested-at-rules { @media foo { @@ -2066,6 +1674,45 @@ test('not', async () => { } @tailwind utilities; `, + [ + 'not-[>img]:flex', + 'not-[+img]:flex', + 'not-[~img]:flex', + 'not-[:checked]/foo:flex', + 'not-[@media_screen,print]:flex', + 'not-[@media_not_screen,print]:flex', + 'not-[@media_not_screen,not_print]:flex', + + 'not-nested-at-rules:flex', + 'not-nested-style-rules:flex', + 'not-multiple-media-conditions:flex', + 'not-starting:flex', + + 'not-parallel-style-rules:flex', + 'not-parallel-at-rules:flex', + 'not-parallel-mixed-rules:flex', + + // The following built-in variants don't have not-* versions because + // there is no sensible negative version of them. + + // These just don't make sense as not-* + 'not-force:flex', + 'not-*:flex', + + // These contain pseudo-elements + 'not-first-letter:flex', + 'not-first-line:flex', + 'not-marker:flex', + 'not-selection:flex', + 'not-file:flex', + 'not-placeholder:flex', + 'not-backdrop:flex', + 'not-before:flex', + 'not-after:flex', + + // This is not a conditional at rule + 'not-starting:flex', + ], ), ).toEqual('') }) @@ -2080,18 +1727,25 @@ test('in', async () => { 'in-data-visible:flex', ]), ).toMatchInlineSnapshot(` - " - .not-in-\\[\\.group\\]\\:flex:not(:where(.group) *), .not-in-\\[p\\]\\:flex:not(:where(:is(p)) *), :where([data-visible]) .in-data-visible\\:flex, :where(.group) .in-\\[\\.group\\]\\:flex, :where(:is(p)) .in-\\[p\\]\\:flex { + ".not-in-\\[\\.group\\]\\:flex:not(:where(.group) *), .not-in-\\[p\\]\\:flex:not(:where(:is(p)) *), :where([data-visible]) .in-data-visible\\:flex, :where(.group) .in-\\[\\.group\\]\\:flex, :where(:is(p)) .in-\\[p\\]\\:flex { display: flex; - } - " + }" `) expect(await run(['in-p:flex', 'in-foo-bar:flex'])).toEqual('') }) test('has', async () => { expect( - await run( + await compileCss( + css` + @custom-variant hocus { + &:hover, + &:focus { + @slot; + } + } + @tailwind utilities; + `, [ 'has-checked:flex', 'has-[:checked]:flex', @@ -2127,32 +1781,15 @@ test('has', async () => { 'peer-has-hocus:flex', 'peer-has-hocus/sibling-name:flex', ], - css` - @custom-variant hocus { - &:hover, - &:focus { - @slot; - } - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - :is(:where(.group):has(:checked) .group-has-checked\\:flex), :is(:where(.group\\/parent-name):has(:checked) .group-has-checked\\/parent-name\\:flex), :is(:where(.group):has(:hover, :focus) .group-has-hocus\\:flex), :is(:where(.group\\/parent-name):has(:hover, :focus) .group-has-hocus\\/parent-name\\:flex), :is(:where(.group):has(:checked) .group-has-\\[\\:checked\\]\\:flex), :is(:where(.group\\/parent-name):has(:checked) .group-has-\\[\\:checked\\]\\/parent-name\\:flex), :is(:where(.group):has(* > img) .group-has-\\[\\&\\>img\\]\\:flex), :is(:where(.group\\/parent-name):has(* > img) .group-has-\\[\\&\\>img\\]\\/parent-name\\:flex), :is(:where(.group):has( + img) .group-has-\\[\\+img\\]\\:flex), :is(:where(.group):has( > img) .group-has-\\[\\>img\\]\\:flex), :is(:where(.group\\/parent-name):has( > img) .group-has-\\[\\>img\\]\\/parent-name\\:flex), :is(:where(.group):has( ~ img) .group-has-\\[\\~img\\]\\:flex), :is(:where(.peer):has(:checked) ~ .peer-has-checked\\:flex), :is(:where(.peer\\/sibling-name):has(:checked) ~ .peer-has-checked\\/sibling-name\\:flex), :is(:where(.peer):has(:hover, :focus) ~ .peer-has-hocus\\:flex), :is(:where(.peer\\/sibling-name):has(:hover, :focus) ~ .peer-has-hocus\\/sibling-name\\:flex), :is(:where(.peer):has(:checked) ~ .peer-has-\\[\\:checked\\]\\:flex), :is(:where(.peer\\/sibling-name):has(:checked) ~ .peer-has-\\[\\:checked\\]\\/sibling-name\\:flex), :is(:where(.peer):has(* > img) ~ .peer-has-\\[\\&\\>img\\]\\:flex), :is(:where(.peer\\/sibling-name):has(* > img) ~ .peer-has-\\[\\&\\>img\\]\\/sibling-name\\:flex), :is(:where(.peer):has( + img) ~ .peer-has-\\[\\+img\\]\\:flex), :is(:where(.peer):has( > img) ~ .peer-has-\\[\\>img\\]\\:flex), :is(:where(.peer\\/sibling-name):has( > img) ~ .peer-has-\\[\\>img\\]\\/sibling-name\\:flex), :is(:where(.peer):has( ~ img) ~ .peer-has-\\[\\~img\\]\\:flex), .has-checked\\:flex:has(:checked), .has-hocus\\:flex:has(:hover, :focus), .has-\\[\\:checked\\]\\:flex:has(:checked), .has-\\[\\&\\>img\\]\\:flex:has(* > img), .has-\\[\\+img\\]\\:flex:has( + img), .has-\\[\\>img\\]\\:flex:has( > img), .has-\\[\\~img\\]\\:flex:has( ~ img) { + ".group-has-checked\\:flex:is(:where(.group):has(:checked) *), .group-has-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *), .group-has-hocus\\:flex:is(:where(.group):has(:hover, :focus) *), .group-has-hocus\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:hover, :focus) *), .group-has-\\[\\:checked\\]\\:flex:is(:where(.group):has(:checked) *), .group-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *), .group-has-\\[\\&\\>img\\]\\:flex:is(:where(.group):has(* > img) *), .group-has-\\[\\&\\>img\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(* > img) *), .group-has-\\[\\+img\\]\\:flex:is(:where(.group):has( + img) *), .group-has-\\[\\>img\\]\\:flex:is(:where(.group):has( > img) *), .group-has-\\[\\>img\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has( > img) *), .group-has-\\[\\~img\\]\\:flex:is(:where(.group):has( ~ img) *), .peer-has-checked\\:flex:is(:where(.peer):has(:checked) ~ *), .peer-has-checked\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:checked) ~ *), .peer-has-hocus\\:flex:is(:where(.peer):has(:hover, :focus) ~ *), .peer-has-hocus\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:hover, :focus) ~ *), .peer-has-\\[\\:checked\\]\\:flex:is(:where(.peer):has(:checked) ~ *), .peer-has-\\[\\:checked\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:checked) ~ *), .peer-has-\\[\\&\\>img\\]\\:flex:is(:where(.peer):has(* > img) ~ *), .peer-has-\\[\\&\\>img\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(* > img) ~ *), .peer-has-\\[\\+img\\]\\:flex:is(:where(.peer):has( + img) ~ *), .peer-has-\\[\\>img\\]\\:flex:is(:where(.peer):has( > img) ~ *), .peer-has-\\[\\>img\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has( > img) ~ *), .peer-has-\\[\\~img\\]\\:flex:is(:where(.peer):has( ~ img) ~ *), .has-checked\\:flex:has(:checked), .has-hocus\\:flex:has(:hover, :focus), .has-\\[\\:checked\\]\\:flex:has(:checked), .has-\\[\\&\\>img\\]\\:flex:has(* > img), .has-\\[\\+img\\]\\:flex:has( + img), .has-\\[\\>img\\]\\:flex:has( > img), .has-\\[\\~img\\]\\:flex:has( ~ img) { display: flex; - } - " + }" `) expect( - await run( - [ - 'has-[:checked]/foo:flex', - 'has-[@media_print]:flex', - 'has-custom-at-rule:flex', - 'has-nested-selectors:flex', - ], + await compileCss( css` @custom-variant custom-at-rule (@media foo); @custom-variant nested-selectors { @@ -2164,6 +1801,12 @@ test('has', async () => { } @tailwind utilities; `, + [ + 'has-[:checked]/foo:flex', + 'has-[@media_print]:flex', + 'has-custom-at-rule:flex', + 'has-nested-selectors:flex', + ], ), ).toEqual('') }) @@ -2191,11 +1834,9 @@ test('aria', async () => { 'peer-aria-[valuenow=1]/sibling-name:flex', ]), ).toMatchInlineSnapshot(` - " - :is(:where(.group)[aria-checked="true"] .group-aria-checked\\:flex), :is(:where(.group\\/parent-name)[aria-checked="true"] .group-aria-checked\\/parent-name\\:flex), :is(:where(.group)[aria-modal] .group-aria-\\[modal\\]\\:flex), :is(:where(.group\\/parent-name)[aria-modal] .group-aria-\\[modal\\]\\/parent-name\\:flex), :is(:where(.group)[aria-valuenow="1"] .group-aria-\\[valuenow\\=1\\]\\:flex), :is(:where(.group\\/parent-name)[aria-valuenow="1"] .group-aria-\\[valuenow\\=1\\]\\/parent-name\\:flex), :is(:where(.peer)[aria-checked="true"] ~ .peer-aria-checked\\:flex), :is(:where(.peer\\/sibling-name)[aria-checked="true"] ~ .peer-aria-checked\\/sibling-name\\:flex), :is(:where(.peer)[aria-modal] ~ .peer-aria-\\[modal\\]\\:flex), :is(:where(.peer\\/sibling-name)[aria-modal] ~ .peer-aria-\\[modal\\]\\/sibling-name\\:flex), :is(:where(.peer)[aria-valuenow="1"] ~ .peer-aria-\\[valuenow\\=1\\]\\:flex), :is(:where(.peer\\/sibling-name)[aria-valuenow="1"] ~ .peer-aria-\\[valuenow\\=1\\]\\/sibling-name\\:flex), .aria-checked\\:flex[aria-checked="true"], .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"], .aria-\\[valuenow_\\=_\\"1\\"\\]\\:flex[aria-valuenow="1"], .aria-\\[valuenow\\=1\\]\\:flex[aria-valuenow="1"] { + ".group-aria-checked\\:flex:is(:where(.group)[aria-checked="true"] *), .group-aria-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-checked="true"] *), .group-aria-\\[modal\\]\\:flex:is(:where(.group)[aria-modal] *), .group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *), .group-aria-\\[valuenow\\=1\\]\\:flex:is(:where(.group)[aria-valuenow="1"] *), .group-aria-\\[valuenow\\=1\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-valuenow="1"] *), .peer-aria-checked\\:flex:is(:where(.peer)[aria-checked="true"] ~ *), .peer-aria-checked\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[aria-checked="true"] ~ *), .peer-aria-\\[modal\\]\\:flex:is(:where(.peer)[aria-modal] ~ *), .peer-aria-\\[modal\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[aria-modal] ~ *), .peer-aria-\\[valuenow\\=1\\]\\:flex:is(:where(.peer)[aria-valuenow="1"] ~ *), .peer-aria-\\[valuenow\\=1\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[aria-valuenow="1"] ~ *), .aria-checked\\:flex[aria-checked="true"], .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"], .aria-\\[valuenow_\\=_\\"1\\"\\]\\:flex[aria-valuenow="1"], .aria-\\[valuenow\\=1\\]\\:flex[aria-valuenow="1"] { display: flex; - } - " + }" `) expect(await run(['aria-checked/foo:flex', 'aria-[invalid=spelling]/foo:flex'])).toEqual('') }) @@ -2230,11 +1871,9 @@ test('data', async () => { 'peer-data-[foo$=bar_baz_i]/sibling-name:flex', ]), ).toMatchInlineSnapshot(` - " - :is(:where(.group)[data-disabled] .group-data-\\[disabled\\]\\:flex), :is(:where(.group\\/parent-name)[data-disabled] .group-data-\\[disabled\\]\\/parent-name\\:flex), :is(:where(.group\\/parent-name)[data-foo$="bar" i] .group-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/parent-name\\:flex), :is(:where(.group\\/parent-name)[data-foo$="bar baz" i] .group-data-\\[foo\\$\\=bar_baz_i\\]\\/parent-name\\:flex), :is(:where(.group)[data-foo="1"] .group-data-\\[foo\\=1\\]\\:flex), :is(:where(.group\\/parent-name)[data-foo="1"] .group-data-\\[foo\\=1\\]\\/parent-name\\:flex), :is(:where(.group\\/parent-name)[data-foo="bar baz"] .group-data-\\[foo\\=bar\\ baz\\]\\/parent-name\\:flex), :is(:where(.peer)[data-disabled] ~ .peer-data-\\[disabled\\]\\:flex), :is(:where(.peer\\/sibling-name)[data-disabled] ~ .peer-data-\\[disabled\\]\\/sibling-name\\:flex), :is(:where(.peer\\/sibling-name)[data-foo$="bar" i] ~ .peer-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/sibling-name\\:flex), :is(:where(.peer\\/sibling-name)[data-foo$="bar baz" i] ~ .peer-data-\\[foo\\$\\=bar_baz_i\\]\\/sibling-name\\:flex), :is(:where(.peer)[data-foo="1"] ~ .peer-data-\\[foo\\=1\\]\\:flex), :is(:where(.peer\\/sibling-name)[data-foo="1"] ~ .peer-data-\\[foo\\=1\\]\\/sibling-name\\:flex), :is(:where(.peer\\/sibling-name)[data-foo="bar baz"] ~ .peer-data-\\[foo\\=bar\\ baz\\]\\/sibling-name\\:flex), .data-disabled\\:flex[data-disabled], .data-\\[foo\\$\\=\\'bar\\'_i\\]\\:flex[data-foo$="bar" i], .data-\\[foo\\$\\=bar_baz_i\\]\\:flex[data-foo$="bar baz" i], .data-\\[foo\\=1\\]\\:flex[data-foo="1"], .data-\\[foo\\=bar_baz\\]\\:flex[data-foo="bar baz"], .data-\\[potato_\\=_\\"salad\\"\\]\\:flex[data-potato="salad"], .data-\\[potato_\\^\\=_\\"salad\\"\\]\\:flex[data-potato^="salad"], .data-\\[potato\\=\\"\\^_\\=\\"\\]\\:flex[data-potato="^ ="], .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] { + ".group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *), .group-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-disabled] *), .group-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo$="bar" i] *), .group-data-\\[foo\\$\\=bar_baz_i\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo$="bar baz" i] *), .group-data-\\[foo\\=1\\]\\:flex:is(:where(.group)[data-foo="1"] *), .group-data-\\[foo\\=1\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo="1"] *), .group-data-\\[foo\\=bar\\ baz\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo="bar baz"] *), .peer-data-\\[disabled\\]\\:flex:is(:where(.peer)[data-disabled] ~ *), .peer-data-\\[disabled\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-disabled] ~ *), .peer-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-foo$="bar" i] ~ *), .peer-data-\\[foo\\$\\=bar_baz_i\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-foo$="bar baz" i] ~ *), .peer-data-\\[foo\\=1\\]\\:flex:is(:where(.peer)[data-foo="1"] ~ *), .peer-data-\\[foo\\=1\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-foo="1"] ~ *), .peer-data-\\[foo\\=bar\\ baz\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-foo="bar baz"] ~ *), .data-disabled\\:flex[data-disabled], .data-\\[foo\\$\\=\\'bar\\'_i\\]\\:flex[data-foo$="bar" i], .data-\\[foo\\$\\=bar_baz_i\\]\\:flex[data-foo$="bar baz" i], .data-\\[foo\\=1\\]\\:flex[data-foo="1"], .data-\\[foo\\=bar_baz\\]\\:flex[data-foo="bar baz"], .data-\\[potato_\\=_\\"salad\\"\\]\\:flex[data-potato="salad"], .data-\\[potato_\\^\\=_\\"salad\\"\\]\\:flex[data-potato^="salad"], .data-\\[potato\\=\\"\\^_\\=\\"\\]\\:flex[data-potato="^ ="], .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] { display: flex; - } - " + }" `) expect( await run([ @@ -2244,175 +1883,140 @@ test('data', async () => { 'data-[potato=salad]/foo:flex', ]), ).toEqual('') - - // Non-ASCII code points are valid in attribute names - expect(await run(['data-[état]:flex', 'aria-[état]:flex'])).toMatchInlineSnapshot(` - " - .aria-\\[état\\]\\:flex[aria-état], .data-\\[état\\]\\:flex[data-état] { - display: flex; - } - " - `) }) test('portrait', async () => { expect(await run(['portrait:flex'])).toMatchInlineSnapshot(` - " - @media (orientation: portrait) { + "@media (orientation: portrait) { .portrait\\:flex { display: flex; } - } - " + }" `) expect(await run(['portrait/foo:flex'])).toEqual('') }) test('landscape', async () => { expect(await run(['landscape:flex'])).toMatchInlineSnapshot(` - " - @media (orientation: landscape) { + "@media (orientation: landscape) { .landscape\\:flex { display: flex; } - } - " + }" `) expect(await run(['landscape/foo:flex'])).toEqual('') }) test('contrast-more', async () => { expect(await run(['contrast-more:flex'])).toMatchInlineSnapshot(` - " - @media (prefers-contrast: more) { + "@media (prefers-contrast: more) { .contrast-more\\:flex { display: flex; } - } - " + }" `) expect(await run(['contrast-more/foo:flex'])).toEqual('') }) test('contrast-less', async () => { expect(await run(['contrast-less:flex'])).toMatchInlineSnapshot(` - " - @media (prefers-contrast: less) { + "@media (prefers-contrast: less) { .contrast-less\\:flex { display: flex; } - } - " + }" `) expect(await run(['contrast-less/foo:flex'])).toEqual('') }) test('forced-colors', async () => { expect(await run(['forced-colors:flex'])).toMatchInlineSnapshot(` - " - @media (forced-colors: active) { + "@media (forced-colors: active) { .forced-colors\\:flex { display: flex; } - } - " + }" `) expect(await run(['forced-colors/foo:flex'])).toEqual('') }) test('inverted-colors', async () => { expect(await run(['inverted-colors:flex'])).toMatchInlineSnapshot(` - " - @media (inverted-colors: inverted) { + "@media (inverted-colors: inverted) { .inverted-colors\\:flex { display: flex; } - } - " + }" `) }) test('pointer-none', async () => { expect(await run(['pointer-none:flex'])).toMatchInlineSnapshot(` - " - @media (pointer: none) { + "@media (pointer: none) { .pointer-none\\:flex { display: flex; } - } - " + }" `) }) test('pointer-coarse', async () => { expect(await run(['pointer-coarse:flex'])).toMatchInlineSnapshot(` - " - @media (pointer: coarse) { + "@media (pointer: coarse) { .pointer-coarse\\:flex { display: flex; } - } - " + }" `) }) test('pointer-fine', async () => { expect(await run(['pointer-fine:flex'])).toMatchInlineSnapshot(` - " - @media (pointer: fine) { + "@media (pointer: fine) { .pointer-fine\\:flex { display: flex; } - } - " + }" `) }) test('any-pointer-none', async () => { expect(await run(['any-pointer-none:flex'])).toMatchInlineSnapshot(` - " - @media (any-pointer: none) { + "@media (any-pointer: none) { .any-pointer-none\\:flex { display: flex; } - } - " + }" `) }) test('any-pointer-coarse', async () => { expect(await run(['any-pointer-coarse:flex'])).toMatchInlineSnapshot(` - " - @media (any-pointer: coarse) { + "@media (any-pointer: coarse) { .any-pointer-coarse\\:flex { display: flex; } - } - " + }" `) }) test('any-pointer-fine', async () => { expect(await run(['any-pointer-fine:flex'])).toMatchInlineSnapshot(` - " - @media (any-pointer: fine) { + "@media (any-pointer: fine) { .any-pointer-fine\\:flex { display: flex; } - } - " + }" `) }) test('scripting-none', async () => { expect(await run(['noscript:flex'])).toMatchInlineSnapshot(` - " - @media (scripting: none) { + "@media (scripting: none) { .noscript\\:flex { display: flex; } - } - " + }" `) }) @@ -2431,11 +2035,9 @@ test('nth', async () => { 'nth-last-of-type-[2n+1]:flex', ]), ).toMatchInlineSnapshot(` - " - .nth-3\\:flex:nth-child(3), .nth-\\[2n\\+1\\]\\:flex:nth-child(odd), .nth-\\[2n\\+1_of_\\.foo\\]\\:flex:nth-child(odd of .foo), .nth-last-3\\:flex:nth-last-child(3), .nth-last-\\[2n\\+1\\]\\:flex:nth-last-child(odd), .nth-last-\\[2n\\+1_of_\\.foo\\]\\:flex:nth-last-child(odd of .foo), .nth-of-type-3\\:flex:nth-of-type(3), .nth-of-type-\\[2n\\+1\\]\\:flex:nth-of-type(odd), .nth-last-of-type-3\\:flex:nth-last-of-type(3), .nth-last-of-type-\\[2n\\+1\\]\\:flex:nth-last-of-type(odd) { + ".nth-3\\:flex:nth-child(3), .nth-\\[2n\\+1\\]\\:flex:nth-child(odd), .nth-\\[2n\\+1_of_\\.foo\\]\\:flex:nth-child(odd of .foo), .nth-last-3\\:flex:nth-last-child(3), .nth-last-\\[2n\\+1\\]\\:flex:nth-last-child(odd), .nth-last-\\[2n\\+1_of_\\.foo\\]\\:flex:nth-last-child(odd of .foo), .nth-of-type-3\\:flex:nth-of-type(3), .nth-of-type-\\[2n\\+1\\]\\:flex:nth-of-type(odd), .nth-last-of-type-3\\:flex:nth-last-of-type(3), .nth-last-of-type-\\[2n\\+1\\]\\:flex:nth-last-of-type(odd) { display: flex; - } - " + }" `) expect( @@ -2467,16 +2069,15 @@ test('nth', async () => { }) test('container queries', async () => { - let input = css` - @theme { - --container-lg: 1024px; - --container-foo-bar: 1440px; - } - @tailwind utilities; - ` - expect( - await run( + await compileCss( + css` + @theme { + --container-lg: 1024px; + --container-foo-bar: 1440px; + } + @tailwind utilities; + `, [ '@lg:flex', '@lg/name:flex', @@ -2499,11 +2100,9 @@ test('container queries', async () => { '@max-foo-bar:flex', '@max-foo-bar/name:flex', ], - input, ), ).toMatchInlineSnapshot(` - " - @container name not (min-width: 1440px) { + "@container name not (min-width: 1440px) { .\\@max-foo-bar\\/name\\:flex { display: flex; } @@ -2597,11 +2196,17 @@ test('container queries', async () => { .\\@min-foo-bar\\:flex { display: flex; } - } - " + }" `) expect( - await run( + await compileCss( + css` + @theme { + --container-lg: 1024px; + --container-foo-bar: 1440px; + } + @tailwind utilities; + `, [ '@-lg:flex', '@-lg/name:flex', @@ -2624,14 +2229,23 @@ test('container queries', async () => { '@-max-foo-bar:flex', '@-max-foo-bar/name:flex', ], - input, ), ).toEqual('') }) test('variant order', async () => { expect( - await run( + await compileCss( + css` + @theme { + --breakpoint-sm: 640px; + --breakpoint-md: 768px; + --breakpoint-lg: 1024px; + --breakpoint-xl: 1280px; + --breakpoint-2xl: 1536px; + } + @tailwind utilities; + `, [ '[&_p]:flex', '2xl:flex', @@ -2708,20 +2322,9 @@ test('variant order', async () => { 'visited:flex', 'xl:flex', ], - css` - @theme { - --breakpoint-sm: 640px; - --breakpoint-md: 768px; - --breakpoint-lg: 1024px; - --breakpoint-xl: 1280px; - --breakpoint-2xl: 1536px; - } - @tailwind utilities; - `, ), ).toMatchInlineSnapshot(` - " - @layer properties { + "@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: ""; @@ -2730,7 +2333,7 @@ test('variant order', async () => { } @media (hover: hover) { - :is(:where(.group):hover .group-hover\\:flex), :is(:where(.peer):hover ~ .peer-hover\\:flex) { + .group-hover\\:flex:is(:where(.group):hover *), .peer-hover\\:flex:is(:where(.peer):hover ~ *) { display: flex; } } @@ -2780,15 +2383,7 @@ test('variant order', async () => { display: flex; } - .first\\:flex:first-child, .last\\:flex:last-child, .only\\:flex:only-child, .odd\\:flex:nth-child(odd), .even\\:flex:nth-child(2n), .first-of-type\\:flex:first-of-type, .last-of-type\\:flex:last-of-type, .only-of-type\\:flex:only-of-type, .visited\\:flex:visited, .target\\:flex:target, .open\\:flex:is([open], :popover-open, :open), .default\\:flex:default, .checked\\:flex:checked, .indeterminate\\:flex:indeterminate, .placeholder-shown\\:flex:placeholder-shown { - display: flex; - } - - .autofill\\:flex:autofill { - display: flex; - } - - .optional\\:flex:optional, .required\\:flex:required, .valid\\:flex:valid, .invalid\\:flex:invalid, .in-range\\:flex:in-range, .out-of-range\\:flex:out-of-range, .read-only\\:flex:read-only, .empty\\:flex:empty, .focus-within\\:flex:focus-within { + .first\\:flex:first-child, .last\\:flex:last-child, .only\\:flex:only-child, .odd\\:flex:nth-child(odd), .even\\:flex:nth-child(2n), .first-of-type\\:flex:first-of-type, .last-of-type\\:flex:last-of-type, .only-of-type\\:flex:only-of-type, .visited\\:flex:visited, .target\\:flex:target, .open\\:flex:is([open], :popover-open, :open), .default\\:flex:default, .checked\\:flex:checked, .indeterminate\\:flex:indeterminate, .placeholder-shown\\:flex:placeholder-shown, .autofill\\:flex:autofill, .optional\\:flex:optional, .required\\:flex:required, .valid\\:flex:valid, .invalid\\:flex:invalid, .in-range\\:flex:in-range, .out-of-range\\:flex:out-of-range, .read-only\\:flex:read-only, .empty\\:flex:empty, .focus-within\\:flex:focus-within { display: flex; } @@ -2904,8 +2499,7 @@ test('variant order', async () => { syntax: "*"; inherits: false; initial-value: ""; - } - " + }" `) }) @@ -2931,13 +2525,15 @@ test('variants with the same root are sorted deterministically', async () => { ]) for (let classList of classLists) { - expect(await run(classList)).toMatchInlineSnapshot(` - " - .data-active\\:flex[data-active], .data-focus\\:flex[data-focus], .data-hover\\:flex[data-hover], .data-\\[bar\\]\\:flex[data-bar], .data-\\[baz\\]\\:flex[data-baz], .data-\\[foo\\]\\:flex[data-foo] { - display: flex; - } - " - `) + let output = await compileCss('@tailwind utilities;', classList) + + expect(output.trim()).toEqual( + dedent(` + .data-active\\:flex[data-active], .data-focus\\:flex[data-focus], .data-hover\\:flex[data-hover], .data-\\[bar\\]\\:flex[data-bar], .data-\\[baz\\]\\:flex[data-baz], .data-\\[foo\\]\\:flex[data-foo] { + display: flex; + } + `), + ) } }) @@ -2962,31 +2558,31 @@ test('matchVariant sorts deterministically', async () => { ]) for (let classList of classLists) { - expect( - await run(classList, '@tailwind utilities; @plugin "./plugin.js";', { - async loadModule(_id: string) { - return { - path: '', - base: '/', - module: createPlugin(({ matchVariant }) => { - matchVariant('is-data', (value) => `&:is([data-${value}])`, { - values: { - DEFAULT: 'default', - foo: 'foo', - bar: 'bar', - }, - }) - }), - } - }, - }), - ).toMatchInlineSnapshot(` - " - .is-data\\:flex[data-default], .is-data-foo\\:flex[data-foo], .is-data-bar\\:flex[data-bar], .is-data-\\[potato\\]\\:flex[data-potato], .is-data-\\[sandwich\\]\\:flex[data-sandwich] { - display: flex; - } - " - `) + let output = await compileCss('@tailwind utilities; @plugin "./plugin.js";', classList, { + async loadModule(id: string) { + return { + path: '', + base: '/', + module: createPlugin(({ matchVariant }) => { + matchVariant('is-data', (value) => `&:is([data-${value}])`, { + values: { + DEFAULT: 'default', + foo: 'foo', + bar: 'bar', + }, + }) + }), + } + }, + }) + + expect(output.trim()).toEqual( + dedent(` + .is-data\\:flex[data-default], .is-data-foo\\:flex[data-foo], .is-data-bar\\:flex[data-bar], .is-data-\\[potato\\]\\:flex[data-potato], .is-data-\\[sandwich\\]\\:flex[data-sandwich] { + display: flex; + } + `), + ) } }) @@ -3002,11 +2598,9 @@ test('move modifier of compound variant to sub-variant if its also a compound va 'group-peer-focus/name:flex', ]), ).toMatchInlineSnapshot(` - " - .not-group-focus\\/name\\:flex:not(:is(:where(.group\\/name):focus *)), :is(:is(:where(.peer):focus ~ :where(.group\\/name)) .group-peer-focus\\/name\\:flex), :where(:is(:where(.group\\/name):focus *)) .in-group-focus\\/name\\:flex, .has-group-focus\\/name\\:flex:has(:is(:where(.group\\/name):focus *)) { + ".not-group-focus\\/name\\:flex:not(:is(:where(.group\\/name):focus *)), .group-peer-focus\\/name\\:flex:is(:where(.group\\/name):is(:where(.peer):focus ~ *) *), :where(:is(:where(.group\\/name):focus *)) .in-group-focus\\/name\\:flex, .has-group-focus\\/name\\:flex:has(:is(:where(.group\\/name):focus *)) { display: flex; - } - " + }" `) }) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index d2695b4d2..82a2b8592 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -3,7 +3,6 @@ import { atRoot, atRule, cloneAstNode, - context, decl, rule, styleRule, @@ -12,7 +11,6 @@ import { type Rule, type StyleRule, } from './ast' -import * as AttributeSelectorParser from './attribute-selector-parser' import { type Variant } from './candidate' import { applyVariant } from './compile' import type { DesignSystem } from './design-system' @@ -21,7 +19,6 @@ import { compareBreakpoints } from './utils/compare-breakpoints' import { DefaultMap } from './utils/default-map' import { isPositiveInteger } from './utils/infer-data-type' import { segment } from './utils/segment' -import * as ValueParser from './value-parser' import { walk, WalkAction } from './walk' export const IS_VALID_VARIANT_NAME = /^@?[a-z0-9][a-zA-Z0-9_-]*(? { if (node.kind === 'rule') { selectors.push(node.selector) } else if (node.kind === 'at-rule' && node.name === '@variant') { usesAtVariant = true } else if (node.kind === 'at-rule' && node.name !== '@slot') { - if (node.name === '@scope') usesAtScope = true selectors.push(`${node.name} ${node.params}`) } }) @@ -107,14 +102,8 @@ export class Variants { (r) => { let body = ast.map(cloneAstNode) if (usesAtVariant) substituteAtVariant(body, designSystem) - - if (usesAtScope) { - substituteAtSlot(body, [context({ source: 'user' }, r.nodes)]) - r.nodes = [context({ source: 'variant' }, body)] - } else { - substituteAtSlot(body, r.nodes) - r.nodes = body - } + substituteAtSlot(body, r.nodes) + r.nodes = body }, { compounds: compoundsForSelectors(selectors) }, ) @@ -386,83 +375,35 @@ export function createVariants(theme: Theme): Variants { function negateConditions(ruleName: string, conditions: string[]) { return conditions.map((condition) => { - switch (ruleName) { - case '@container': { - let ast = ValueParser.parse(condition.trim()) + condition = condition.trim() - // @container {query} - // ^^^^^^^ - // ast 0 - if (ast.length >= 1 && ast[0].kind === 'function') { - return `not ${condition}` - } + let parts = segment(condition, ' ') - // @container not {query} - // ^^^ ^ ^^^^^^^ - // ast 0 1 2 - else if ( - ast.length >= 3 && - ast[0].kind === 'word' && - ast[0].value === 'not' && - ast[2].kind === 'function' - ) { - // Drop the leading `not` (ast[0]) and separator (ast[1]) - ast.splice(0, 2) - - return ValueParser.toCss(ast) - } - - // @container {name} not {query} - // ^^^^^^ ^ ^^^ ^ ^^^^^^^ - // ast 0 1 2 3 4 - else if ( - ast.length >= 5 && - ast[0].kind === 'word' && - ast[2].kind === 'word' && - ast[2].value === 'not' && - ast[4].kind === 'function' - ) { - // Drop the `not` (ast[2]) and separator (ast[3]) - ast.splice(2, 2) - - return ValueParser.toCss(ast) - } - - // @container {name} {query} - // ^^^^^^ ^ ^^^^^^^ - // ast 0 1 2 - else if ( - ast.length >= 3 && - ast[0].kind === 'word' && - ast[0].value !== 'not' && - ast[2].kind === 'function' - ) { - // Inject a separator and a `not`, after the `name` (ast[0]) - ast.splice(1, 0, { kind: 'separator', value: ' ' }, { kind: 'word', value: 'not' }) - - return ValueParser.toCss(ast) - } - - // Fallback - else { - return `not ${condition}` - } - } - - default: { - condition = condition.trim() - - let parts = segment(condition, ' ') - - // @media not {query} - // @supports not {query} - if (parts[0] === 'not') { - return parts.slice(1).join(' ') - } + // @media not {query} + // @supports not {query} + // @container not {query} + if (parts[0] === 'not') { + return parts.slice(1).join(' ') + } + if (ruleName === '@container') { + // @container {query} + if (parts[0][0] === '(') { return `not ${condition}` } + + // @container {name} not {query} + else if (parts[1] === 'not') { + return `${parts[0]} ${parts.slice(2).join(' ')}` + } + + // @container {name} {query} + else { + return `${parts[0]} not ${parts.slice(1).join(' ')}` + } } + + return `not ${condition}` }) } @@ -610,13 +551,7 @@ export function createVariants(theme: Theme): Variants { selector = `:is(${selector})` } - // Put the target inside `:is` so browsers can limit style invalidation to - // it when the group changes. Keep `&` instead of the candidate's class so - // this also works with `@apply` and variants that change the target. - // - // The `:is` wrapper also keeps `has-group-*` from anchoring the group - // itself to the element carrying the utility. - node.selector = `:is(${selector} &)` + node.selector = `&:is(${selector} *)` // Track that the variant was actually applied didApply = true @@ -657,7 +592,7 @@ export function createVariants(theme: Theme): Variants { // For most variants we rely entirely on CSS nesting to build-up the final // selector, but there is no way to use CSS nesting to make `&` refer to - // just the `.peer` class the way we'd need to for these variants, so we + // just the `.group` class the way we'd need to for these variants, so we // need to replace it in the selector ourselves. let selector = node.selector.replaceAll('&', variantSelector) @@ -668,8 +603,7 @@ export function createVariants(theme: Theme): Variants { selector = `:is(${selector})` } - // As with `group`, constrain the target without repeating its selector. - node.selector = `:is(${selector} ~ &)` + node.selector = `&:is(${selector} ~ *)` // Track that the variant was actually applied didApply = true @@ -862,15 +796,11 @@ export function createVariants(theme: Theme): Variants { if (!variant.value || variant.modifier) return null if (variant.value.kind === 'arbitrary') { - let selector = `[aria-${quoteAttributeValue(variant.value.value)}]` - let parsed = AttributeSelectorParser.parse(selector) - if (parsed === null) return null - ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)] + ruleNode.nodes = [ + styleRule(`&[aria-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes), + ] } else { - let selector = `[aria-${variant.value.value}="true"]` - let parsed = AttributeSelectorParser.parse(selector) - if (parsed === null) return null - ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)] + ruleNode.nodes = [styleRule(`&[aria-${variant.value.value}="true"]`, ruleNode.nodes)] } }) @@ -889,11 +819,9 @@ export function createVariants(theme: Theme): Variants { variants.functional('data', (ruleNode, variant) => { if (!variant.value || variant.modifier) return null - let selector = `[data-${quoteAttributeValue(variant.value.value)}]` - let parsed = AttributeSelectorParser.parse(selector) - if (parsed === null) return null - - ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)] + ruleNode.nodes = [ + styleRule(`&[data-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes), + ] }) variants.functional('nth', (ruleNode, variant) => { @@ -945,22 +873,7 @@ export function createVariants(theme: Theme): Variants { if (/^[\w-]*\s*\(/.test(value)) { // Chrome has a bug where `(condition1)or(condition2)` is not valid, but // `(condition1) or (condition2)` is supported. - let changed = false - let ast = ValueParser.parse(value) - walk(ast, (node) => { - if (node.kind !== 'function') return - - // Leave selectors as-is, they could contain `selector(a:not(b))`, and - // in this case we don't want the space around the `not`. - if (node.value === 'selector') return WalkAction.Skip - - if (node.value === 'and' || node.value === 'or' || node.value === 'not') { - changed = true - node.value = ` ${node.value} ` - } - }) - - let query = changed ? ValueParser.toCss(ast) : value + let query = value.replace(/\b(and|or|not)\b/g, ' $1 ') ruleNode.nodes = [atRule('@supports', query, ruleNode.nodes)] return @@ -1283,7 +1196,7 @@ export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) { walk(ast, (node) => { // Replace `@slot` with rule nodes if (node.kind === 'at-rule' && node.name === '@slot') { - return WalkAction.ReplaceSkip(nodes) + return WalkAction.Replace(nodes) } // Wrap `@keyframes` and `@property` in `AtRoot` nodes @@ -1299,50 +1212,24 @@ export function substituteAtVariant(ast: AstNode[], designSystem: DesignSystem): walk(ast, (variantNode) => { if (variantNode.kind !== 'at-rule' || variantNode.name !== '@variant') return - let nodes: AstNode[] = [] - let compoundVariants = segment(variantNode.params, ',') - for (let [idx, compoundVariant] of compoundVariants.entries()) { - // Starting with the `&` rule node - // - // Only clone the nodes when we have multiple compound variants to deal - // with. The last one can use the original nodes. We do need unique AST - // nodes for sourcemap `dst` location information. - let node = styleRule( - '&', - idx === compoundVariants.length - 1 - ? variantNode.nodes - : variantNode.nodes.map(cloneAstNode), - ) + // Starting with the `&` rule node + let node = styleRule('&', variantNode.nodes) - let stackedVariants = segment(compoundVariant, ':') - for (let i = stackedVariants.length - 1; i >= 0; --i) { - let variant = stackedVariants[i].trim() + let variant = variantNode.params - if (!variant) { - throw new Error(`Cannot use \`@variant\` with empty variant`) - } + let variantAst = designSystem.parseVariant(variant) + if (variantAst === null) { + throw new Error(`Cannot use \`@variant\` with unknown variant: ${variant}`) + } - let variantAst = designSystem.parseVariant(variant) - if (variantAst === null) { - throw new Error(`Cannot use \`@variant\` with unknown variant: ${variant}`) - } - - let result = applyVariant(node, variantAst, designSystem.variants) - if (result === null) { - throw new Error(`Cannot use \`@variant\` with variant: ${variant}`) - } - } - - if (node.selector === '&') { - nodes.push(...node.nodes) - } else { - nodes.push(node) - } + let result = applyVariant(node, variantAst, designSystem.variants) + if (result === null) { + throw new Error(`Cannot use \`@variant\` with variant: ${variant}`) } // Update the variant at-rule node, to be the `&` rule node features |= Features.Variants - return WalkAction.Replace(nodes) + return WalkAction.Replace(node) }) return features } diff --git a/packages/tailwindcss/src/walk.test.ts b/packages/tailwindcss/src/walk.test.ts index 794d7e629..09720cc52 100644 --- a/packages/tailwindcss/src/walk.test.ts +++ b/packages/tailwindcss/src/walk.test.ts @@ -956,35 +956,33 @@ describe('AST Enter & Exit', () => { let visited: string[] = [] walk(ast, { enter(node, ctx) { - expect(ctx.index).toEqual(ctx.siblings.indexOf(node)) - visited.push(`${' '.repeat(ctx.depth)} Enter(${node.kind} @ ${ctx.index})`) + visited.push(`${' '.repeat(ctx.depth)} Enter(${node.kind})`) }, exit(node, ctx) { - expect(ctx.index).toEqual(ctx.siblings.indexOf(node)) - visited.push(`${' '.repeat(ctx.depth)} Exit(${node.kind} @ ${ctx.index})`) + visited.push(`${' '.repeat(ctx.depth)} Exit(${node.kind})`) }, }) expect(`\n${visited.join('\n')}\n`).toMatchInlineSnapshot(` " - Enter(a @ 0) - Enter(b @ 0) - Enter(c @ 0) - Exit(c @ 0) - Exit(b @ 0) - Enter(d @ 1) - Enter(e @ 0) - Enter(f @ 0) - Exit(f @ 0) - Exit(e @ 0) - Exit(d @ 1) - Enter(g @ 2) - Enter(h @ 0) - Exit(h @ 0) - Exit(g @ 2) - Exit(a @ 0) - Enter(i @ 1) - Exit(i @ 1) + Enter(a) + Enter(b) + Enter(c) + Exit(c) + Exit(b) + Enter(d) + Enter(e) + Enter(f) + Exit(f) + Exit(e) + Exit(d) + Enter(g) + Enter(h) + Exit(h) + Exit(g) + Exit(a) + Enter(i) + Exit(i) " `) }) diff --git a/packages/tailwindcss/src/walk.ts b/packages/tailwindcss/src/walk.ts index 2c268a7bd..ccb384256 100644 --- a/packages/tailwindcss/src/walk.ts +++ b/packages/tailwindcss/src/walk.ts @@ -36,8 +36,6 @@ type Parent = T & { nodes: T[] } export interface VisitContext { parent: Parent | null depth: number - index: number - siblings: T[] path: () => T[] } @@ -54,72 +52,52 @@ export function walk( else walkImplementation(ast, hooks.enter, hooks.exit) } -interface Stack { - value: T - prev: Stack | null -} - function walkImplementation( ast: T[], enter: (node: T, ctx: VisitContext) => EnterResult | void = () => WalkAction.Continue, exit: (node: T, ctx: VisitContext) => ExitResult | void = () => WalkAction.Continue, ) { - type StackFrame = [nodes: T[], offset: number, parent: Parent | null] - let stack: Stack | null = { value: [ast, 0, null], prev: null } - + let stack: [nodes: T[], offset: number, parent: Parent | null][] = [[ast, 0, null]] let ctx: VisitContext = { parent: null, depth: 0, - index: 0, - siblings: ast, path() { let path: T[] = [] - let frames: Stack | null = stack - - while (frames) { - let parent = frames.value[2] + for (let i = 1; i < stack.length; i++) { + let parent = stack[i][2] if (parent) path.push(parent) - frames = frames.prev } - path.reverse() - return path }, } - while (stack !== null) { - let frame = stack.value + while (stack.length > 0) { + let depth = stack.length - 1 + let frame = stack[depth] let nodes = frame[0] let offset = frame[1] let parent = frame[2] // Done with this level if (offset >= nodes.length) { - stack = stack.prev - ctx.depth -= 1 + stack.pop() continue } ctx.parent = parent - ctx.siblings = nodes + ctx.depth = depth // Enter phase (offsets are positive) if (offset >= 0) { - ctx.index = offset - let node = nodes[offset] let result = enter(node, ctx) ?? WalkAction.Continue switch (result.kind) { case WalkKind.Continue: { if (node.nodes && node.nodes.length > 0) { - ctx.depth += 1 - stack = { - value: [node.nodes, 0, node as Parent], - prev: stack, - } + stack.push([node.nodes, 0, node as Parent]) } frame[1] = ~offset // Prepare for exit phase, same offset @@ -162,7 +140,6 @@ function walkImplementation( // Exit phase for nodes[~offset] let index = ~offset // Two's complement to get original offset - ctx.index = index let node = nodes[index] let result = exit(node, ctx) ?? WalkAction.Continue diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index e5da091d8..0c3534efd 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -1911,7 +1911,7 @@ test('outline style is optional', async ({ page }) => { html`
`, ) - expect(await getPropertyValue('#x', 'outline')).toEqual('2px solid rgb(255, 255, 255)') + expect(await getPropertyValue('#x', 'outline')).toEqual('rgb(255, 255, 255) solid 2px') }) test('outline style is preserved when changing outline width', async ({ page }) => { @@ -1922,11 +1922,11 @@ test('outline style is preserved when changing outline width', async ({ page })
`, ) - expect(await getPropertyValue('#x', 'outline')).toEqual('2px dotted rgb(255, 255, 255)') + expect(await getPropertyValue('#x', 'outline')).toEqual('rgb(255, 255, 255) dotted 2px') await page.locator('#x').hover() - expect(await getPropertyValue('#x', 'outline')).toEqual('4px dotted rgb(255, 255, 255)') + expect(await getPropertyValue('#x', 'outline')).toEqual('rgb(255, 255, 255) dotted 4px') }) test('borders can be added without a border-style utility', async ({ page }) => { @@ -2177,330 +2177,6 @@ test('shadow DOM has access to variables', async ({ page }) => { expect(gap).toBe('8px') }) -// @scope related tests -{ - test('custom variant using `@scope`', async ({ page }) => { - let { getPropertyValue } = await render( - page, - html` -
-
In scope
-
-
Beyond the limit
-
-
-
Not in scope
- `, - css` - @custom-variant in-checkout { - @scope (.checkout) to (.payment) { - @slot; - } - } - `, - ) - - test.skip(!(await supportsAtScope(page)), '`@scope` is not supported in this browser') - - expect(await getPropertyValue('#in', 'text-decoration-line')).toEqual('underline') - expect(await getPropertyValue('#beyond', 'text-decoration-line')).toEqual('none') - expect(await getPropertyValue('#out', 'text-decoration-line')).toEqual('none') - }) - - test('custom variant using `@scope` behaves like the equivalent authored CSS', async ({ - page, - }) => { - // Render the same structure twice: - // - Once styled by authored CSS using `@scope`, like the "normal CSS" - // reference in https://github.com/tailwindlabs/tailwindcss/issues/18961 - // - Once styled by utilities using an equivalent custom variant - function structure(prefix: string, probe: string) { - return html` -
-
In scope
-
-
Beyond the limit
-
-
-
Not in scope
- ` - } - - let { getPropertyValue } = await render( - page, - structure('a', 'a-probe') + structure('c', 'in-checkout:underline'), - css` - @custom-variant in-checkout { - @scope (.checkout) to (.payment) { - @slot; - } - } - `, - ) - - // The `a-*` structure is styled by the authored CSS as-is - await page.addStyleTag({ - content: css` - @scope (.checkout) to (.payment) { - .a-probe { - text-decoration-line: underline; - } - } - `, - }) - - test.skip(!(await supportsAtScope(page)), '`@scope` is not supported in this browser') - - for (let prefix of ['a', 'c']) { - expect(await getPropertyValue(`#${prefix}-in`, 'text-decoration-line')).toEqual('underline') - expect(await getPropertyValue(`#${prefix}-beyond`, 'text-decoration-line')).toEqual('none') - expect(await getPropertyValue(`#${prefix}-out`, 'text-decoration-line')).toEqual('none') - } - }) - - test('user-authored nested `@scope` behaves like the authored CSS', async ({ page }) => { - // User-authored `@scope` rules are compiled to their native CSS nesting - // semantics. Rendering the same structure twice: - // - Once styled by the authored (nested) CSS as-is - // - Once styled by the compiled output - function structure(prefix: string) { - return html` -
-
- in scope -
- beyond the limit -
-
-
-
- scope not inside parent -
- ` - } - - function scopeCss(prefix: string) { - return css` - .${prefix}-parent { - @scope (& > .${prefix}-scope) to (& .${prefix}-limit) { - .${prefix}-content { - text-decoration-line: underline; - } - } - } - ` - } - - // The `c-*` structure is styled by the compiled CSS - let { getPropertyValue } = await render(page, structure('a') + structure('c'), scopeCss('c')) - - // The `a-*` structure is styled by the authored CSS as-is - await page.addStyleTag({ content: scopeCss('a') }) - - test.skip(!(await supportsAtScope(page)), '`@scope` is not supported in this browser') - - // The compiled output only applies within the scope - expect(await getPropertyValue('#c-in', 'text-decoration-line')).toEqual('underline') - expect(await getPropertyValue('#c-beyond', 'text-decoration-line')).toEqual('none') - expect(await getPropertyValue('#c-out', 'text-decoration-line')).toEqual('none') - - // The authored CSS behaves the same - expect(await getPropertyValue('#a-in', 'text-decoration-line')).toEqual('underline') - expect(await getPropertyValue('#a-out', 'text-decoration-line')).toEqual('none') - - // WebKit currently drops the `` limit entirely when it contains - // `&`. Since we resolve the `&` at build time, the compiled output works - // even though the authored CSS doesn't. - if (test.info().project.name !== 'webkit') { - expect(await getPropertyValue('#a-beyond', 'text-decoration-line')).toEqual('none') - } - }) - - test('user-authored `@scope` nested inside another `@scope`', async ({ page }) => { - function structure(prefix: string) { - return html` - -
-
-
ctrl
-
-
- - -
-
-
ancestor
-
-
- - -
-
-
-
beyond
-
-
-
- ` - } - - function scopeCss(prefix: string) { - return css` - @scope (.${prefix}-outer) to (.${prefix}-outer-limit) { - .${prefix}-middle { - @scope (.${prefix}-inner) to (.${prefix}-inner-limit) { - text-decoration-line: underline; - } - } - } - ` - } - - // The `c-*` structure is styled by the compiled CSS - let { getPropertyValue } = await render(page, structure('a') + structure('c'), scopeCss('c')) - - // The `a-*` structure is styled by the authored CSS as-is - await page.addStyleTag({ content: scopeCss('a') }) - - test.skip(!(await supportsAtScope(page)), '`@scope` is not supported in this browser') - - // The compiled output behaves the same in every browser - expect(await getPropertyValue('#c-ctrl', 'text-decoration-line')).toEqual('underline') - expect(await getPropertyValue('#c-ancestor', 'text-decoration-line')).toEqual('none') - expect(await getPropertyValue('#c-beyond', 'text-decoration-line')).toEqual('none') - - // The authored CSS behaves the same … - expect(await getPropertyValue('#a-ctrl', 'text-decoration-line')).toEqual('underline') - expect(await getPropertyValue('#a-beyond', 'text-decoration-line')).toEqual('none') - - // … except in Chromium, which resolves the nesting context of a - // rule-nested `@scope` without the enclosing scope's constraint (Safari - // and Firefox both constrain it). The compiled output normalizes this - // browser difference. - if (test.info().project.name !== 'chromium') { - expect(await getPropertyValue('#a-ancestor', 'text-decoration-line')).toEqual('none') - } - }) - - function supportsAtScope(page: Page) { - return page.evaluate(() => typeof CSSScopeRule !== 'undefined') - } -} - -for (let kind of ['group', 'peer']) { - test(`${kind}-* variants`, async ({ page }) => { - let targets = html` -
-
- ` - - let { getPropertyValue } = await render( - page, - kind === 'group' - ? html`
${targets}
` - : html`
-
- ${targets} -
`, - css` - .owner > .applied { - @apply ${kind}-focus:z-20; - } - `, - ) - - expect(await getPropertyValue('#target', 'z-index')).toEqual('auto') - expect(await getPropertyValue('#applied', 'z-index')).toEqual('auto') - - await page.locator('#trigger').focus() - - expect(await getPropertyValue('#target', 'z-index')).toEqual('10') - expect(await getPropertyValue('#applied', 'z-index')).toEqual('20') - - await page.locator('#trigger').blur() - - expect(await getPropertyValue('#target', 'z-index')).toEqual('auto') - expect(await getPropertyValue('#applied', 'z-index')).toEqual('auto') - }) -} - -test('group-* variants do not change specificity', async ({ page }) => { - let { getPropertyValue } = await render( - page, - html` -
-
-
- `, - css` - @layer utilities { - .target { - z-index: 20; - } - .target.equal { - z-index: 30; - } - } - `, - ) - - await page.locator('#trigger').focus() - - expect(await getPropertyValue('#target', 'z-index')).toEqual('10') - - await page.locator('#target').evaluate((element) => element.classList.add('equal')) - - expect(await getPropertyValue('#target', 'z-index')).toEqual('30') -}) - -test('stacked group-* variants match regardless of the order of the groups', async ({ page }) => { - for (let [outer, inner] of [ - ['a', 'b'], - ['b', 'a'], - ]) { - let { getPropertyValue } = await render( - page, - html` -
-
-
-
-
- `, - ) - - expect(await getPropertyValue('#target', 'z-index')).toEqual('10') - - await page.locator('#inner').evaluate((element) => element.removeAttribute('data-active')) - - expect(await getPropertyValue('#target', 'z-index')).toEqual('auto') - } -}) - -test('compound group-* and peer-* variants', async ({ page }) => { - let { getPropertyValue } = await render( - page, - html` -
-
-
-
-
-
- `, - ) - - await page.locator('#peer').focus() - - expect(await getPropertyValue('#target', 'z-index')).toEqual('10') - expect(await getPropertyValue('#middle', 'z-index')).toEqual('auto') - - await page.locator('#outer').focus() - - expect(await getPropertyValue('#target', 'z-index')).toEqual('auto') - expect(await getPropertyValue('#middle', 'z-index')).toEqual('20') -}) - // --- const preflight = fs.readFileSync(path.resolve(__dirname, '..', 'preflight.css'), 'utf-8') @@ -2573,7 +2249,7 @@ async function getPropertyValue( selector: [string, string | undefined], property: string, ) { - let value = await page.evaluate( + return page.evaluate( ([[selector, pseudo], property]) => { return window .getComputedStyle(document.querySelector(selector)!, pseudo) @@ -2581,16 +2257,4 @@ async function getPropertyValue( }, [selector, property] as const, ) - - switch (property) { - case 'outline': - // Order in webkit vs chromium is different. Sort to normalize. - return segment(value, ' ') - .map((part) => part.trim()) - .sort((a, z) => a.length - z.length) - .join(' ') - - default: - return value - } } diff --git a/packages/tailwindcss/theme.css b/packages/tailwindcss/theme.css index 05364f5c2..52d447f7f 100644 --- a/packages/tailwindcss/theme.css +++ b/packages/tailwindcss/theme.css @@ -1,7 +1,7 @@ @theme default { --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'; + ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', @@ -235,7 +235,7 @@ --color-gray-900: oklch(21% 0.034 264.665); --color-gray-950: oklch(13% 0.028 261.692); - --color-zinc-50: oklch(98.5% 0 none); + --color-zinc-50: oklch(98.5% 0 0); --color-zinc-100: oklch(96.7% 0.001 286.375); --color-zinc-200: oklch(92% 0.004 286.32); --color-zinc-300: oklch(87.1% 0.006 286.286); @@ -247,17 +247,17 @@ --color-zinc-900: oklch(21% 0.006 285.885); --color-zinc-950: oklch(14.1% 0.005 285.823); - --color-neutral-50: oklch(98.5% 0 none); - --color-neutral-100: oklch(97% 0 none); - --color-neutral-200: oklch(92.2% 0 none); - --color-neutral-300: oklch(87% 0 none); - --color-neutral-400: oklch(70.8% 0 none); - --color-neutral-500: oklch(55.6% 0 none); - --color-neutral-600: oklch(43.9% 0 none); - --color-neutral-700: oklch(37.1% 0 none); - --color-neutral-800: oklch(26.9% 0 none); - --color-neutral-900: oklch(20.5% 0 none); - --color-neutral-950: oklch(14.5% 0 none); + --color-neutral-50: oklch(98.5% 0 0); + --color-neutral-100: oklch(97% 0 0); + --color-neutral-200: oklch(92.2% 0 0); + --color-neutral-300: oklch(87% 0 0); + --color-neutral-400: oklch(70.8% 0 0); + --color-neutral-500: oklch(55.6% 0 0); + --color-neutral-600: oklch(43.9% 0 0); + --color-neutral-700: oklch(37.1% 0 0); + --color-neutral-800: oklch(26.9% 0 0); + --color-neutral-900: oklch(20.5% 0 0); + --color-neutral-950: oklch(14.5% 0 0); --color-stone-50: oklch(98.5% 0.001 106.423); --color-stone-100: oklch(97% 0.001 106.424); @@ -271,54 +271,6 @@ --color-stone-900: oklch(21.6% 0.006 56.043); --color-stone-950: oklch(14.7% 0.004 49.25); - --color-mauve-50: oklch(98.5% 0 none); - --color-mauve-100: oklch(96% 0.003 325.6); - --color-mauve-200: oklch(92.2% 0.005 325.62); - --color-mauve-300: oklch(86.5% 0.012 325.68); - --color-mauve-400: oklch(71.1% 0.019 323.02); - --color-mauve-500: oklch(54.2% 0.034 322.5); - --color-mauve-600: oklch(43.5% 0.029 321.78); - --color-mauve-700: oklch(36.4% 0.029 323.89); - --color-mauve-800: oklch(26.3% 0.024 320.12); - --color-mauve-900: oklch(21.2% 0.019 322.12); - --color-mauve-950: oklch(14.5% 0.008 326); - - --color-olive-50: oklch(98.8% 0.003 106.5); - --color-olive-100: oklch(96.6% 0.005 106.5); - --color-olive-200: oklch(93% 0.007 106.5); - --color-olive-300: oklch(88% 0.011 106.6); - --color-olive-400: oklch(73.7% 0.021 106.9); - --color-olive-500: oklch(58% 0.031 107.3); - --color-olive-600: oklch(46.6% 0.025 107.3); - --color-olive-700: oklch(39.4% 0.023 107.4); - --color-olive-800: oklch(28.6% 0.016 107.4); - --color-olive-900: oklch(22.8% 0.013 107.4); - --color-olive-950: oklch(15.3% 0.006 107.1); - - --color-mist-50: oklch(98.7% 0.002 197.1); - --color-mist-100: oklch(96.3% 0.002 197.1); - --color-mist-200: oklch(92.5% 0.005 214.3); - --color-mist-300: oklch(87.2% 0.007 219.6); - --color-mist-400: oklch(72.3% 0.014 214.4); - --color-mist-500: oklch(56% 0.021 213.5); - --color-mist-600: oklch(45% 0.017 213.2); - --color-mist-700: oklch(37.8% 0.015 216); - --color-mist-800: oklch(27.5% 0.011 216.9); - --color-mist-900: oklch(21.8% 0.008 223.9); - --color-mist-950: oklch(14.8% 0.004 228.8); - - --color-taupe-50: oklch(98.6% 0.002 67.8); - --color-taupe-100: oklch(96% 0.002 17.2); - --color-taupe-200: oklch(92.2% 0.005 34.3); - --color-taupe-300: oklch(86.8% 0.007 39.5); - --color-taupe-400: oklch(71.4% 0.014 41.2); - --color-taupe-500: oklch(54.7% 0.021 43.1); - --color-taupe-600: oklch(43.8% 0.017 39.3); - --color-taupe-700: oklch(36.7% 0.016 35.7); - --color-taupe-800: oklch(26.8% 0.011 36.5); - --color-taupe-900: oklch(21.4% 0.009 43.1); - --color-taupe-950: oklch(14.7% 0.004 49.3); - --color-black: #000; --color-white: #fff; diff --git a/packages/tailwindcss/tsconfig.json b/packages/tailwindcss/tsconfig.json index d2131cbeb..6ae022f65 100644 --- a/packages/tailwindcss/tsconfig.json +++ b/packages/tailwindcss/tsconfig.json @@ -1,6 +1,3 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { - "verbatimModuleSyntax": false, - }, } diff --git a/packages/tailwindcss/vitest.config.mts b/packages/tailwindcss/vitest.config.ts similarity index 100% rename from packages/tailwindcss/vitest.config.mts rename to packages/tailwindcss/vitest.config.ts diff --git a/patches/@napi-rs__cli@3.7.4.patch b/patches/@napi-rs__cli@3.7.4.patch deleted file mode 100644 index e72212499..000000000 --- a/patches/@napi-rs__cli@3.7.4.patch +++ /dev/null @@ -1,80 +0,0 @@ -diff --git a/dist/cli.js b/dist/cli.js -index c4c2d568bc070c07dbf3a1f9f4a0c946466668cf..353bbca86e3261a2ba9a8fc56dc612c7c84dab6d 100755 ---- a/dist/cli.js -+++ b/dist/cli.js -@@ -1,4 +1,13 @@ - #!/usr/bin/env node -+// PATCHED (see patches/@napi-rs__cli@3.7.4.patch): the embedded wasi loader -+// and worker templates below retry `new WASI(...)` with narrower preopens, -+// because preopening `/` throws `UVWASI_EACCES` on sandboxed platforms (e.g. -+// OpenHarmony), which would make the generated wasm binding fail to load. -+// -+// The same templates also exist in `dist/index.js` and `dist/index.cjs` (the -+// programmatic API). Those are intentionally NOT patched because we only -+// build through the `napi` bin, which runs this file. If we ever start using -+// the programmatic API, update the patch to cover those bundles too. - import { createRequire } from "node:module"; - import { Cli, Command, Option } from "clipanion"; - import path, { basename, dirname, isAbsolute, join, parse, resolve } from "node:path"; -@@ -1021,13 +1030,25 @@ const { - - const __rootDir = __nodePath.parse(process.cwd()).root - --const __wasi = new __nodeWASI({ -- version: 'preview1', -- env: process.env, -- preopens: { -- [__rootDir]: __rootDir, -+const __wasi = (() => { -+ // Preopening '/' fails with UVWASI_EACCES in sandboxed environments (e.g. -+ // OpenHarmony, Android), which would prevent the wasm binding from loading -+ // at all. Retry with narrower preopens instead. Without any preopens the -+ // binding still loads; only file system access is unavailable. -+ let lastError = null -+ for (const dir of [__rootDir, process.cwd(), null]) { -+ try { -+ return new __nodeWASI({ -+ version: 'preview1', -+ env: process.env, -+ preopens: dir === null ? {} : { [dir]: dir }, -+ }) -+ } catch (error) { -+ lastError = error -+ } - } --}) -+ throw lastError -+})() - - const __emnapiContext = __emnapiGetDefaultContext() - -@@ -1151,13 +1172,22 @@ const __rootDir = parse(process.cwd()).root; - - const handler = new MessageHandler({ - onLoad({ wasmModule, wasmMemory }) { -- const wasi = new WASI({ -- version: 'preview1', -- env: process.env, -- preopens: { -- [__rootDir]: __rootDir, -- }, -- }); -+ // Keep in sync with the preopen fallback in the main-thread loader. -+ const wasi = (() => { -+ let lastError = null; -+ for (const dir of [__rootDir, process.cwd(), null]) { -+ try { -+ return new WASI({ -+ version: 'preview1', -+ env: process.env, -+ preopens: dir === null ? {} : { [dir]: dir }, -+ }); -+ } catch (error) { -+ lastError = error; -+ } -+ } -+ throw lastError; -+ })(); - - return instantiateNapiModuleSync(wasmModule, { - childThread: true, diff --git a/patches/@parcel__watcher@2.6.0.patch b/patches/@parcel__watcher@2.5.1.patch similarity index 52% rename from patches/@parcel__watcher@2.6.0.patch rename to patches/@parcel__watcher@2.5.1.patch index 37ff83f04..ed9fa9040 100644 --- a/patches/@parcel__watcher@2.6.0.patch +++ b/patches/@parcel__watcher@2.5.1.patch @@ -1,22 +1,20 @@ diff --git a/index.js b/index.js -index 78ee744d74900c39a839cafa46b295258fc86307..f28fd092644ccfb8aff2618b966cb5219a181a02 100644 +index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2a9a311e3 100644 --- a/index.js +++ b/index.js -@@ -1,43 +1,32 @@ - const {createWrapper} = require('./wrapper'); +@@ -1,41 +1,34 @@ +-const {createWrapper} = require('./wrapper'); ++const { createWrapper } = require('./wrapper') -let name = `@parcel/watcher-${process.platform}-${process.arch}`; -if (process.platform === 'linux') { -- const {MUSL, familySync} = require('detect-libc'); -- const family = familySync(); +- const { MUSL, family } = require('detect-libc'); - if (family === MUSL) { - name += '-musl'; - } else { - name += '-glibc'; - } -} -+// We need to take the `process.env.PLATFORM_LIBC` into account, and we also -+// need static analysis, so we can't make use of the `parts.push(…)` approach. +function loadPackage() { + if (process.platform === 'linux') { + if (process.env.PLATFORM_LIBC === "musl") { @@ -24,8 +22,7 @@ index 78ee744d74900c39a839cafa46b295258fc86307..f28fd092644ccfb8aff2618b966cb521 + } else if (process.env.PLATFORM_LIBC === "glibc") { + return require(`@parcel/watcher-${process.platform}-${process.arch}-glibc`) + } else { -+ let { MUSL, GLIBC, familySync } = require('detect-libc') -+ let family = familySync() ++ let { MUSL, GLIBC, family, familySync } = require('detect-libc') -let binding; -try { @@ -40,12 +37,14 @@ index 78ee744d74900c39a839cafa46b295258fc86307..f28fd092644ccfb8aff2618b966cb521 - binding = require('./build/Debug/watcher.node'); - } catch (err) { - handleError(err); -- throw new Error( -- `No prebuild or local build of @parcel/watcher found. Tried ${name}. Please ensure it is installed (don't use --no-optional when installing with npm). Otherwise it is possible we don't support your platform yet. If this is the case, please report an issue to https://github.com/parcel-bundler/watcher.`, -- ); +- throw new Error(`No prebuild or local build of @parcel/watcher found. Tried ${name}. Please ensure it is installed (don't use --no-optional when installing with npm). Otherwise it is possible we don't support your platform yet. If this is the case, please report an issue to https://github.com/parcel-bundler/watcher.`); - } - } -} ++ // Bun polyfills `detect-libc` in compiled binaries. We rely on ++ // detect-libc@1.0.3 but the polyfilled version is 2.x. In detect-libc@2x ++ // there is a `familySync` function that we can use instead. ++ if (typeof familySync === 'function') family = familySync() -function handleError(err) { - if (err?.code !== 'MODULE_NOT_FOUND') { @@ -64,7 +63,12 @@ index 78ee744d74900c39a839cafa46b295258fc86307..f28fd092644ccfb8aff2618b966cb521 } -const wrapper = createWrapper(binding); -+const wrapper = createWrapper(loadPackage()); - exports.writeSnapshot = wrapper.writeSnapshot; - exports.getEventsSince = wrapper.getEventsSince; - exports.subscribe = wrapper.subscribe; +-exports.writeSnapshot = wrapper.writeSnapshot; +-exports.getEventsSince = wrapper.getEventsSince; +-exports.subscribe = wrapper.subscribe; +-exports.unsubscribe = wrapper.unsubscribe; ++const wrapper = createWrapper(loadPackage()) ++exports.writeSnapshot = wrapper.writeSnapshot ++exports.getEventsSince = wrapper.getEventsSince ++exports.subscribe = wrapper.subscribe ++exports.unsubscribe = wrapper.unsubscribe diff --git a/patches/lightningcss@1.33.0.patch b/patches/lightningcss@1.30.2.patch similarity index 58% rename from patches/lightningcss@1.33.0.patch rename to patches/lightningcss@1.30.2.patch index e70be2315..204f06ab3 100644 --- a/patches/lightningcss@1.33.0.patch +++ b/patches/lightningcss@1.30.2.patch @@ -1,8 +1,8 @@ diff --git a/node/index.js b/node/index.js -index 6fe25aef457aa5448f9ee6204da1e253f5534800..742be0229b94450b631b6e069ffc372d2b35df47 100644 +index 011d04b450687083e7a0ec9023c41ee79104831c..ef26db49d98c60b5b31f3601f6ad045f0b0e8f4f 100644 --- a/node/index.js +++ b/node/index.js -@@ -1,25 +1,40 @@ +@@ -1,28 +1,41 @@ -let parts = [process.platform, process.arch]; -if (process.platform === 'linux') { - const { MUSL, familySync } = require('detect-libc'); @@ -11,8 +11,12 @@ index 6fe25aef457aa5448f9ee6204da1e253f5534800..742be0229b94450b631b6e069ffc372d - parts.push('musl'); - } else if (process.arch === 'arm') { - parts.push('gnueabihf'); -+// We need to take the `process.env.PLATFORM_LIBC` into account, and we also -+// need static analysis, so we can't make use of the `parts.push(…)` approach. +- } else { +- parts.push('gnu'); +- } +-} else if (process.platform === 'win32') { +- parts.push('msvc'); +-} +function loadPackage() { + if (process.platform === 'linux') { + if (process.env.PLATFORM_LIBC === 'musl') { @@ -24,8 +28,19 @@ index 6fe25aef457aa5448f9ee6204da1e253f5534800..742be0229b94450b631b6e069ffc372d + return require(`lightningcss-${process.platform}-${process.arch}-gnu`) + } + } else { -+ let { MUSL, GLIBC, familySync } = require('detect-libc') -+ let family = familySync() ++ let { MUSL, GLIBC, family, familySync } = require('detect-libc') + +-if (process.env.CSS_TRANSFORMER_WASM) { +- module.exports = require(`../pkg`); +-} else { +- try { +- module.exports = require(`lightningcss-${parts.join('-')}`); +- } catch (err) { +- module.exports = require(`../lightningcss.${parts.join('-')}.node`); ++ // Bun polyfills `detect-libc` in compiled binaries. We rely on ++ // detect-libc@1.0.3 but the polyfilled version is 2.x. In detect-libc@2x ++ // there is a `familySync` function that we can use instead. ++ if (typeof familySync === 'function') family = familySync() + + if (family === MUSL) { + return require(`lightningcss-${process.platform}-${process.arch}-musl`) @@ -41,22 +56,15 @@ index 6fe25aef457aa5448f9ee6204da1e253f5534800..742be0229b94450b631b6e069ffc372d + } + } else if (process.platform === 'win32') { + return require(`lightningcss-${process.platform}-${process.arch}-msvc`) - } else { -- parts.push('gnu'); ++ } else { + return require(`lightningcss-${process.platform}-${process.arch}`) } --} else if (process.platform === 'win32') { -- parts.push('msvc'); } - --let native; --try { -- native = require(`lightningcss-${parts.join('-')}`); --} catch (err) { -- native = require(`../lightningcss.${parts.join('-')}.node`); --} - -+let native = loadPackage(); - module.exports.transform = wrap(native.transform); - module.exports.transformStyleAttribute = wrap(native.transformStyleAttribute); - module.exports.bundle = wrap(native.bundle); + +-module.exports.browserslistToTargets = require('./browserslistToTargets'); +-module.exports.composeVisitors = require('./composeVisitors'); +-module.exports.Features = require('./flags').Features; ++module.exports = loadPackage() ++module.exports.browserslistToTargets = require('./browserslistToTargets') ++module.exports.composeVisitors = require('./composeVisitors') ++module.exports.Features = require('./flags').Features diff --git a/playgrounds/nextjs/app/page.module.css b/playgrounds/nextjs/app/page.module.css index b0117f44c..4c5bdd253 100644 --- a/playgrounds/nextjs/app/page.module.css +++ b/playgrounds/nextjs/app/page.module.css @@ -1,5 +1,5 @@ @reference "./globals.css"; .heading { - @apply text-red-500; + @apply skew-7; } diff --git a/playgrounds/nextjs/package.json b/playgrounds/nextjs/package.json index 5487af5a5..a822b0dd9 100644 --- a/playgrounds/nextjs/package.json +++ b/playgrounds/nextjs/package.json @@ -5,20 +5,23 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start" + "start": "next start", + "lint": "next lint" }, "dependencies": { "@tailwindcss/postcss": "workspace:^", "fast-glob": "^3.3.3", - "next": "16.2.7", - "react": "19.2.7", - "react-dom": "19.2.7", + "next": "15.5.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", - "@types/react": "19.2.15", - "@types/react-dom": "19.2.3", - "typescript": "^6.0.3" + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "eslint": "^9.37.0", + "eslint-config-next": "^15.5.6", + "typescript": "^5.5.4" } } diff --git a/playgrounds/nextjs/tsconfig.json b/playgrounds/nextjs/tsconfig.json index 27414aae7..862143548 100644 --- a/playgrounds/nextjs/tsconfig.json +++ b/playgrounds/nextjs/tsconfig.json @@ -10,7 +10,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { @@ -20,14 +20,7 @@ "paths": { "@/*": ["./*"], }, - "target": "ES2017", }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts", - ], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"], } diff --git a/playgrounds/v3/package.json b/playgrounds/v3/package.json index 8c08310db..121f31e52 100644 --- a/playgrounds/v3/package.json +++ b/playgrounds/v3/package.json @@ -9,16 +9,18 @@ "upgrade": "node scripts/upgrade.mjs" }, "dependencies": { - "next": "16.2.7", - "react": "19.2.7", - "react-dom": "19.2.7", + "next": "15.5.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", "tailwindcss": "^3" }, "devDependencies": { - "@types/node": "catalog:", - "@types/react": "19.2.15", - "@types/react-dom": "19.2.3", - "autoprefixer": "^10.5.0", - "typescript": "^6.0.3" + "@types/node": "^20.14.8", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "autoprefixer": "^10.4.21", + "eslint": "^9.37.0", + "eslint-config-next": "^15.5.6", + "typescript": "^5.5.4" } } diff --git a/playgrounds/v3/tsconfig.json b/playgrounds/v3/tsconfig.json index 27414aae7..862143548 100644 --- a/playgrounds/v3/tsconfig.json +++ b/playgrounds/v3/tsconfig.json @@ -10,7 +10,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { @@ -20,14 +20,7 @@ "paths": { "@/*": ["./*"], }, - "target": "ES2017", }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts", - ], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"], } diff --git a/playgrounds/vite/package.json b/playgrounds/vite/package.json index 25ab1a33b..e0e696d5d 100644 --- a/playgrounds/vite/package.json +++ b/playgrounds/vite/package.json @@ -10,15 +10,15 @@ }, "dependencies": { "@tailwindcss/vite": "workspace:^", - "@vitejs/plugin-react": "^6.0.2", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "@vitejs/plugin-react": "^5.1.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", "tailwindcss": "workspace:^" }, "devDependencies": { - "@types/react": "^19.2.15", - "@types/react-dom": "^19.2.3", - "bun": "^1.3.14", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "bun": "^1.3.0", "vite": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8382b2ba5..23f775083 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,136 +6,87 @@ settings: catalogs: default: - '@parcel/watcher': - specifier: 2.6.0 - version: 2.6.0 - '@parcel/watcher-darwin-arm64': - specifier: 2.6.0 - version: 2.6.0 - '@parcel/watcher-darwin-x64': - specifier: 2.6.0 - version: 2.6.0 - '@parcel/watcher-linux-arm64-glibc': - specifier: 2.6.0 - version: 2.6.0 - '@parcel/watcher-linux-arm64-musl': - specifier: 2.6.0 - version: 2.6.0 - '@parcel/watcher-linux-x64-glibc': - specifier: 2.6.0 - version: 2.6.0 - '@parcel/watcher-linux-x64-musl': - specifier: 2.6.0 - version: 2.6.0 - '@parcel/watcher-win32-x64': - specifier: 2.6.0 - version: 2.6.0 '@types/node': - specifier: 22.20.1 - version: 22.20.1 - dedent: - specifier: 1.7.2 - version: 1.7.2 - enhanced-resolve: - specifier: ^5.24.5 - version: 5.24.5 + specifier: ^20.19.0 + version: 20.19.1 lightningcss: - specifier: 1.33.0 - version: 1.33.0 + specifier: 1.30.2 + version: 1.30.2 lightningcss-darwin-arm64: - specifier: 1.33.0 - version: 1.33.0 + specifier: 1.30.2 + version: 1.30.2 lightningcss-darwin-x64: - specifier: 1.33.0 - version: 1.33.0 + specifier: 1.30.2 + version: 1.30.2 lightningcss-linux-arm64-gnu: - specifier: 1.33.0 - version: 1.33.0 + specifier: 1.30.2 + version: 1.30.2 lightningcss-linux-arm64-musl: - specifier: 1.33.0 - version: 1.33.0 + specifier: 1.30.2 + version: 1.30.2 lightningcss-linux-x64-gnu: - specifier: 1.33.0 - version: 1.33.0 + specifier: 1.30.2 + version: 1.30.2 lightningcss-linux-x64-musl: - specifier: 1.33.0 - version: 1.33.0 + specifier: 1.30.2 + version: 1.30.2 lightningcss-win32-x64-msvc: - specifier: 1.33.0 - version: 1.33.0 - postcss: - specifier: ^8.5.25 - version: 8.5.25 + specifier: 1.30.2 + version: 1.30.2 prettier: - specifier: 3.9.6 - version: 3.9.6 + specifier: 3.6.2 + version: 3.6.2 vite: - specifier: ^8.2.0 - version: 8.2.0 - webpack: - specifier: ^5.109.2 - version: 5.109.2 + specifier: ^7.0.0 + version: 7.0.0 patchedDependencies: - '@napi-rs/cli@3.7.4': 9912bf0a9c2cef8329d11c41fbce4d33ce2bdcf660a630b258c962c0a3c44bed - '@parcel/watcher@2.6.0': 705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da - lightningcss@1.33.0: 1d4a8800d60d13d42887b88b3a86576df4b451670308145fb432ec8abbf40930 + '@parcel/watcher@2.5.1': + hash: zs2vvlrje3h42xp5ed2v44fep4 + path: patches/@parcel__watcher@2.5.1.patch + lightningcss@1.30.2: + hash: tzyxy3asfxcqc7ihrooumyi5fm + path: patches/lightningcss@1.30.2.patch importers: .: devDependencies: '@playwright/test': - specifier: ^1.62.1 - version: 1.62.1 + specifier: ^1.56.1 + version: 1.56.1 '@types/node': specifier: 'catalog:' - version: 22.20.1 + version: 20.19.1 postcss: - specifier: 'catalog:' - version: 8.5.25 + specifier: 8.5.6 + version: 8.5.6 postcss-import: specifier: ^16.1.1 - version: 16.1.1(postcss@8.5.25) + version: 16.1.1(postcss@8.5.6) prettier: specifier: 'catalog:' - version: 3.9.6 + version: 3.6.2 prettier-plugin-embed: - specifier: ^0.5.1 - version: 0.5.1 + specifier: ^0.5.0 + version: 0.5.0 prettier-plugin-organize-imports: specifier: ^4.3.0 - version: 4.3.0(prettier@3.9.6)(typescript@5.9.3) + version: 4.3.0(prettier@3.6.2)(typescript@5.5.4) tsup: - specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.25)(typescript@5.9.3)(yaml@2.9.0) + specifier: ^8.5.0 + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.1)(typescript@5.5.4)(yaml@2.6.0) turbo: - specifier: ^2.10.8 - version: 2.10.8 + specifier: ^2.5.4 + version: 2.5.4 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^5.5.4 + version: 5.5.4 vitest: - specifier: ^4.1.10 - version: 4.1.10(@types/node@22.20.1)(vite@8.2.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + specifier: ^2.0.5 + version: 2.0.5(@types/node@20.19.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6) crates/node: - devDependencies: - '@emnapi/core': - specifier: 1.11.3 - version: 1.11.3 - '@emnapi/runtime': - specifier: 1.11.3 - version: 1.11.3 - '@napi-rs/cli': - specifier: 3.7.4 - version: 3.7.4(patch_hash=9912bf0a9c2cef8329d11c41fbce4d33ce2bdcf660a630b258c962c0a3c44bed)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@25.9.1)(node-addon-api@8.7.0) - '@napi-rs/wasm-runtime': - specifier: ^1.2.2 - version: 1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - emnapi: - specifier: 1.11.3 - version: 1.11.3(node-addon-api@8.7.0) optionalDependencies: '@tailwindcss/oxide-android-arm64': specifier: workspace:* @@ -173,6 +124,16 @@ importers: '@tailwindcss/oxide-win32-x64-msvc': specifier: workspace:* version: link:npm/win32-x64-msvc + devDependencies: + '@napi-rs/cli': + specifier: ^3.4.1 + version: 3.4.1(@emnapi/runtime@1.6.0)(@types/node@20.19.1)(node-addon-api@8.3.0) + '@napi-rs/wasm-runtime': + specifier: ^1.0.7 + version: 1.0.7 + emnapi: + specifier: 1.6.0 + version: 1.6.0(node-addon-api@8.3.0) crates/node/npm/android-arm-eabi: {} @@ -197,22 +158,22 @@ importers: crates/node/npm/wasm32-wasi: dependencies: '@emnapi/core': - specifier: ^1.11.3 - version: 1.11.3 + specifier: ^1.6.0 + version: 1.6.0 '@emnapi/runtime': - specifier: ^1.11.3 - version: 1.11.3 + specifier: ^1.6.0 + version: 1.6.0 '@emnapi/wasi-threads': - specifier: ^1.2.3 - version: 1.2.3 + specifier: ^1.1.0 + version: 1.1.0 '@napi-rs/wasm-runtime': - specifier: ^1.2.2 - version: 1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + specifier: ^1.0.7 + version: 1.0.7 '@tybys/wasm-util': - specifier: ^0.10.3 - version: 0.10.3 + specifier: ^0.10.1 + version: 0.10.1 tslib: - specifier: ^2.8.1 + specifier: ^2.4.0 version: 2.8.1 crates/node/npm/win32-arm64-msvc: {} @@ -222,26 +183,23 @@ importers: integrations: devDependencies: dedent: - specifier: 'catalog:' - version: 1.7.2 + specifier: 1.7.0 + version: 1.7.0 fast-glob: specifier: ^3.3.3 version: 3.3.3 source-map-js: specifier: ^1.2.1 version: 1.2.1 - yaml: - specifier: ^2.9.0 - version: 2.9.0 packages/@tailwindcss-browser: devDependencies: h3: - specifier: ^1.15.11 - version: 1.15.11 + specifier: ^1.15.4 + version: 1.15.4 listhen: - specifier: ^1.10.1 - version: 1.10.1(@parcel/watcher@2.6.0(patch_hash=705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da)) + specifier: ^1.9.0 + version: 1.9.0 tailwindcss: specifier: workspace:* version: link:../tailwindcss @@ -249,8 +207,8 @@ importers: packages/@tailwindcss-cli: dependencies: '@parcel/watcher': - specifier: 'catalog:' - version: 2.6.0(patch_hash=705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da) + specifier: ^2.5.1 + version: 2.5.1(patch_hash=zs2vvlrje3h42xp5ed2v44fep4) '@tailwindcss/node': specifier: workspace:* version: link:../@tailwindcss-node @@ -258,8 +216,8 @@ importers: specifier: workspace:* version: link:../../crates/node enhanced-resolve: - specifier: 'catalog:' - version: 5.24.5 + specifier: ^5.18.3 + version: 5.18.3 mri: specifier: ^1.2.0 version: 1.2.0 @@ -273,20 +231,20 @@ importers: packages/@tailwindcss-node: dependencies: '@jridgewell/remapping': - specifier: ^2.3.5 + specifier: ^2.3.4 version: 2.3.5 enhanced-resolve: - specifier: 'catalog:' - version: 5.24.5 + specifier: ^5.18.3 + version: 5.18.3 jiti: - specifier: ^2.7.0 - version: 2.7.0 + specifier: ^2.6.1 + version: 2.6.1 lightningcss: specifier: 'catalog:' - version: 1.33.0(patch_hash=1d4a8800d60d13d42887b88b3a86576df4b451670308145fb432ec8abbf40930) + version: 1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) magic-string: - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^0.30.21 + version: 0.30.21 source-map-js: specifier: ^1.2.1 version: 1.2.1 @@ -306,27 +264,27 @@ importers: specifier: workspace:* version: link:../../crates/node postcss: - specifier: 'catalog:' - version: 8.5.25 + specifier: ^8.4.41 + version: 8.4.41 tailwindcss: specifier: workspace:* version: link:../tailwindcss devDependencies: '@types/node': specifier: 'catalog:' - version: 22.20.1 + version: 20.19.1 '@types/postcss-import': specifier: 14.0.3 version: 14.0.3 dedent: - specifier: 'catalog:' - version: 1.7.2 + specifier: 1.7.0 + version: 1.7.0 internal-example-plugin: specifier: workspace:* version: link:../internal-example-plugin postcss-import: specifier: ^16.1.1 - version: 16.1.1(postcss@8.5.25) + version: 16.1.1(postcss@8.4.41) packages/@tailwindcss-standalone: dependencies: @@ -337,91 +295,69 @@ importers: specifier: workspace:* version: link:../@tailwindcss-cli '@tailwindcss/forms': - specifier: ^0.5.11 - version: 0.5.11(tailwindcss@packages+tailwindcss) + specifier: ^0.5.10 + version: 0.5.10(tailwindcss@packages+tailwindcss) '@tailwindcss/typography': - specifier: ^0.5.20 - version: 0.5.20(tailwindcss@packages+tailwindcss) + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@packages+tailwindcss) detect-libc: specifier: 1.0.3 version: 1.0.3 enhanced-resolve: - specifier: 'catalog:' - version: 5.24.5 + specifier: ^5.18.3 + version: 5.18.3 tailwindcss: specifier: workspace:* version: link:../tailwindcss devDependencies: '@parcel/watcher-darwin-arm64': - specifier: 'catalog:' - version: 2.6.0 + specifier: ^2.5.1 + version: 2.5.1 '@parcel/watcher-darwin-x64': - specifier: 'catalog:' - version: 2.6.0 + specifier: ^2.5.1 + version: 2.5.1 '@parcel/watcher-linux-arm64-glibc': - specifier: 'catalog:' - version: 2.6.0 + specifier: ^2.5.1 + version: 2.5.1 '@parcel/watcher-linux-arm64-musl': - specifier: 'catalog:' - version: 2.6.0 + specifier: ^2.5.1 + version: 2.5.1 '@parcel/watcher-linux-x64-glibc': - specifier: 'catalog:' - version: 2.6.0 + specifier: ^2.5.1 + version: 2.5.1 '@parcel/watcher-linux-x64-musl': - specifier: 'catalog:' - version: 2.6.0 + specifier: ^2.5.1 + version: 2.5.1 '@parcel/watcher-win32-x64': - specifier: 'catalog:' - version: 2.6.0 + specifier: ^2.5.1 + version: 2.5.1 '@types/bun': - specifier: ^1.3.14 - version: 1.3.14 + specifier: ^1.3.0 + version: 1.3.0(@types/react@19.2.2) bun: - specifier: ^1.3.14 - version: 1.3.14 + specifier: ^1.3.0 + version: 1.3.0 lightningcss-darwin-arm64: specifier: 'catalog:' - version: 1.33.0 + version: 1.30.2 lightningcss-darwin-x64: specifier: 'catalog:' - version: 1.33.0 + version: 1.30.2 lightningcss-linux-arm64-gnu: specifier: 'catalog:' - version: 1.33.0 + version: 1.30.2 lightningcss-linux-arm64-musl: specifier: 'catalog:' - version: 1.33.0 + version: 1.30.2 lightningcss-linux-x64-gnu: specifier: 'catalog:' - version: 1.33.0 + version: 1.30.2 lightningcss-linux-x64-musl: specifier: 'catalog:' - version: 1.33.0 + version: 1.30.2 lightningcss-win32-x64-msvc: specifier: 'catalog:' - version: 1.33.0 - - packages/@tailwindcss-turbopack: - dependencies: - '@alloc/quick-lru': - specifier: ^5.2.0 - version: 5.2.0 - '@tailwindcss/node': - specifier: workspace:* - version: link:../@tailwindcss-node - '@tailwindcss/oxide': - specifier: workspace:* - version: link:../../crates/node - tailwindcss: - specifier: workspace:* - version: link:../tailwindcss - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 22.20.1 - webpack: - specifier: 'catalog:' - version: 5.109.2(esbuild@0.27.7)(postcss@8.5.25) + version: 1.30.2 packages/@tailwindcss-upgrade: dependencies: @@ -431,18 +367,21 @@ importers: '@tailwindcss/oxide': specifier: workspace:* version: link:../../crates/node + braces: + specifier: ^3.0.3 + version: 3.0.3 dedent: - specifier: 'catalog:' - version: 1.7.2 + specifier: 1.7.0 + version: 1.7.0 enhanced-resolve: - specifier: 'catalog:' - version: 5.24.5 + specifier: ^5.18.3 + version: 5.18.3 globby: - specifier: ^16.2.2 - version: 16.2.2 + specifier: ^15.0.0 + version: 15.0.0 jiti: - specifier: ^2.7.0 - version: 2.7.0 + specifier: ^2.0.0-beta.3 + version: 2.6.1 mri: specifier: ^1.2.0 version: 1.2.0 @@ -450,39 +389,42 @@ importers: specifier: ^1.1.1 version: 1.1.1 postcss: - specifier: 'catalog:' - version: 8.5.25 + specifier: ^8.4.41 + version: 8.4.41 postcss-import: specifier: ^16.1.1 - version: 16.1.1(postcss@8.5.25) + version: 16.1.1(postcss@8.4.41) postcss-selector-parser: - specifier: ^7.1.4 - version: 7.1.4 + specifier: ^7.1.0 + version: 7.1.0 prettier: specifier: 'catalog:' - version: 3.9.6 + version: 3.6.2 semver: - specifier: ^7.8.5 - version: 7.8.5 + specifier: ^7.7.3 + version: 7.7.3 tailwindcss: specifier: workspace:* version: link:../tailwindcss tree-sitter: - specifier: ^0.25.1 - version: 0.25.1 + specifier: ^0.22.4 + version: 0.22.4 tree-sitter-typescript: specifier: ^0.23.2 - version: 0.23.2(tree-sitter@0.25.1) + version: 0.23.2(tree-sitter@0.22.4) devDependencies: + '@types/braces': + specifier: ^3.0.5 + version: 3.0.5 '@types/node': specifier: 'catalog:' - version: 22.20.1 + version: 20.19.1 '@types/postcss-import': specifier: ^14.0.3 version: 14.0.3 '@types/semver': - specifier: ^7.8.0 - version: 7.8.0 + specifier: ^7.7.1 + version: 7.7.1 packages/@tailwindcss-vite: dependencies: @@ -498,58 +440,33 @@ importers: devDependencies: '@types/node': specifier: 'catalog:' - version: 22.20.1 + version: 20.19.1 vite: specifier: 'catalog:' - version: 8.2.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) - - packages/@tailwindcss-webpack: - dependencies: - '@alloc/quick-lru': - specifier: ^5.2.0 - version: 5.2.0 - '@rspack/core': - specifier: ^1.0.0 || ^2.0.0 - version: 2.0.2(@swc/helpers@0.5.15) - '@tailwindcss/node': - specifier: workspace:* - version: link:../@tailwindcss-node - '@tailwindcss/oxide': - specifier: workspace:* - version: link:../../crates/node - tailwindcss: - specifier: workspace:* - version: link:../tailwindcss - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 22.20.1 - webpack: - specifier: 'catalog:' - version: 5.109.2(esbuild@0.27.7)(postcss@8.5.25) + version: 7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0) packages/internal-example-plugin: {} packages/tailwindcss: devDependencies: '@jridgewell/remapping': - specifier: ^2.3.5 + specifier: ^2.3.4 version: 2.3.5 '@tailwindcss/oxide': specifier: workspace:^ version: link:../../crates/node '@types/node': specifier: 'catalog:' - version: 22.20.1 + version: 20.19.1 dedent: - specifier: 'catalog:' - version: 1.7.2 + specifier: 1.7.0 + version: 1.7.0 lightningcss: specifier: 'catalog:' - version: 1.33.0(patch_hash=1d4a8800d60d13d42887b88b3a86576df4b451670308145fb432ec8abbf40930) + version: 1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) magic-string: - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^0.30.21 + version: 0.30.21 source-map-js: specifier: ^1.2.1 version: 1.2.1 @@ -563,61 +480,73 @@ importers: specifier: ^3.3.3 version: 3.3.3 next: - specifier: 16.2.7 - version: 16.2.7(@playwright/test@1.62.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 15.5.6 + version: 15.5.6(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: - specifier: 19.2.7 - version: 19.2.7 + specifier: ^19.2.0 + version: 19.2.0 react-dom: - specifier: 19.2.7 - version: 19.2.7(react@19.2.7) + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) tailwindcss: specifier: workspace:^ version: link:../../packages/tailwindcss devDependencies: '@types/node': specifier: 'catalog:' - version: 22.20.1 + version: 20.19.1 '@types/react': - specifier: 19.2.15 - version: 19.2.15 + specifier: ^19.2.2 + version: 19.2.2 '@types/react-dom': - specifier: 19.2.3 - version: 19.2.3(@types/react@19.2.15) + specifier: ^19.2.2 + version: 19.2.2(@types/react@19.2.2) + eslint: + specifier: ^9.37.0 + version: 9.37.0(jiti@2.6.1) + eslint-config-next: + specifier: ^15.5.6 + version: 15.5.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4) typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^5.5.4 + version: 5.5.4 playgrounds/v3: dependencies: next: - specifier: 16.2.7 - version: 16.2.7(@playwright/test@1.62.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 15.5.6 + version: 15.5.6(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: - specifier: 19.2.7 - version: 19.2.7 + specifier: ^19.2.0 + version: 19.2.0 react-dom: - specifier: 19.2.7 - version: 19.2.7(react@19.2.7) + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) tailwindcss: specifier: ^3 - version: 3.4.19(yaml@2.9.0) + version: 3.4.14 devDependencies: '@types/node': - specifier: 'catalog:' - version: 22.20.1 + specifier: ^20.14.8 + version: 20.14.13 '@types/react': - specifier: 19.2.15 - version: 19.2.15 + specifier: ^19.2.2 + version: 19.2.2 '@types/react-dom': - specifier: 19.2.3 - version: 19.2.3(@types/react@19.2.15) + specifier: ^19.2.2 + version: 19.2.2(@types/react@19.2.2) autoprefixer: - specifier: ^10.5.0 - version: 10.5.0(postcss@8.5.25) + specifier: ^10.4.21 + version: 10.4.21(postcss@8.4.47) + eslint: + specifier: ^9.37.0 + version: 9.37.0(jiti@2.6.1) + eslint-config-next: + specifier: ^15.5.6 + version: 15.5.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3) typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^5.5.4 + version: 5.6.3 playgrounds/vite: dependencies: @@ -625,30 +554,30 @@ importers: specifier: workspace:^ version: link:../../packages/@tailwindcss-vite '@vitejs/plugin-react': - specifier: ^6.0.2 - version: 6.0.2(vite@8.2.0(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + specifier: ^5.1.0 + version: 5.1.0(vite@7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0)) react: - specifier: ^19.2.6 - version: 19.2.6 + specifier: ^19.2.0 + version: 19.2.0 react-dom: - specifier: ^19.2.6 - version: 19.2.6(react@19.2.6) + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) tailwindcss: specifier: workspace:^ version: link:../../packages/tailwindcss devDependencies: '@types/react': - specifier: ^19.2.15 - version: 19.2.15 + specifier: ^19.2.2 + version: 19.2.2 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.15) + specifier: ^19.2.2 + version: 19.2.2(@types/react@19.2.2) bun: - specifier: ^1.3.14 - version: 1.3.14 + specifier: ^1.3.0 + version: 1.3.0 vite: specifier: 'catalog:' - version: 8.2.0(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) + version: 7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0) packages: @@ -656,478 +585,864 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} - '@emnapi/core@1.11.3': - resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} - '@emnapi/core@2.0.0-alpha.3': - resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} + '@babel/compat-data@7.27.5': + resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} + engines: {node: '>=6.9.0'} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} - '@emnapi/runtime@1.11.3': - resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} - '@emnapi/runtime@2.0.0-alpha.3': - resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} - '@emnapi/wasi-threads@1.2.3': - resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} - '@emnapi/wasi-threads@2.0.1': - resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.5.0': + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + + '@emnapi/core@1.6.0': + resolution: {integrity: sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==} + + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + + '@emnapi/runtime@1.6.0': + resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + '@esbuild/aix-ppc64@0.25.0': + resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + '@esbuild/android-arm64@0.25.0': + resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + '@esbuild/android-arm@0.25.0': + resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + '@esbuild/android-x64@0.25.0': + resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + '@esbuild/darwin-arm64@0.25.0': + resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + '@esbuild/darwin-x64@0.25.0': + resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + '@esbuild/freebsd-arm64@0.25.0': + resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + '@esbuild/freebsd-x64@0.25.0': + resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + '@esbuild/linux-arm64@0.25.0': + resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + '@esbuild/linux-arm@0.25.0': + resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + '@esbuild/linux-ia32@0.25.0': + resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + '@esbuild/linux-loong64@0.25.0': + resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + '@esbuild/linux-mips64el@0.25.0': + resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + '@esbuild/linux-ppc64@0.25.0': + resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + '@esbuild/linux-riscv64@0.25.0': + resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + '@esbuild/linux-s390x@0.25.0': + resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + '@esbuild/linux-x64@0.25.0': + resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.0': + resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + '@esbuild/netbsd-x64@0.25.0': + resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + '@esbuild/openbsd-arm64@0.25.0': + resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + '@esbuild/openbsd-x64@0.25.0': + resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] + cpu: [x64] + os: [openbsd] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + '@esbuild/sunos-x64@0.25.0': + resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + '@esbuild/win32-arm64@0.25.0': + resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + '@esbuild/win32-ia32@0.25.0': + resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + '@esbuild/win32-x64@0.25.0': + resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} engines: {node: '>=18'} + cpu: [x64] + os: [win32] - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.0': + resolution: {integrity: sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.16.0': + resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.37.0': + resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.0': + resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.2': + resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + engines: {node: '>=18.18'} + + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} cpu: [arm64] os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} cpu: [arm] os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} cpu: [ppc64] os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} cpu: [s390x] os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} cpu: [x64] os: [linux] - libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} cpu: [arm64] os: [linux] - libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} cpu: [x64] os: [linux] - libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] - '@inquirer/ansi@2.0.7': - resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/ansi@1.0.0': + resolution: {integrity: sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==} + engines: {node: '>=18'} - '@inquirer/checkbox@5.2.1': - resolution: {integrity: sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/checkbox@4.2.4': + resolution: {integrity: sha512-2n9Vgf4HSciFq8ttKXk+qy+GsyTXPV1An6QAwe/8bkbbqvG4VW1I/ZY1pNu2rf+h9bdzMLPbRSfcNxkHBy/Ydw==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/confirm@6.1.1': - resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/confirm@5.1.18': + resolution: {integrity: sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/core@11.2.1': - resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/core@10.2.2': + resolution: {integrity: sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/editor@5.2.2': - resolution: {integrity: sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/editor@4.2.20': + resolution: {integrity: sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/expand@5.1.1': - resolution: {integrity: sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/expand@4.0.20': + resolution: {integrity: sha512-Dt9S+6qUg94fEvgn54F2Syf0Z3U8xmnBI9ATq2f5h9xt09fs2IJXSCIXyyVHwvggKWFXEY/7jATRo2K6Dkn6Ow==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/external-editor@3.0.3': - resolution: {integrity: sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/external-editor@1.0.2': + resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/figures@2.0.7': - resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/figures@1.0.13': + resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} + engines: {node: '>=18'} - '@inquirer/input@5.1.2': - resolution: {integrity: sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/input@4.2.4': + resolution: {integrity: sha512-cwSGpLBMwpwcZZsc6s1gThm0J+it/KIJ+1qFL2euLmSKUMGumJ5TcbMgxEjMjNHRGadouIYbiIgruKoDZk7klw==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/number@4.1.1': - resolution: {integrity: sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/number@3.0.20': + resolution: {integrity: sha512-bbooay64VD1Z6uMfNehED2A2YOPHSJnQLs9/4WNiV/EK+vXczf/R988itL2XLDGTgmhMF2KkiWZo+iEZmc4jqg==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/password@5.1.1': - resolution: {integrity: sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/password@4.0.20': + resolution: {integrity: sha512-nxSaPV2cPvvoOmRygQR+h0B+Av73B01cqYLcr7NXcGXhbmsYfUb8fDdw2Us1bI2YsX+VvY7I7upgFYsyf8+Nug==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/prompts@8.5.2': - resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/prompts@7.8.6': + resolution: {integrity: sha512-68JhkiojicX9SBUD8FE/pSKbOKtwoyaVj1kwqLfvjlVXZvOy3iaSWX4dCLsZyYx/5Ur07Fq+yuDNOen+5ce6ig==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/rawlist@5.3.1': - resolution: {integrity: sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/rawlist@4.1.8': + resolution: {integrity: sha512-CQ2VkIASbgI2PxdzlkeeieLRmniaUU1Aoi5ggEdm6BIyqopE9GuDXdDOj9XiwOqK5qm72oI2i6J+Gnjaa26ejg==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/search@4.2.1': - resolution: {integrity: sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/search@3.1.3': + resolution: {integrity: sha512-D5T6ioybJJH0IiSUK/JXcoRrrm8sXwzrVMjibuPs+AgxmogKslaafy1oxFiorNI4s3ElSkeQZbhYQgLqiL8h6Q==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/select@5.2.1': - resolution: {integrity: sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/select@4.3.4': + resolution: {integrity: sha512-Qp20nySRmfbuJBBsgPU7E/cL62Hf250vMZRzYDcBHty2zdD1kKCnoDFWRr0WO2ZzaXp3R7a4esaVGJUx0E6zvA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/type@4.0.7': - resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + '@inquirer/type@3.0.8': + resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} @@ -1136,21 +1451,31 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.11': - resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@napi-rs/cli@3.7.4': - resolution: {integrity: sha512-idELIUceJ9zPgx/shcMt1fSSsteFG68v56RIXi4qUm7OYuJOt7CoMj2KnHVmbOqXgUHWQU1lCULrtQdZDG4F5A==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@napi-rs/cli@3.4.1': + resolution: {integrity: sha512-ayhm+NfrP5Hmh7vy5pfyYm/ktYtLh2PrgdLuqHTAubO7RoO2JkUE4F991AtgYxNewwXI8+guZLxU8itV7QnDrQ==} engines: {node: '>= 16'} hasBin: true peerDependencies: - '@emnapi/runtime': ^1.7.1 + '@emnapi/runtime': ^1.5.0 peerDependenciesMeta: '@emnapi/runtime': optional: true @@ -1231,49 +1556,42 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/lzma-linux-arm64-musl@1.4.5': resolution: {integrity: sha512-yWjcPDgJ2nIL3KNvi4536dlT/CcCWO0DUyEOlBs/SacG7BeD6IjGh6yYzd3/X1Y3JItCbZoDoLUH8iB1lTXo3w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/lzma-linux-ppc64-gnu@1.4.5': resolution: {integrity: sha512-0XRhKuIU/9ZjT4WDIG/qnX7Xz7mSQHYZo9Gb3MP2gcvBgr6BA4zywQ9k3gmQaPn9ECE+CZg2V7DV7kT+x2pUMQ==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] - libc: [glibc] '@napi-rs/lzma-linux-riscv64-gnu@1.4.5': resolution: {integrity: sha512-QrqDIPEUUB23GCpyQj/QFyMlr8SGxxyExeZz9OWFnHfb70kXdTLWrHS/hEI1Ru+lSbQ/6xRqeoGyQ4Aqdg+/RA==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@napi-rs/lzma-linux-s390x-gnu@1.4.5': resolution: {integrity: sha512-k8RVM5aMhW86E9H0QXdquwojew4H3SwPxbRVbl49/COJQWCUjGi79X6mYruMnMPEznZinUiT1jgKbFo2A00NdA==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] - libc: [glibc] '@napi-rs/lzma-linux-x64-gnu@1.4.5': resolution: {integrity: sha512-6rMtBgnIq2Wcl1rQdZsnM+rtCcVCbws1nF8S2NzaUsVaZv8bjrPiAa0lwg4Eqnn1d9lgwqT+cZgm5m+//K08Kw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/lzma-linux-x64-musl@1.4.5': resolution: {integrity: sha512-eiadGBKi7Vd0bCArBUOO/qqRYPHt/VQVvGyYvDFt6C2ZSIjlD+HuOl+2oS1sjf4CFjK4eDIog6EdXnL0NE6iyQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/lzma-wasm32-wasi@1.4.5': resolution: {integrity: sha512-+VyHHlr68dvey6fXc2hehw9gHVFIW3TtGF1XkcbAu65qVXsA9D/T+uuoRVqhE+JCyFHFrO0ixRbZDRK1XJt1sA==} @@ -1343,42 +1661,36 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/tar-linux-arm64-musl@1.1.0': resolution: {integrity: sha512-L/y1/26q9L/uBqiW/JdOb/Dc94egFvNALUZV2WCGKQXc6UByPBMgdiEyW2dtoYxYYYYc+AKD+jr+wQPcvX2vrQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/tar-linux-ppc64-gnu@1.1.0': resolution: {integrity: sha512-EPE1K/80RQvPbLRJDJs1QmCIcH+7WRi0F73+oTe1582y9RtfGRuzAkzeBuAGRXAQEjRQw/RjtNqr6UTJ+8UuWQ==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] - libc: [glibc] '@napi-rs/tar-linux-s390x-gnu@1.1.0': resolution: {integrity: sha512-B2jhWiB1ffw1nQBqLUP1h4+J1ovAxBOoe5N2IqDMOc63fsPZKNqF1PvO/dIem8z7LL4U4bsfmhy3gBfu547oNQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] - libc: [glibc] '@napi-rs/tar-linux-x64-gnu@1.1.0': resolution: {integrity: sha512-tbZDHnb9617lTnsDMGo/eAMZxnsQFnaRe+MszRqHguKfMwkisc9CCJnks/r1o84u5fECI+J/HOrKXgczq/3Oww==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/tar-linux-x64-musl@1.1.0': resolution: {integrity: sha512-dV6cODlzbO8u6Anmv2N/ilQHq/AWz0xyltuXoLU3yUyXbZcnWYZuB2rL8OBGPmqNcD+x9NdScBNXh7vWN0naSQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/tar-wasm32-wasi@1.1.0': resolution: {integrity: sha512-jIa9nb2HzOrfH0F8QQ9g3WE4aMH5vSI5/1NYVNm9ysCmNjCCtMXCAhlI3WKCdm/DwHf0zLqdrrtDFXODcNaqMw==} @@ -1407,18 +1719,8 @@ packages: resolution: {integrity: sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ==} engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@napi-rs/wasm-runtime@1.2.2': - resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + '@napi-rs/wasm-runtime@1.0.7': + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} '@napi-rs/wasm-tools-android-arm-eabi@1.0.1': resolution: {integrity: sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw==} @@ -1455,28 +1757,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/wasm-tools-linux-arm64-musl@1.0.1': resolution: {integrity: sha512-jAasbIvjZXCgX0TCuEFQr+4D6Lla/3AAVx2LmDuMjgG4xoIXzjKWl7c4chuaD+TI+prWT0X6LJcdzFT+ROKGHQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/wasm-tools-linux-x64-gnu@1.0.1': resolution: {integrity: sha512-Plgk5rPqqK2nocBGajkMVbGm010Z7dnUgq0wtnYRZbzWWxwWcXfZMPa8EYxrK4eE8SzpI7VlZP1tdVsdjgGwMw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/wasm-tools-linux-x64-musl@1.0.1': resolution: {integrity: sha512-GW7AzGuWxtQkyHknHWYFdR0CHmW6is8rG2Rf4V6GNmMpmwtXt/ItWYWtBe4zqJWycMNazpfZKSw/BpT7/MVCXQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/wasm-tools-wasm32-wasi@1.0.1': resolution: {integrity: sha512-/nQVSTrqSsn7YdAc2R7Ips/tnw5SPUcl3D7QrXCNGPqjbatIspnaexvaOYNyKMU6xPu+pc0BTnKVmqhlJJCPLA==} @@ -1505,57 +1803,56 @@ packages: resolution: {integrity: sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ==} engines: {node: '>= 10'} - '@next/env@16.2.7': - resolution: {integrity: sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==} + '@next/env@15.5.6': + resolution: {integrity: sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==} - '@next/swc-darwin-arm64@16.2.7': - resolution: {integrity: sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==} + '@next/eslint-plugin-next@15.5.6': + resolution: {integrity: sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==} + + '@next/swc-darwin-arm64@15.5.6': + resolution: {integrity: sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.7': - resolution: {integrity: sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==} + '@next/swc-darwin-x64@15.5.6': + resolution: {integrity: sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.7': - resolution: {integrity: sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==} + '@next/swc-linux-arm64-gnu@15.5.6': + resolution: {integrity: sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.7': - resolution: {integrity: sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==} + '@next/swc-linux-arm64-musl@15.5.6': + resolution: {integrity: sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] - '@next/swc-linux-x64-gnu@16.2.7': - resolution: {integrity: sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==} + '@next/swc-linux-x64-gnu@15.5.6': + resolution: {integrity: sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] - '@next/swc-linux-x64-musl@16.2.7': - resolution: {integrity: sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==} + '@next/swc-linux-x64-musl@15.5.6': + resolution: {integrity: sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.7': - resolution: {integrity: sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==} + '@next/swc-win32-arm64-msvc@15.5.6': + resolution: {integrity: sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.7': - resolution: {integrity: sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==} + '@next/swc-win32-x64-msvc@15.5.6': + resolution: {integrity: sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1572,27 +1869,34 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + '@octokit/auth-token@6.0.0': resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} engines: {node: '>= 20'} - '@octokit/core@7.0.6': - resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==} + '@octokit/core@7.0.4': + resolution: {integrity: sha512-jOT8V1Ba5BdC79sKrRWDdMT5l1R+XNHTPR6CPWzUP2EcfAcvIHZWF0eAbmRcpOOP5gVIwnqNg0C4nvh6Abc3OA==} engines: {node: '>= 20'} - '@octokit/endpoint@11.0.3': - resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==} + '@octokit/endpoint@11.0.0': + resolution: {integrity: sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==} engines: {node: '>= 20'} - '@octokit/graphql@9.0.3': - resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} + '@octokit/graphql@9.0.1': + resolution: {integrity: sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==} engines: {node: '>= 20'} - '@octokit/openapi-types@27.0.0': - resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} - '@octokit/plugin-paginate-rest@14.0.0': - resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} + '@octokit/openapi-types@26.0.0': + resolution: {integrity: sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==} + + '@octokit/plugin-paginate-rest@13.1.1': + resolution: {integrity: sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==} engines: {node: '>= 20'} peerDependencies: '@octokit/core': '>=6' @@ -1603,509 +1907,472 @@ packages: peerDependencies: '@octokit/core': '>=6' - '@octokit/plugin-rest-endpoint-methods@17.0.0': - resolution: {integrity: sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==} + '@octokit/plugin-rest-endpoint-methods@16.1.0': + resolution: {integrity: sha512-nCsyiKoGRnhH5LkH8hJEZb9swpqOcsW+VXv1QoyUNQXJeVODG4+xM6UICEqyqe9XFr6LkL8BIiFCPev8zMDXPw==} engines: {node: '>= 20'} peerDependencies: '@octokit/core': '>=6' - '@octokit/request-error@7.1.0': - resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} + '@octokit/request-error@7.0.0': + resolution: {integrity: sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==} engines: {node: '>= 20'} - '@octokit/request@10.0.9': - resolution: {integrity: sha512-o8Bi3f608eyM+7BmBiUWxFsdjLb3/ym1cQek5LZOv9KkZcxRrHCPhhRzm6xjO6HVZ85ItD6+sTsjxo821SVa/A==} + '@octokit/request@10.0.3': + resolution: {integrity: sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==} engines: {node: '>= 20'} - '@octokit/rest@22.0.1': - resolution: {integrity: sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==} + '@octokit/rest@22.0.0': + resolution: {integrity: sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==} engines: {node: '>= 20'} - '@octokit/types@16.0.0': - resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} - '@oven/bun-darwin-aarch64@1.3.14': - resolution: {integrity: sha512-Omj20SuiHBOUjUBIyqtkNjSUIjOtEOJwmbix/ZyFH4BaQ6OZTaaRWIR4TjHVz0yadHgli6lLTiAh1uarnvD49A==} + '@octokit/types@15.0.0': + resolution: {integrity: sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==} + + '@oven/bun-darwin-aarch64@1.3.0': + resolution: {integrity: sha512-WeXSaL29ylJEZMYHHW28QZ6rgAbxQ1KuNSZD9gvd3fPlo0s6s2PglvPArjjP07nmvIK9m4OffN0k4M98O7WmAg==} cpu: [arm64] os: [darwin] - '@oven/bun-darwin-x64-baseline@1.3.14': - resolution: {integrity: sha512-OSfsTZstc898HHElhU4NccaBGOSSDn5VfahiVTnidZ9B/+wb7WTyfZJaBeJcfjwJ9H2W9uTh2TGtl3UfcXgV9g==} + '@oven/bun-darwin-x64-baseline@1.3.0': + resolution: {integrity: sha512-+FSr/ub5vA/EkD3fMhHJUzYioSf/sXd50OGxNDAntVxcDu4tXL/81Ka3R/gkZmjznpLFIzovU/1Ts+b7dlkrfw==} cpu: [x64] os: [darwin] - '@oven/bun-darwin-x64@1.3.14': - resolution: {integrity: sha512-FFj3QdU/OhlDyZOJ8CWfN5eWLpRlT4qjZg7lMQi7jA6GuoY5ajlO1zWLP/MuHYRSbXQUvV52RejNi8DVnAp13w==} + '@oven/bun-darwin-x64@1.3.0': + resolution: {integrity: sha512-CFKjoUWQH0Oz3UHYfKbdKLq0wGryrFsTJEYq839qAwHQSECvVZYAnxVVDYUDa0yQFonhO2qSHY41f6HK+b7xtw==} cpu: [x64] os: [darwin] - '@oven/bun-freebsd-aarch64@1.3.14': - resolution: {integrity: sha512-LIKrXaFxAHybVO5Pf+9XP2FHUj/5APvXTUKk9dqHm5iFz4oH+W24cmhjkJirNujh9hKeTyrpWSe3no9JZKowIw==} - cpu: [arm64] - os: [freebsd] - - '@oven/bun-freebsd-x64@1.3.14': - resolution: {integrity: sha512-uwD+fGUH1ADpIF3B1U2jWzzb20QwRLZfj5QZ28GUCGrAJ/nTmWrD6YYGsblCY1wuhldRez3lU40AyuvSCyLYmw==} - cpu: [x64] - os: [freebsd] - - '@oven/bun-linux-aarch64-android@1.3.14': - resolution: {integrity: sha512-y4kq5b85lsrmFb9Xvi4w9mA5IEFJkLMrSmYn06q24KjL9rUWDWO3VFZEtteZxUN5+ec3Zm5S8OnJw1umaCbVjA==} - cpu: [arm64] - os: [android] - - '@oven/bun-linux-aarch64-musl@1.3.14': - resolution: {integrity: sha512-jmqOA92Cd1NL/1XBd4bFkJLxQ86K0RW7ohxS2qzzAvuitO4JiIxjjTeCspoU44zCozH72HpfZfUE2On31OjnWA==} + '@oven/bun-linux-aarch64-musl@1.3.0': + resolution: {integrity: sha512-HT5sr7N8NDYbQRjAnT7ISpx64y+ewZZRQozOJb0+KQObKvg4UUNXGm4Pn1xA4/WPMZDDazjO8E2vtOQw1nJlAQ==} cpu: [arm64] os: [linux] - '@oven/bun-linux-aarch64@1.3.14': - resolution: {integrity: sha512-X5SsPZHs+iYO8R/efIcRtc7gT2Q2DgPfliCxEkx4cXBumwkw0c/EsHMNwH3EgGpCDaZ7IYVPhpCG/xBOQHEwZw==} + '@oven/bun-linux-aarch64@1.3.0': + resolution: {integrity: sha512-WHthS/eLkCNcp9pk4W8aubRl9fIUgt2XhHyLrP0GClB1FVvmodu/zIOtG0NXNpzlzB8+gglOkGo4dPjfVf4Z+g==} cpu: [arm64] os: [linux] - '@oven/bun-linux-x64-android@1.3.14': - resolution: {integrity: sha512-qe9e1d+3VAEU7nAA2ol9Jvmy/o99PVMSgZhHn7Q/9O3YcDrfEqyQ8zm4zoe5qTEo8HZH0dN03Le0Ys2eQPs7eg==} - cpu: [x64] - os: [android] - - '@oven/bun-linux-x64-baseline@1.3.14': - resolution: {integrity: sha512-q/8EdOC0yUE8FPeoOVq8/Pw5I9/tJaYmUfO/uDUAREx8IUnOJH1RJ5A3BjFqre8pvJoiZA9AovPJq5FnNNjSxA==} + '@oven/bun-linux-x64-baseline@1.3.0': + resolution: {integrity: sha512-OmlEH3nlxQyv7HOvTH21vyNAZGv9DIPnrTznzvKiOQxkOphhCyKvPTlF13ydw4s/i18iwaUrhHy+YG9HSSxa4Q==} cpu: [x64] os: [linux] - '@oven/bun-linux-x64-musl-baseline@1.3.14': - resolution: {integrity: sha512-n6iE71G4lQE4XkrZhQQcL5YUlxDbnq6nqV7zeQi33PMsLT/0kYE+RvHOtBWZ3w0wMdXZfINmp63hIb9ijUBGtw==} + '@oven/bun-linux-x64-musl-baseline@1.3.0': + resolution: {integrity: sha512-hrr7mDvUjMX1tuJaXz448tMsgKIqGJBY8+rJqztKOw1U5+a/v2w5HuIIW1ce7ut0ZwEn+KIDvAujlPvpH33vpQ==} cpu: [x64] os: [linux] - '@oven/bun-linux-x64-musl@1.3.14': - resolution: {integrity: sha512-GBCB/k/sIqcr06eTNgg7g46qiUv35Jasx4XiccJ/n7RGqrE4RWUD/XJBbWFprVPjvqd59+QtSnS99XGqvftHfg==} + '@oven/bun-linux-x64-musl@1.3.0': + resolution: {integrity: sha512-rtzUEzCynl3Rhgn/iR9DQezSFiZMcAXAbU+xfROqsweMGKwvwIA2ckyyckO08psEP8XcUZTs3LT9CH7PnaMiEA==} cpu: [x64] os: [linux] - '@oven/bun-linux-x64@1.3.14': - resolution: {integrity: sha512-7OVTAKvwfPmSbIV1HpdOoVVx5VRc427GuPPne93N6vk4eQBPId9nXmZDh9/zGaKPdbVjVtQSZafWQoUjx38Utw==} + '@oven/bun-linux-x64@1.3.0': + resolution: {integrity: sha512-sGEWoJQXO4GDr0x4t/yJQ/Bq1yNkOdX9tHbZZ+DBGJt3z3r7jeb4Digv8xQUk6gdTFC9vnGHuin+KW3/yD1Aww==} cpu: [x64] os: [linux] - '@oven/bun-windows-aarch64@1.3.14': - resolution: {integrity: sha512-T7s3x/BsVKQObGU6QDkZeI6wKynzqGbBH1yI77jrrj5siElclxr3DQrDIk8CV4G5/SJq2HHq4kpLyYY2DKCSmA==} - cpu: [arm64] - os: [win32] - - '@oven/bun-windows-x64-baseline@1.3.14': - resolution: {integrity: sha512-uIjLUC1S9DWgICzuoMba7vurBJnBruE4S5CxnvmZkdqWVXRzx1Rgu636HoH+k0qeaQCFh3jeG3JQ1y6fRHv0sw==} + '@oven/bun-windows-x64-baseline@1.3.0': + resolution: {integrity: sha512-/jVZ8eYjpYHLDFNoT86cP+AjuWvpkzFY+0R0a1bdeu0sQ6ILuy1FV6hz1hUAP390E09VCo5oP76fnx29giHTtA==} cpu: [x64] os: [win32] - '@oven/bun-windows-x64@1.3.14': - resolution: {integrity: sha512-mUFWL3BoYkNpjd8e9PqROiFF/1Xeotq20mABJsiQH62jM1g5zqWh4khw1RZ6bX8Q8fWvlPaxG1PjofkmjUi3vg==} + '@oven/bun-windows-x64@1.3.0': + resolution: {integrity: sha512-xXwtpZVVP7T+vkxcF/TUVVOGRjEfkByO4mKveKYb4xnHWV4u4NnV0oNmzyMKkvmj10to5j2h0oZxA4ZVVv4gfA==} cpu: [x64] os: [win32] - '@oxc-project/types@0.142.0': - resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} - - '@parcel/watcher-android-arm64@2.6.0': - resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + '@parcel/watcher-android-arm64@2.5.0': + resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [android] - '@parcel/watcher-darwin-arm64@2.6.0': - resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.0': + resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [darwin] - '@parcel/watcher-darwin-x64@2.6.0': - resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.0': + resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [darwin] - '@parcel/watcher-freebsd-x64@2.6.0': - resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.0': + resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [freebsd] - '@parcel/watcher-linux-arm-glibc@2.6.0': - resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.0': + resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] - '@parcel/watcher-linux-arm-musl@2.6.0': - resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] - '@parcel/watcher-linux-arm64-glibc@2.6.0': - resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + '@parcel/watcher-linux-arm-musl@2.5.0': + resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - '@parcel/watcher-linux-arm64-musl@2.6.0': - resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - '@parcel/watcher-linux-x64-glibc@2.6.0': - resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + '@parcel/watcher-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.0': + resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - '@parcel/watcher-linux-x64-musl@2.6.0': - resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - '@parcel/watcher-wasm@2.5.6': - resolution: {integrity: sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA==} + '@parcel/watcher-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-wasm@2.5.0': + resolution: {integrity: sha512-Z4ouuR8Pfggk1EYYbTaIoxc+Yv4o7cGQnH0Xy8+pQ+HbiW+ZnwhcD2LPf/prfq1nIWpAxjOkQ8uSMFWMtBLiVQ==} engines: {node: '>= 10.0.0'} bundledDependencies: - napi-wasm - '@parcel/watcher-win32-arm64@2.6.0': - resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + '@parcel/watcher-win32-arm64@2.5.0': + resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [win32] - '@parcel/watcher-win32-x64@2.6.0': - resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.0': + resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.0': + resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [win32] - '@parcel/watcher@2.6.0': - resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.0': + resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==} engines: {node: '>= 10.0.0'} - '@playwright/test@1.62.1': - resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} - engines: {node: '>=20'} + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@playwright/test@1.56.1': + resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} + engines: {node: '>=18'} hasBin: true - '@rolldown/binding-android-arm64@1.2.1': - resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] + '@rolldown/pluginutils@1.0.0-beta.43': + resolution: {integrity: sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==} - '@rolldown/binding-darwin-arm64@1.2.1': - resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.2.1': - resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.2.1': - resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.2.1': - resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.2.1': - resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.2.1': - resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.2.1': - resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.2.1': - resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.2.1': - resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.2.1': - resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.2.1': - resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.2.1': - resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - - '@rolldown/binding-win32-arm64-msvc@1.2.1': - resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.2.1': - resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@rollup/rollup-android-arm-eabi@4.60.4': - resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + '@rollup/rollup-android-arm-eabi@4.34.9': + resolution: {integrity: sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.4': - resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + '@rollup/rollup-android-arm-eabi@4.44.0': + resolution: {integrity: sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.34.9': + resolution: {integrity: sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.4': - resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + '@rollup/rollup-android-arm64@4.44.0': + resolution: {integrity: sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.34.9': + resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.4': - resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + '@rollup/rollup-darwin-arm64@4.44.0': + resolution: {integrity: sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.34.9': + resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.4': - resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + '@rollup/rollup-darwin-x64@4.44.0': + resolution: {integrity: sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.34.9': + resolution: {integrity: sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.4': - resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + '@rollup/rollup-freebsd-arm64@4.44.0': + resolution: {integrity: sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.34.9': + resolution: {integrity: sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + '@rollup/rollup-freebsd-x64@4.44.0': + resolution: {integrity: sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': + resolution: {integrity: sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + '@rollup/rollup-linux-arm-gnueabihf@4.44.0': + resolution: {integrity: sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==} cpu: [arm] os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.4': - resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + '@rollup/rollup-linux-arm-musleabihf@4.34.9': + resolution: {integrity: sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.44.0': + resolution: {integrity: sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.34.9': + resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==} cpu: [arm64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.4': - resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + '@rollup/rollup-linux-arm64-gnu@4.44.0': + resolution: {integrity: sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==} cpu: [arm64] os: [linux] - libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.4': - resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + '@rollup/rollup-linux-arm64-musl@4.34.9': + resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.44.0': + resolution: {integrity: sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': + resolution: {integrity: sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==} cpu: [loong64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.4': - resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + '@rollup/rollup-linux-loongarch64-gnu@4.44.0': + resolution: {integrity: sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==} cpu: [loong64] os: [linux] - libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': + resolution: {integrity: sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==} cpu: [ppc64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.4': - resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + '@rollup/rollup-linux-powerpc64le-gnu@4.44.0': + resolution: {integrity: sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==} cpu: [ppc64] os: [linux] - libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + '@rollup/rollup-linux-riscv64-gnu@4.34.9': + resolution: {integrity: sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==} cpu: [riscv64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.4': - resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + '@rollup/rollup-linux-riscv64-gnu@4.44.0': + resolution: {integrity: sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==} cpu: [riscv64] os: [linux] - libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.4': - resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + '@rollup/rollup-linux-riscv64-musl@4.44.0': + resolution: {integrity: sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.34.9': + resolution: {integrity: sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==} cpu: [s390x] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.4': - resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + '@rollup/rollup-linux-s390x-gnu@4.44.0': + resolution: {integrity: sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.34.9': + resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==} cpu: [x64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.4': - resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + '@rollup/rollup-linux-x64-gnu@4.44.0': + resolution: {integrity: sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==} cpu: [x64] os: [linux] - libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.4': - resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + '@rollup/rollup-linux-x64-musl@4.34.9': + resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==} cpu: [x64] - os: [openbsd] + os: [linux] - '@rollup/rollup-openharmony-arm64@4.60.4': - resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} - cpu: [arm64] - os: [openharmony] + '@rollup/rollup-linux-x64-musl@4.44.0': + resolution: {integrity: sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==} + cpu: [x64] + os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.60.4': - resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + '@rollup/rollup-win32-arm64-msvc@4.34.9': + resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.4': - resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + '@rollup/rollup-win32-arm64-msvc@4.44.0': + resolution: {integrity: sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.34.9': + resolution: {integrity: sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.4': - resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.4': - resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} - cpu: [x64] - os: [win32] - - '@rspack/binding-darwin-arm64@2.0.2': - resolution: {integrity: sha512-0o7lbgBBsDlICWdjIH0q3e0BsSco4GRiImHWVfZSVEG+q2+ykZJvSvYCVhPM1Co375Z0S3VMPa/8SjcY1FHwlw==} - cpu: [arm64] - os: [darwin] - - '@rspack/binding-darwin-x64@2.0.2': - resolution: {integrity: sha512-tOwxZpoPlTlRs/w6UyUinXJ4TYRVHMlR7+eQxO1R3muKpixvhXQjtvoaY16HuFyTVky5F0IfOoWr3x9FEsgdLg==} - cpu: [x64] - os: [darwin] - - '@rspack/binding-linux-arm64-gnu@2.0.2': - resolution: {integrity: sha512-1ZD4YFhG1rmgqj+W8hfwHyKV8xDxGsc/3KgU0FwmiVEX7JfzhCkgBO/xlCG79kRKSrzuVzt4icO/G3cCKn0pag==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rspack/binding-linux-arm64-musl@2.0.2': - resolution: {integrity: sha512-/PtTkM/DsDLjeuXTmeJeRfbjCDbcL9jvoVgZrgxYFZ28y2cdLvbChbW9uigOzs5dQEs1CIBQXMTTj7KhdBTuQg==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rspack/binding-linux-x64-gnu@2.0.2': - resolution: {integrity: sha512-bBjsZxMHRaPo6X9SokApm6ucs+UhXtAJFyJJyuk2BH4XJsLeCU9Dz1vMwioeohFbJUUeTASVPm6/BL+RhSaunw==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rspack/binding-linux-x64-musl@2.0.2': - resolution: {integrity: sha512-HjlpInqzabDNkhVsUJpsHPqa9QYVWBViJoyWNjzXCAW0vKMDvwaphyUvokSinX8FGTlZi/sr5UEaHJo6XtQ35g==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rspack/binding-wasm32-wasi@2.0.2': - resolution: {integrity: sha512-YaRYNFLJRpkGfYjSWR7n9f+nQKtrlmrrffpAn/blc2geHcRvXoBc5SCs1idPtsLhj7H9qWWhs7ucjyHy4csWFg==} - cpu: [wasm32] - - '@rspack/binding-win32-arm64-msvc@2.0.2': - resolution: {integrity: sha512-d/3kTEKq+asLjRFPO96t+wfWiM7DLN76VQEPDD9bc1kdsZXlVJBuvyXfsgK8bbEvKplWXYcSsokhmEnuXrLOpg==} - cpu: [arm64] - os: [win32] - - '@rspack/binding-win32-ia32-msvc@2.0.2': - resolution: {integrity: sha512-161cWineq3RW+Jdm1FAfSpXeUtYWvhB3kAbm46vNT9h/YYz+spwsFMvveAZ1nsVSVL0IC5lDBGUte7yUAY8K2g==} + '@rollup/rollup-win32-ia32-msvc@4.44.0': + resolution: {integrity: sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==} cpu: [ia32] os: [win32] - '@rspack/binding-win32-x64-msvc@2.0.2': - resolution: {integrity: sha512-y7Q0S1FE+OlkL5GMqLG0PwxrPw6E1r892KhGrGKE1Vdufe5YTEx6xTPxzZ+b7N2KPD7s9G1/iJmWHQxb1+Bjkg==} + '@rollup/rollup-win32-x64-msvc@4.34.9': + resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==} cpu: [x64] os: [win32] - '@rspack/binding@2.0.2': - resolution: {integrity: sha512-0kZPplW9GWx8mfC6DfsaRY3QBIYPuUs42JfmSM6aSb8tMHZAXQeLeMB8M+h8i4SeI+aFtCgO6UuYGtyWf7+L+A==} + '@rollup/rollup-win32-x64-msvc@4.44.0': + resolution: {integrity: sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==} + cpu: [x64] + os: [win32] - '@rspack/core@2.0.2': - resolution: {integrity: sha512-VM3UHOo26uC+4QSqY5tU1ybI7KuXY5rTof8nhFOaBY9SYau0Smvr+hMSAPmrmHwknB6dXT8yaNVxrj7I+qxE1Q==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 - '@swc/helpers': '>=0.5.1' - peerDependenciesMeta: - '@module-federation/runtime-tools': - optional: true - '@swc/helpers': - optional: true + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.10.4': + resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -2114,200 +2381,183 @@ packages: peerDependencies: tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' - '@tailwindcss/forms@0.5.11': - resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} + '@tailwindcss/forms@0.5.10': + resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - '@tailwindcss/typography@0.5.20': - resolution: {integrity: sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==} + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} peerDependencies: - tailwindcss: '>=3.0.0 || >=4.0.0 || insiders' + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@turbo/darwin-64@2.10.8': - resolution: {integrity: sha512-po+7rfJfUnFXjWlcoN2RwhErgzCdRtBc1T26vYPcywHlggmCQiQe1uWaE4j+BibI2uY9/2pDoFzMN0rmSaPFOw==} - cpu: [x64] - os: [darwin] + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@turbo/darwin-arm64@2.10.8': - resolution: {integrity: sha512-+zB2btDJ00lnPRuqOvpVvgl4x34k/djZQGZTTCfjn7JgNCl8QFY5Njo5+dqkY1g/+9gbbsnAvWm9CmJg9ebcXA==} - cpu: [arm64] - os: [darwin] + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - '@turbo/linux-64@2.10.8': - resolution: {integrity: sha512-K1dxqiVisyN7cViVsfQLs6xscQbYuI8aO2nbUhFURDACgEDfZRdP/b4CCxeosBJpcMfhYyiibWqJorCnvz9kKg==} - cpu: [x64] - os: [android, linux] + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} - '@turbo/linux-arm64@2.10.8': - resolution: {integrity: sha512-Gi77ibVnrE1fEmvr+/wBD/yvRqhwp/RQuCp2+//lv1U1wNFFyVg0V7Wj8FG9FXPFAw5QHReo8rxc9+wBSDZjzA==} - cpu: [arm64] - os: [android, linux] + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - '@turbo/windows-64@2.10.8': - resolution: {integrity: sha512-znnLO1haJPYTHoKMKwlAvlkjRiYbbhBzME6wIGaMd+fwir23U6jVd1ecaTWWi1fbnRVqxMfgDBKseQ/hLKb83g==} - cpu: [x64] - os: [win32] + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - '@turbo/windows-arm64@2.10.8': - resolution: {integrity: sha512-VN30vh3b3Czh2WzYHNTfF1FE0YMZ5aHsLO8dBMGHJewA6792wX6iJR8ZxlzFW6WdOu0gEAKIvlYhfyT81Wkm4Q==} - cpu: [arm64] - os: [win32] + '@types/braces@3.0.5': + resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/bun@1.3.0': + resolution: {integrity: sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA==} - '@types/bun@1.3.14': - resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.20.1': - resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@25.9.1': - resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/node@20.14.13': + resolution: {integrity: sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==} + + '@types/node@20.19.1': + resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==} '@types/postcss-import@14.0.3': resolution: {integrity: sha512-raZhRVTf6Vw5+QbmQ7LOHSDML71A5rj4+EqDzAbrZPfxfoGzFxMHRCq16VlddGIZpHELw0BG4G0YE2ANkdZiIQ==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.2': + resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.15': - resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + '@types/react@19.2.2': + resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} - '@types/semver@7.8.0': - resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@vitejs/plugin-react@6.0.2': - resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + '@typescript-eslint/eslint-plugin@8.11.0': + resolution: {integrity: sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.11.0': + resolution: {integrity: sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.11.0': + resolution: {integrity: sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.11.0': + resolution: {integrity: sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.11.0': + resolution: {integrity: sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.11.0': + resolution: {integrity: sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.11.0': + resolution: {integrity: sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@8.11.0': + resolution: {integrity: sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@5.1.0': + resolution: {integrity: sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/runner@2.0.5': + resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} + + '@vitest/snapshot@2.0.5': + resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} + + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - - '@webassemblyjs/ast@1.14.1': - resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - - '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - - '@webassemblyjs/helper-api-error@1.13.2': - resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - - '@webassemblyjs/helper-buffer@1.14.1': - resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - - '@webassemblyjs/helper-numbers@1.13.2': - resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - - '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - - '@webassemblyjs/ieee754@1.13.2': - resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - - '@webassemblyjs/leb128@1.13.2': - resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - - '@webassemblyjs/utf8@1.13.2': - resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - - '@webassemblyjs/wasm-edit@1.14.1': - resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - - '@webassemblyjs/wasm-gen@1.14.1': - resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - - '@webassemblyjs/wasm-opt@1.14.1': - resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - - '@webassemblyjs/wasm-parser@1.14.1': - resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - - '@webassemblyjs/wast-printer@1.14.1': - resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - - '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - - '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true - ajv-keywords@5.1.0: - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.8.2 + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -2322,21 +2572,70 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - autoprefixer@10.5.0: - resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 - baseline-browser-mapping@2.10.31: - resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} - engines: {node: '>=6.0.0'} - hasBin: true + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.10.2: + resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} @@ -2345,25 +2644,33 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - bun-types@1.3.14: - resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} + bun-types@1.3.0: + resolution: {integrity: sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ==} + peerDependencies: + '@types/react': ^19 - bun@1.3.14: - resolution: {integrity: sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg==} + bun@1.3.0: + resolution: {integrity: sha512-YI7mFs7iWc/VsGsh2aw6eAPD2cjzn1j+LKdYVk09x1CrdTWKYIHyd+dG5iQoN9//3hCDoZj8U6vKpZzEf5UARA==} cpu: [arm64, x64] - os: [darwin, linux, android, freebsd, win32] + os: [darwin, linux, win32] hasBin: true bundle-require@5.1.0: @@ -2376,19 +2683,35 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001793: - resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + caniuse-lite@1.0.30001705: + resolution: {integrity: sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==} - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} - chardet@2.1.1: - resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chardet@2.1.0: + resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} @@ -2398,12 +2721,8 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chrome-trace-event@1.0.4: - resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} - engines: {node: '>=6.0'} - - citty@0.2.2: - resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} @@ -2417,6 +2736,24 @@ packages: peerDependencies: typanion: '*' + clipboardy@4.0.0: + resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} + engines: {node: '>=18'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -2427,41 +2764,84 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} + consola@3.4.0: + resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} + engines: {node: ^14.18.0 || >=16.10.0} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-es@1.2.3: - resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crossws@0.3.1: + resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==} crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - crossws@0.4.10: - resolution: {integrity: sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA==} - peerDependencies: - srvx: '>=0.11.5' - peerDependenciesMeta: - srvx: - optional: true - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -2472,16 +2852,31 @@ packages: supports-color: optional: true - dedent@1.7.2: - resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + dedent@1.7.0: + resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: babel-plugin-macros: optional: true - defu@6.1.7: - resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -2491,8 +2886,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} didyoumean@1.2.2: @@ -2501,33 +2896,80 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - electron-to-chromium@1.5.361: - resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} - emnapi@1.11.3: - resolution: {integrity: sha512-+/ZS90YK/rYfVOHtGLHkGffVsnmD/MAKaBHio+Y4XAtg75RLr4cveV/w0jTkUdLM1CcAlaRgG76mpIemWAlk0A==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.119: + resolution: {integrity: sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==} + + emnapi@1.6.0: + resolution: {integrity: sha512-Hx0FqhPchuwkDauQdgamWAn6MHbOX1CPIFfObXqxTEmngZUYM9mZLcPS/civeVT5x73xkBV1ZIwKWfessIFTgQ==} peerDependencies: node-addon-api: '>= 6.1.0' peerDependenciesMeta: node-addon-api: optional: true - enhanced-resolve@5.24.5: - resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} + es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-iterator-helpers@1.1.0: + resolution: {integrity: sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==} + engines: {node: '>= 0.4'} - es-toolkit@1.50.0: - resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.39.10: + resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.25.0: + resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} engines: {node: '>=18'} hasBin: true @@ -2535,18 +2977,118 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@15.5.6: + resolution: {integrity: sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.6.3: + resolution: {integrity: sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.1: + resolution: {integrity: sha512-zHByM9WTUMnfsDTafGXRiqxp6lFtNoSOWBY6FonVRn3A+BUwN1L/tdBXT40BcBJi0cZjOGTXZ0eD/rTG9fEJ0g==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.0.0: + resolution: {integrity: sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.2: + resolution: {integrity: sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.37.0: + resolution: {integrity: sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} - estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -2554,13 +3096,13 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -2568,54 +3110,66 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-equals@5.4.0: - resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} - engines: {node: '>=6.0.0'} + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-stringify@4.0.0: - resolution: {integrity: sha512-lE2DIivBaLysf6hK5WH/VfMgqRbvBVHcpGVVTmA5Zi8oWIjq9YxIt6lYGdUgP1HNSXxTIat7HEIDnrSvXSeKQw==} + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - - fast-wrap-ansi@0.2.2: - resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - find-up-simple@1.0.1: - resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} engines: {node: '>=18'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} @@ -2630,8 +3184,37 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-port-please@3.2.0: - resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-port-please@3.1.2: + resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2641,185 +3224,420 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - globby@16.2.2: - resolution: {integrity: sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@15.0.0: + resolution: {integrity: sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==} engines: {node: '>=20'} + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - h3@1.15.11: - resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + h3@1.15.4: + resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} + is-bun-module@1.2.1: + resolution: {integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.0.2: + resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@4.0.0: - resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} - engines: {node: '>=12'} + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} - jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + + is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + is-weakset@2.0.3: + resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} + engines: {node: '>= 0.4'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + is64bit@2.0.0: + resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} + engines: {node: '>=18'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.3: + resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==} + engines: {node: '>= 0.4'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true - jiti@2.7.0: - resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-yaml@4.3.1: - resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true - json-with-bigint@3.5.8: - resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.2: + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} engines: {node: '>=14'} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - listhen@1.10.1: - resolution: {integrity: sha512-6nt/86SkqUQSLW1ofz8MxC6RhRMqOl3ONISe6qqvJ3xj09aJWQx6DhgSZpugs3PX4PXdOas/WD6A9jx6J2N19A==} + listhen@1.9.0: + resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} hasBin: true - peerDependencies: - '@parcel/watcher': ^2.5.6 - peerDependenciesMeta: - '@parcel/watcher': - optional: true load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magic-string@1.1.0: - resolution: {integrity: sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g==} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2827,97 +3645,78 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micro-memoize@5.1.1: - resolution: {integrity: sha512-QDwluos8YeMijiKxZGwaV4f4tzj0soS6+xcsJhJ3+4wdEIHMyKbIKVUziebOgWX3e6yiijdoaHo+9tyhbnaWXA==} + micro-memoize@4.1.3: + resolution: {integrity: sha512-DzRMi8smUZXT7rCGikRwldEh6eO6qzKiPPopcr1+2EY3AYKpy5fu159PKWwIS9A6IWnrvPKDMcuFtyrroZa8Bw==} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} mini-svg-data-uri@1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true - minimizer-webpack-plugin@5.6.1: - resolution: {integrity: sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@minify-html/node': '*' - '@swc/core': '*' - '@swc/css': '*' - '@swc/html': '*' - clean-css: '*' - cssnano: '*' - csso: '*' - esbuild: '*' - html-minifier-terser: '*' - lightningcss: '*' - postcss: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@minify-html/node': - optional: true - '@swc/core': - optional: true - '@swc/css': - optional: true - '@swc/html': - optional: true - clean-css: - optional: true - cssnano: - optional: true - csso: - optional: true - esbuild: - optional: true - html-minifier-terser: - optional: true - lightningcss: - optional: true - postcss: - optional: true - uglify-js: - optional: true + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.7.3: + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mute-stream@3.0.0: - resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} - engines: {node: ^20.17.0 || >=22.9.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@16.2.7: - resolution: {integrity: sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==} - engines: {node: '>=20.9.0'} + next@15.5.6: + resolution: {integrity: sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -2939,29 +3738,44 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-addon-api@8.7.0: - resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} + node-addon-api@8.2.2: + resolution: {integrity: sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==} engines: {node: ^18 || ^20 || >= 21} - node-forge@1.4.0: - resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + node-addon-api@8.3.0: + resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==} + engines: {node: ^18 || ^20 || >= 21} + + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} + node-gyp-build@4.8.2: + resolution: {integrity: sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==} + hasBin: true + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-mock-http@1.0.4: - resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + node-mock-http@1.0.2: + resolution: {integrity: sha512-zWaamgDUdo9SSLw47we78+zYw/bDr5gH8pH7oRRs8V3KmBtu8GLgGIbV2p/gRPd3LWpEOpjQj7X1FOU3VFMJ8g==} - node-releases@2.0.46: - resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} - engines: {node: '>=18'} + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2970,59 +3784,130 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} - engines: {node: '>=12.20.0'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.entries@1.1.8: + resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} package-up@5.0.0: resolution: {integrity: sha512-MQEgDUvXCa3sGvqHg3pzHO8e9gqTCMPVrWUko3vPQGntwegmFo52mZb2abIVTjFnUcW0BcPz0D93jV5Cas1DWA==} engines: {node: '>=18'} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@6.0.0: + resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} + engines: {node: '>=18'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-types@1.3.0: + resolution: {integrity: sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==} - playwright-core@1.62.1: - resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} - engines: {node: '>=20'} + playwright-core@1.56.1: + resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + engines: {node: '>=18'} hasBin: true - playwright@1.62.1: - resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} - engines: {node: '>=20'} + playwright@1.56.1: + resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + engines: {node: '>=18'} hasBin: true + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -3035,12 +3920,24 @@ packages: peerDependencies: postcss: ^8.0.0 - postcss-js@4.1.0: - resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: postcss: ^8.4.21 + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -3073,8 +3970,8 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss-selector-parser@7.1.4: - resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} engines: {node: '>=4'} postcss-value-parser@4.2.0: @@ -3084,16 +3981,24 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.16: - resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + postcss@8.4.41: + resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + postcss@8.4.47: + resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} - prettier-plugin-embed@0.5.1: - resolution: {integrity: sha512-2Ege8gIlLNTvHElUeU5XcFsD7/dbDXkQA6H9TczHSJAGxB58nNjjifk/dlRMS5E29eqQx/z+ToA4ZVMWzjME/A==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-embed@0.5.0: + resolution: {integrity: sha512-A5nzX8U9x+FJdpOKrDrH9eq86xHZNiGguWpphS6chTME0OK1bDgH1X+WLtZq7qV3kUEMkL/dHkr6C1NLdUA7RQ==} prettier-plugin-organize-imports@4.3.0: resolution: {integrity: sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==} @@ -3105,33 +4010,38 @@ packages: vue-tsc: optional: true - prettier@3.9.6: - resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} hasBin: true + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - react-dom@19.2.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: - react: ^19.2.6 + react: ^19.2.0 - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react@19.2.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -3141,59 +4051,101 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + readdirp@4.1.1: + resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} engines: {node: '>= 14.18.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} + reflect.getprototypeof@1.0.6: + resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.2.1: - resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} - engines: {node: ^20.19.0 || >=22.12.0} + rollup@4.34.9: + resolution: {integrity: sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rollup@4.60.4: - resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + rollup@4.44.0: + resolution: {integrity: sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - schema-utils@4.3.3: - resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} - engines: {node: '>= 10.13.0'} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true - semver@7.8.5: - resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3201,6 +4153,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + slash@5.1.0: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} @@ -3216,18 +4171,66 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.1.0: - resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} - std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} @@ -3242,34 +4245,34 @@ packages: babel-plugin-macros: optional: true - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} - engines: {node: '>=20'} + system-architecture@0.1.0: + resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} + engines: {node: '>=18'} - tailwindcss@3.4.19: - resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + tailwindcss@3.4.14: + resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==} engines: {node: '>=14.0.0'} hasBin: true - tapable@2.3.3: - resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} - terser@5.48.0: - resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + terser@5.31.6: + resolution: {integrity: sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==} engines: {node: '>=10'} hasBin: true @@ -3286,33 +4289,36 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyclip@0.1.15: - resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==} - engines: {node: ^16.14.0 || >= 17.3.0} - tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} - engines: {node: '>=18'} - - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + tinypool@1.0.0: + resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} engines: {node: '>=14.0.0'} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -3333,17 +4339,26 @@ packages: tree-sitter: optional: true - tree-sitter@0.25.1: - resolution: {integrity: sha512-mrcEdkYtHfrK1A6fs3O6FxkBo0Qig5XUXqHhxUOQu0bmPo00QF4XaSx4edpazdHwxnSCjlGKGgIqWdaN4dvTLA==} + tree-sitter@0.22.4: + resolution: {integrity: sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -3361,72 +4376,171 @@ packages: typescript: optional: true - turbo@2.10.8: - resolution: {integrity: sha512-9+8YX5QOkGXzZxcIykTHgaooRHGMWO+jfdyRK0o+rN0U7hBIig2MrJ8r/aNzIPDPhdA73SGb0O+tIztaModTMg==} + tsx@4.19.1: + resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} + engines: {node: '>=18.0.0'} + hasBin: true + + turbo-darwin-64@2.5.4: + resolution: {integrity: sha512-ah6YnH2dErojhFooxEzmvsoZQTMImaruZhFPfMKPBq8sb+hALRdvBNLqfc8NWlZq576FkfRZ/MSi4SHvVFT9PQ==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.5.4: + resolution: {integrity: sha512-2+Nx6LAyuXw2MdXb7pxqle3MYignLvS7OwtsP9SgtSBaMlnNlxl9BovzqdYAgkUW3AsYiQMJ/wBRb7d+xemM5A==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.5.4: + resolution: {integrity: sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.5.4: + resolution: {integrity: sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.5.4: + resolution: {integrity: sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.5.4: + resolution: {integrity: sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A==} + cpu: [arm64] + os: [win32] + + turbo@2.5.4: + resolution: {integrity: sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==} hasBin: true typanion@3.14.0: resolution: {integrity: sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==} - type-fest@5.6.0: - resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} - engines: {node: '>=20'} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + type-fest@4.36.0: + resolution: {integrity: sha512-3T/PUdKTCnkUmhQU6FFJEHsLwadsRegktX3TNHk+2JJB9HlA8gp1/VXblXVDI93kSnXF2rdPx0GMbHtJIV2LPg==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} - unicorn-magic@0.4.0: - resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} - engines: {node: '>=20'} + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} - universal-user-agent@7.0.3: - resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} - - untun@0.2.2: - resolution: {integrity: sha512-+NnOJcSiEtYsVgJmXUzQbJeRAFXJC4yPJYuh6kF9B0Rm6zunXcs/3GZOTllyocSbUDIxD6Bj7e/4ATw7sph1Sw==} + untun@0.1.3: + resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} hasBin: true - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - uqr@0.1.3: - resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} + uqr@0.1.2: + resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vite@8.2.0: - resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + vite-node@2.0.5: + resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.0: + resolution: {integrity: sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@7.0.0: + resolution: {integrity: sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.0 - esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 + lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -3437,14 +4551,12 @@ packages: peerDependenciesMeta: '@types/node': optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true jiti: optional: true less: optional: true + lightningcss: + optional: true sass: optional: true sass-embedded: @@ -3460,39 +4572,23 @@ packages: yaml: optional: true - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@2.0.5: + resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.0.5 + '@vitest/ui': 2.0.5 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@opentelemetry/api': - optional: true '@types/node': optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': + '@vitest/browser': optional: true '@vitest/ui': optional: true @@ -3501,419 +4597,764 @@ packages: jsdom: optional: true - watchpack@2.5.2: - resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} - engines: {node: '>=10.13.0'} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - webpack-sources@3.5.1: - resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} - engines: {node: '>=10.13.0'} + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - webpack@5.109.2: - resolution: {integrity: sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==} - engines: {node: '>=10.13.0'} + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + + which-builtin-type@1.1.4: + resolution: {integrity: sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + engines: {node: '>= 14'} hasBin: true + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + snapshots: '@alloc/quick-lru@5.2.0': {} - '@emnapi/core@1.10.0': + '@ampproject/remapping@2.3.0': dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 - '@emnapi/core@1.11.3': + '@babel/code-frame@7.27.1': dependencies: - '@emnapi/wasi-threads': 1.2.3 + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.5': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.27.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@emnapi/core@1.5.0': + dependencies: + '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 - '@emnapi/core@2.0.0-alpha.3': + '@emnapi/core@1.6.0': dependencies: - '@emnapi/wasi-threads': 2.0.1 + '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.3': + '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 - '@emnapi/runtime@2.0.0-alpha.3': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.3': + '@emnapi/runtime@1.6.0': dependencies: tslib: 2.8.1 - '@emnapi/wasi-threads@2.0.1': + '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 + + '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.27.7': + '@esbuild/aix-ppc64@0.23.1': optional: true - '@esbuild/android-arm64@0.27.7': + '@esbuild/aix-ppc64@0.25.0': optional: true - '@esbuild/android-arm@0.27.7': + '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-x64@0.27.7': + '@esbuild/android-arm64@0.23.1': optional: true - '@esbuild/darwin-arm64@0.27.7': + '@esbuild/android-arm64@0.25.0': optional: true - '@esbuild/darwin-x64@0.27.7': + '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.27.7': + '@esbuild/android-arm@0.23.1': optional: true - '@esbuild/freebsd-x64@0.27.7': + '@esbuild/android-arm@0.25.0': optional: true - '@esbuild/linux-arm64@0.27.7': + '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/linux-arm@0.27.7': + '@esbuild/android-x64@0.23.1': optional: true - '@esbuild/linux-ia32@0.27.7': + '@esbuild/android-x64@0.25.0': optional: true - '@esbuild/linux-loong64@0.27.7': + '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/linux-mips64el@0.27.7': + '@esbuild/darwin-arm64@0.23.1': optional: true - '@esbuild/linux-ppc64@0.27.7': + '@esbuild/darwin-arm64@0.25.0': optional: true - '@esbuild/linux-riscv64@0.27.7': + '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/linux-s390x@0.27.7': + '@esbuild/darwin-x64@0.23.1': optional: true - '@esbuild/linux-x64@0.27.7': + '@esbuild/darwin-x64@0.25.0': optional: true - '@esbuild/netbsd-arm64@0.27.7': + '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.27.7': + '@esbuild/freebsd-arm64@0.23.1': optional: true - '@esbuild/openbsd-arm64@0.27.7': + '@esbuild/freebsd-arm64@0.25.0': optional: true - '@esbuild/openbsd-x64@0.27.7': + '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/openharmony-arm64@0.27.7': + '@esbuild/freebsd-x64@0.23.1': optional: true - '@esbuild/sunos-x64@0.27.7': + '@esbuild/freebsd-x64@0.25.0': optional: true - '@esbuild/win32-arm64@0.27.7': + '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/win32-ia32@0.27.7': + '@esbuild/linux-arm64@0.23.1': optional: true - '@esbuild/win32-x64@0.27.7': + '@esbuild/linux-arm64@0.25.0': optional: true - '@img/colour@1.1.0': + '@esbuild/linux-arm@0.21.5': optional: true - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@esbuild/linux-arm@0.23.1': optional: true - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@esbuild/linux-arm@0.25.0': optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@esbuild/linux-ia32@0.21.5': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@esbuild/linux-ia32@0.23.1': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@esbuild/linux-ia32@0.25.0': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@esbuild/linux-loong64@0.21.5': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@esbuild/linux-loong64@0.23.1': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@esbuild/linux-loong64@0.25.0': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@esbuild/linux-mips64el@0.21.5': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@esbuild/linux-mips64el@0.23.1': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@esbuild/linux-mips64el@0.25.0': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@esbuild/linux-ppc64@0.21.5': optional: true - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@esbuild/linux-ppc64@0.23.1': optional: true - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@esbuild/linux-ppc64@0.25.0': optional: true - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@esbuild/linux-riscv64@0.21.5': optional: true - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@esbuild/linux-riscv64@0.23.1': optional: true - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@esbuild/linux-riscv64@0.25.0': optional: true - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@esbuild/linux-s390x@0.21.5': optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@esbuild/linux-s390x@0.23.1': optional: true - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@esbuild/linux-s390x@0.25.0': optional: true - '@img/sharp-wasm32@0.34.5': + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/linux-x64@0.25.0': + optional: true + + '@esbuild/netbsd-arm64@0.25.0': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.25.0': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.25.0': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.25.0': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.25.0': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.25.0': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.25.0': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + + '@esbuild/win32-x64@0.25.0': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0(jiti@2.6.1))': dependencies: - '@emnapi/runtime': 1.11.3 - optional: true + eslint: 9.37.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 - '@img/sharp-win32-arm64@0.34.5': - optional: true + '@eslint-community/regexpp@4.12.1': {} - '@img/sharp-win32-ia32@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true - - '@inquirer/ansi@2.0.7': {} - - '@inquirer/checkbox@5.2.1(@types/node@25.9.1)': + '@eslint/config-array@0.21.0': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@eslint/object-schema': 2.1.6 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.0': + dependencies: + '@eslint/core': 0.16.0 + + '@eslint/core@0.16.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.37.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.4.0': + dependencies: + '@eslint/core': 0.16.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.2': {} + + '@img/sharp-darwin-arm64@0.34.3': optionalDependencies: - '@types/node': 25.9.1 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + optional: true - '@inquirer/confirm@6.1.1(@types/node@25.9.1)': - dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@img/sharp-darwin-x64@0.34.3': optionalDependencies: - '@types/node': 25.9.1 + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true - '@inquirer/core@11.2.1(@types/node@25.9.1)': + '@img/sharp-libvips-darwin-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + optional: true + + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 + optional: true + + '@img/sharp-linux-arm@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.0 + optional: true + + '@img/sharp-linux-ppc64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.0 + optional: true + + '@img/sharp-linux-s390x@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.0 + optional: true + + '@img/sharp-linux-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + + '@img/sharp-wasm32@0.34.3': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@emnapi/runtime': 1.6.0 + optional: true + + '@img/sharp-win32-arm64@0.34.3': + optional: true + + '@img/sharp-win32-ia32@0.34.3': + optional: true + + '@img/sharp-win32-x64@0.34.3': + optional: true + + '@inquirer/ansi@1.0.0': {} + + '@inquirer/checkbox@4.2.4(@types/node@20.19.1)': + dependencies: + '@inquirer/ansi': 1.0.0 + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@20.19.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/confirm@5.1.18(@types/node@20.19.1)': + dependencies: + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + optionalDependencies: + '@types/node': 20.19.1 + + '@inquirer/core@10.2.2(@types/node@20.19.1)': + dependencies: + '@inquirer/ansi': 1.0.0 + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@20.19.1) cli-width: 4.1.0 - fast-wrap-ansi: 0.2.2 - mute-stream: 3.0.0 + mute-stream: 2.0.0 signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/editor@5.2.2(@types/node@25.9.1)': + '@inquirer/editor@4.2.20(@types/node@20.19.1)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/external-editor': 3.0.3(@types/node@25.9.1) - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/external-editor': 1.0.2(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/expand@5.1.1(@types/node@25.9.1)': + '@inquirer/expand@4.0.20(@types/node@20.19.1)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/external-editor@3.0.3(@types/node@25.9.1)': + '@inquirer/external-editor@1.0.2(@types/node@20.19.1)': dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 + chardet: 2.1.0 + iconv-lite: 0.7.0 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/figures@2.0.7': {} + '@inquirer/figures@1.0.13': {} - '@inquirer/input@5.1.2(@types/node@25.9.1)': + '@inquirer/input@4.2.4(@types/node@20.19.1)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/number@4.1.1(@types/node@25.9.1)': + '@inquirer/number@3.0.20(@types/node@20.19.1)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/password@5.1.1(@types/node@25.9.1)': + '@inquirer/password@4.0.20(@types/node@20.19.1)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/ansi': 1.0.0 + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/prompts@8.5.2(@types/node@25.9.1)': + '@inquirer/prompts@7.8.6(@types/node@20.19.1)': dependencies: - '@inquirer/checkbox': 5.2.1(@types/node@25.9.1) - '@inquirer/confirm': 6.1.1(@types/node@25.9.1) - '@inquirer/editor': 5.2.2(@types/node@25.9.1) - '@inquirer/expand': 5.1.1(@types/node@25.9.1) - '@inquirer/input': 5.1.2(@types/node@25.9.1) - '@inquirer/number': 4.1.1(@types/node@25.9.1) - '@inquirer/password': 5.1.1(@types/node@25.9.1) - '@inquirer/rawlist': 5.3.1(@types/node@25.9.1) - '@inquirer/search': 4.2.1(@types/node@25.9.1) - '@inquirer/select': 5.2.1(@types/node@25.9.1) + '@inquirer/checkbox': 4.2.4(@types/node@20.19.1) + '@inquirer/confirm': 5.1.18(@types/node@20.19.1) + '@inquirer/editor': 4.2.20(@types/node@20.19.1) + '@inquirer/expand': 4.0.20(@types/node@20.19.1) + '@inquirer/input': 4.2.4(@types/node@20.19.1) + '@inquirer/number': 3.0.20(@types/node@20.19.1) + '@inquirer/password': 4.0.20(@types/node@20.19.1) + '@inquirer/rawlist': 4.1.8(@types/node@20.19.1) + '@inquirer/search': 3.1.3(@types/node@20.19.1) + '@inquirer/select': 4.3.4(@types/node@20.19.1) optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/rawlist@5.3.1(@types/node@25.9.1)': + '@inquirer/rawlist@4.1.8(@types/node@20.19.1)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/type': 3.0.8(@types/node@20.19.1) + yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/search@4.2.1(@types/node@25.9.1)': + '@inquirer/search@3.1.3(@types/node@20.19.1)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@20.19.1) + yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/select@5.2.1(@types/node@25.9.1)': + '@inquirer/select@4.3.4(@types/node@20.19.1)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@25.9.1) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.1) + '@inquirer/ansi': 1.0.0 + '@inquirer/core': 10.2.2(@types/node@20.19.1) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@20.19.1) + yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@inquirer/type@4.0.7(@types/node@25.9.1)': + '@inquirer/type@3.0.8(@types/node@20.19.1)': optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 - '@jridgewell/gen-mapping@0.3.13': + '@isaacs/cliui@8.0.2': dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/source-map@0.3.11': + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + optional: true + + '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.31': + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/cli@3.7.4(patch_hash=9912bf0a9c2cef8329d11c41fbce4d33ce2bdcf660a630b258c962c0a3c44bed)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@25.9.1)(node-addon-api@8.7.0)': + '@jridgewell/trace-mapping@0.3.29': dependencies: - '@inquirer/prompts': 8.5.2(@types/node@25.9.1) - '@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - '@napi-rs/wasm-tools': 1.0.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - '@octokit/rest': 22.0.1 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@napi-rs/cli@3.4.1(@emnapi/runtime@1.6.0)(@types/node@20.19.1)(node-addon-api@8.3.0)': + dependencies: + '@inquirer/prompts': 7.8.6(@types/node@20.19.1) + '@napi-rs/cross-toolchain': 1.0.3 + '@napi-rs/wasm-tools': 1.0.1 + '@octokit/rest': 22.0.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) colorette: 2.0.20 - emnapi: 1.11.3(node-addon-api@8.7.0) - es-toolkit: 1.50.0 - js-yaml: 4.3.1 - obug: 2.1.4 - semver: 7.8.5 + debug: 4.4.3 + emnapi: 1.6.0(node-addon-api@8.3.0) + es-toolkit: 1.39.10 + js-yaml: 4.1.0 + semver: 7.7.3 typanion: 3.14.0 optionalDependencies: - '@emnapi/runtime': 1.11.3 + '@emnapi/runtime': 1.6.0 transitivePeerDependencies: - - '@emnapi/core' - '@napi-rs/cross-toolchain-arm64-target-aarch64' - '@napi-rs/cross-toolchain-arm64-target-armv7' - '@napi-rs/cross-toolchain-arm64-target-ppc64le' @@ -3928,14 +5369,12 @@ snapshots: - node-addon-api - supports-color - '@napi-rs/cross-toolchain@1.0.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + '@napi-rs/cross-toolchain@1.0.3': dependencies: - '@napi-rs/lzma': 1.4.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - '@napi-rs/tar': 1.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + '@napi-rs/lzma': 1.4.5 + '@napi-rs/tar': 1.1.0 debug: 4.4.3 transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - supports-color '@napi-rs/lzma-android-arm-eabi@1.4.5': @@ -3977,12 +5416,9 @@ snapshots: '@napi-rs/lzma-linux-x64-musl@1.4.5': optional: true - '@napi-rs/lzma-wasm32-wasi@1.4.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + '@napi-rs/lzma-wasm32-wasi@1.4.5': dependencies: - '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@napi-rs/wasm-runtime': 1.0.7 optional: true '@napi-rs/lzma-win32-arm64-msvc@1.4.5': @@ -3994,7 +5430,7 @@ snapshots: '@napi-rs/lzma-win32-x64-msvc@1.4.5': optional: true - '@napi-rs/lzma@1.4.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + '@napi-rs/lzma@1.4.5': optionalDependencies: '@napi-rs/lzma-android-arm-eabi': 1.4.5 '@napi-rs/lzma-android-arm64': 1.4.5 @@ -4009,13 +5445,10 @@ snapshots: '@napi-rs/lzma-linux-s390x-gnu': 1.4.5 '@napi-rs/lzma-linux-x64-gnu': 1.4.5 '@napi-rs/lzma-linux-x64-musl': 1.4.5 - '@napi-rs/lzma-wasm32-wasi': 1.4.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + '@napi-rs/lzma-wasm32-wasi': 1.4.5 '@napi-rs/lzma-win32-arm64-msvc': 1.4.5 '@napi-rs/lzma-win32-ia32-msvc': 1.4.5 '@napi-rs/lzma-win32-x64-msvc': 1.4.5 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' '@napi-rs/tar-android-arm-eabi@1.1.0': optional: true @@ -4053,12 +5486,9 @@ snapshots: '@napi-rs/tar-linux-x64-musl@1.1.0': optional: true - '@napi-rs/tar-wasm32-wasi@1.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + '@napi-rs/tar-wasm32-wasi@1.1.0': dependencies: - '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@napi-rs/wasm-runtime': 1.0.7 optional: true '@napi-rs/tar-win32-arm64-msvc@1.1.0': @@ -4070,7 +5500,7 @@ snapshots: '@napi-rs/tar-win32-x64-msvc@1.1.0': optional: true - '@napi-rs/tar@1.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + '@napi-rs/tar@1.1.0': optionalDependencies: '@napi-rs/tar-android-arm-eabi': 1.1.0 '@napi-rs/tar-android-arm64': 1.1.0 @@ -4084,33 +5514,16 @@ snapshots: '@napi-rs/tar-linux-s390x-gnu': 1.1.0 '@napi-rs/tar-linux-x64-gnu': 1.1.0 '@napi-rs/tar-linux-x64-musl': 1.1.0 - '@napi-rs/tar-wasm32-wasi': 1.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + '@napi-rs/tar-wasm32-wasi': 1.1.0 '@napi-rs/tar-win32-arm64-msvc': 1.1.0 '@napi-rs/tar-win32-ia32-msvc': 1.1.0 '@napi-rs/tar-win32-x64-msvc': 1.1.0 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.0.7': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': - dependencies: - '@emnapi/core': 1.11.3 - '@emnapi/runtime': 1.11.3 - '@tybys/wasm-util': 0.10.3 - - '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': - dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 - '@tybys/wasm-util': 0.10.3 - optional: true + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.1 '@napi-rs/wasm-tools-android-arm-eabi@1.0.1': optional: true @@ -4139,12 +5552,9 @@ snapshots: '@napi-rs/wasm-tools-linux-x64-musl@1.0.1': optional: true - '@napi-rs/wasm-tools-wasm32-wasi@1.0.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + '@napi-rs/wasm-tools-wasm32-wasi@1.0.1': dependencies: - '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@napi-rs/wasm-runtime': 1.0.7 optional: true '@napi-rs/wasm-tools-win32-arm64-msvc@1.0.1': @@ -4156,7 +5566,7 @@ snapshots: '@napi-rs/wasm-tools-win32-x64-msvc@1.0.1': optional: true - '@napi-rs/wasm-tools@1.0.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + '@napi-rs/wasm-tools@1.0.1': optionalDependencies: '@napi-rs/wasm-tools-android-arm-eabi': 1.0.1 '@napi-rs/wasm-tools-android-arm64': 1.0.1 @@ -4167,38 +5577,39 @@ snapshots: '@napi-rs/wasm-tools-linux-arm64-musl': 1.0.1 '@napi-rs/wasm-tools-linux-x64-gnu': 1.0.1 '@napi-rs/wasm-tools-linux-x64-musl': 1.0.1 - '@napi-rs/wasm-tools-wasm32-wasi': 1.0.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + '@napi-rs/wasm-tools-wasm32-wasi': 1.0.1 '@napi-rs/wasm-tools-win32-arm64-msvc': 1.0.1 '@napi-rs/wasm-tools-win32-ia32-msvc': 1.0.1 '@napi-rs/wasm-tools-win32-x64-msvc': 1.0.1 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@next/env@16.2.7': {} + '@next/env@15.5.6': {} - '@next/swc-darwin-arm64@16.2.7': + '@next/eslint-plugin-next@15.5.6': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.5.6': optional: true - '@next/swc-darwin-x64@16.2.7': + '@next/swc-darwin-x64@15.5.6': optional: true - '@next/swc-linux-arm64-gnu@16.2.7': + '@next/swc-linux-arm64-gnu@15.5.6': optional: true - '@next/swc-linux-arm64-musl@16.2.7': + '@next/swc-linux-arm64-musl@15.5.6': optional: true - '@next/swc-linux-x64-gnu@16.2.7': + '@next/swc-linux-x64-gnu@15.5.6': optional: true - '@next/swc-linux-x64-musl@16.2.7': + '@next/swc-linux-x64-musl@15.5.6': optional: true - '@next/swc-win32-arm64-msvc@16.2.7': + '@next/swc-win32-arm64-msvc@15.5.6': optional: true - '@next/swc-win32-x64-msvc@16.2.7': + '@next/swc-win32-x64-msvc@15.5.6': optional: true '@nodelib/fs.scandir@2.1.5': @@ -4211,363 +5622,361 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + fastq: 1.17.1 + + '@nolyfill/is-core-module@1.0.39': {} '@octokit/auth-token@6.0.0': {} - '@octokit/core@7.0.6': + '@octokit/core@7.0.4': dependencies: '@octokit/auth-token': 6.0.0 - '@octokit/graphql': 9.0.3 - '@octokit/request': 10.0.9 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 + '@octokit/graphql': 9.0.1 + '@octokit/request': 10.0.3 + '@octokit/request-error': 7.0.0 + '@octokit/types': 15.0.0 before-after-hook: 4.0.0 - universal-user-agent: 7.0.3 + universal-user-agent: 7.0.2 - '@octokit/endpoint@11.0.3': + '@octokit/endpoint@11.0.0': dependencies: - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.2 - '@octokit/graphql@9.0.3': + '@octokit/graphql@9.0.1': dependencies: - '@octokit/request': 10.0.9 - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 + '@octokit/request': 10.0.3 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.2 - '@octokit/openapi-types@27.0.0': {} + '@octokit/openapi-types@25.1.0': {} - '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)': + '@octokit/openapi-types@26.0.0': {} + + '@octokit/plugin-paginate-rest@13.1.1(@octokit/core@7.0.4)': dependencies: - '@octokit/core': 7.0.6 - '@octokit/types': 16.0.0 + '@octokit/core': 7.0.4 + '@octokit/types': 14.1.0 - '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.6)': + '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.4)': dependencies: - '@octokit/core': 7.0.6 + '@octokit/core': 7.0.4 - '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.6)': + '@octokit/plugin-rest-endpoint-methods@16.1.0(@octokit/core@7.0.4)': dependencies: - '@octokit/core': 7.0.6 - '@octokit/types': 16.0.0 + '@octokit/core': 7.0.4 + '@octokit/types': 15.0.0 - '@octokit/request-error@7.1.0': + '@octokit/request-error@7.0.0': dependencies: - '@octokit/types': 16.0.0 + '@octokit/types': 14.1.0 - '@octokit/request@10.0.9': + '@octokit/request@10.0.3': dependencies: - '@octokit/endpoint': 11.0.3 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - content-type: 2.0.0 + '@octokit/endpoint': 11.0.0 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.1.0 fast-content-type-parse: 3.0.0 - json-with-bigint: 3.5.8 - universal-user-agent: 7.0.3 + universal-user-agent: 7.0.2 - '@octokit/rest@22.0.1': + '@octokit/rest@22.0.0': dependencies: - '@octokit/core': 7.0.6 - '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) - '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.6) - '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6) + '@octokit/core': 7.0.4 + '@octokit/plugin-paginate-rest': 13.1.1(@octokit/core@7.0.4) + '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.4) + '@octokit/plugin-rest-endpoint-methods': 16.1.0(@octokit/core@7.0.4) - '@octokit/types@16.0.0': + '@octokit/types@14.1.0': dependencies: - '@octokit/openapi-types': 27.0.0 + '@octokit/openapi-types': 25.1.0 - '@oven/bun-darwin-aarch64@1.3.14': + '@octokit/types@15.0.0': + dependencies: + '@octokit/openapi-types': 26.0.0 + + '@oven/bun-darwin-aarch64@1.3.0': optional: true - '@oven/bun-darwin-x64-baseline@1.3.14': + '@oven/bun-darwin-x64-baseline@1.3.0': optional: true - '@oven/bun-darwin-x64@1.3.14': + '@oven/bun-darwin-x64@1.3.0': optional: true - '@oven/bun-freebsd-aarch64@1.3.14': + '@oven/bun-linux-aarch64-musl@1.3.0': optional: true - '@oven/bun-freebsd-x64@1.3.14': + '@oven/bun-linux-aarch64@1.3.0': optional: true - '@oven/bun-linux-aarch64-android@1.3.14': + '@oven/bun-linux-x64-baseline@1.3.0': optional: true - '@oven/bun-linux-aarch64-musl@1.3.14': + '@oven/bun-linux-x64-musl-baseline@1.3.0': optional: true - '@oven/bun-linux-aarch64@1.3.14': + '@oven/bun-linux-x64-musl@1.3.0': optional: true - '@oven/bun-linux-x64-android@1.3.14': + '@oven/bun-linux-x64@1.3.0': optional: true - '@oven/bun-linux-x64-baseline@1.3.14': + '@oven/bun-windows-x64-baseline@1.3.0': optional: true - '@oven/bun-linux-x64-musl-baseline@1.3.14': + '@oven/bun-windows-x64@1.3.0': optional: true - '@oven/bun-linux-x64-musl@1.3.14': + '@parcel/watcher-android-arm64@2.5.0': optional: true - '@oven/bun-linux-x64@1.3.14': + '@parcel/watcher-android-arm64@2.5.1': optional: true - '@oven/bun-windows-aarch64@1.3.14': + '@parcel/watcher-darwin-arm64@2.5.0': optional: true - '@oven/bun-windows-x64-baseline@1.3.14': + '@parcel/watcher-darwin-arm64@2.5.1': {} + + '@parcel/watcher-darwin-x64@2.5.0': optional: true - '@oven/bun-windows-x64@1.3.14': + '@parcel/watcher-darwin-x64@2.5.1': {} + + '@parcel/watcher-freebsd-x64@2.5.0': optional: true - '@oxc-project/types@0.142.0': {} - - '@parcel/watcher-android-arm64@2.6.0': + '@parcel/watcher-freebsd-x64@2.5.1': optional: true - '@parcel/watcher-darwin-arm64@2.6.0': {} - - '@parcel/watcher-darwin-x64@2.6.0': {} - - '@parcel/watcher-freebsd-x64@2.6.0': + '@parcel/watcher-linux-arm-glibc@2.5.0': optional: true - '@parcel/watcher-linux-arm-glibc@2.6.0': + '@parcel/watcher-linux-arm-glibc@2.5.1': optional: true - '@parcel/watcher-linux-arm-musl@2.6.0': + '@parcel/watcher-linux-arm-musl@2.5.0': optional: true - '@parcel/watcher-linux-arm64-glibc@2.6.0': {} + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true - '@parcel/watcher-linux-arm64-musl@2.6.0': {} + '@parcel/watcher-linux-arm64-glibc@2.5.0': + optional: true - '@parcel/watcher-linux-x64-glibc@2.6.0': {} + '@parcel/watcher-linux-arm64-glibc@2.5.1': {} - '@parcel/watcher-linux-x64-musl@2.6.0': {} + '@parcel/watcher-linux-arm64-musl@2.5.0': + optional: true - '@parcel/watcher-wasm@2.5.6': + '@parcel/watcher-linux-arm64-musl@2.5.1': {} + + '@parcel/watcher-linux-x64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': {} + + '@parcel/watcher-linux-x64-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': {} + + '@parcel/watcher-wasm@2.5.0': dependencies: is-glob: 4.0.3 - picomatch: 4.0.5 + micromatch: 4.0.7 - '@parcel/watcher-win32-arm64@2.6.0': + '@parcel/watcher-win32-arm64@2.5.0': optional: true - '@parcel/watcher-win32-x64@2.6.0': {} + '@parcel/watcher-win32-arm64@2.5.1': + optional: true - '@parcel/watcher@2.6.0(patch_hash=705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da)': + '@parcel/watcher-win32-ia32@2.5.0': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.0': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': {} + + '@parcel/watcher@2.5.0': dependencies: - detect-libc: 2.1.2 + detect-libc: 1.0.3 is-glob: 4.0.3 + micromatch: 4.0.7 node-addon-api: 7.1.1 - picomatch: 4.0.5 optionalDependencies: - '@parcel/watcher-android-arm64': 2.6.0 - '@parcel/watcher-darwin-arm64': 2.6.0 - '@parcel/watcher-darwin-x64': 2.6.0 - '@parcel/watcher-freebsd-x64': 2.6.0 - '@parcel/watcher-linux-arm-glibc': 2.6.0 - '@parcel/watcher-linux-arm-musl': 2.6.0 - '@parcel/watcher-linux-arm64-glibc': 2.6.0 - '@parcel/watcher-linux-arm64-musl': 2.6.0 - '@parcel/watcher-linux-x64-glibc': 2.6.0 - '@parcel/watcher-linux-x64-musl': 2.6.0 - '@parcel/watcher-win32-arm64': 2.6.0 - '@parcel/watcher-win32-x64': 2.6.0 + '@parcel/watcher-android-arm64': 2.5.0 + '@parcel/watcher-darwin-arm64': 2.5.0 + '@parcel/watcher-darwin-x64': 2.5.0 + '@parcel/watcher-freebsd-x64': 2.5.0 + '@parcel/watcher-linux-arm-glibc': 2.5.0 + '@parcel/watcher-linux-arm-musl': 2.5.0 + '@parcel/watcher-linux-arm64-glibc': 2.5.0 + '@parcel/watcher-linux-arm64-musl': 2.5.0 + '@parcel/watcher-linux-x64-glibc': 2.5.0 + '@parcel/watcher-linux-x64-musl': 2.5.0 + '@parcel/watcher-win32-arm64': 2.5.0 + '@parcel/watcher-win32-ia32': 2.5.0 + '@parcel/watcher-win32-x64': 2.5.0 - '@playwright/test@1.62.1': + '@parcel/watcher@2.5.1(patch_hash=zs2vvlrje3h42xp5ed2v44fep4)': dependencies: - playwright: 1.62.1 - - '@rolldown/binding-android-arm64@1.2.1': - optional: true - - '@rolldown/binding-darwin-arm64@1.2.1': - optional: true - - '@rolldown/binding-darwin-x64@1.2.1': - optional: true - - '@rolldown/binding-freebsd-x64@1.2.1': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.2.1': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.2.1': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.2.1': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.2.1': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.2.1': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.2.1': - optional: true - - '@rolldown/binding-linux-x64-musl@1.2.1': - optional: true - - '@rolldown/binding-openharmony-arm64@1.2.1': - optional: true - - '@rolldown/binding-wasm32-wasi@1.2.1': - dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 - '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.2.1': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.2.1': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@rollup/rollup-android-arm-eabi@4.60.4': - optional: true - - '@rollup/rollup-android-arm64@4.60.4': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.4': - optional: true - - '@rollup/rollup-darwin-x64@4.60.4': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.4': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.4': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.4': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.4': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.4': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.4': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.4': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.4': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.4': - optional: true - - '@rspack/binding-darwin-arm64@2.0.2': - optional: true - - '@rspack/binding-darwin-x64@2.0.2': - optional: true - - '@rspack/binding-linux-arm64-gnu@2.0.2': - optional: true - - '@rspack/binding-linux-arm64-musl@2.0.2': - optional: true - - '@rspack/binding-linux-x64-gnu@2.0.2': - optional: true - - '@rspack/binding-linux-x64-musl@2.0.2': - optional: true - - '@rspack/binding-wasm32-wasi@2.0.2': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@rspack/binding-win32-arm64-msvc@2.0.2': - optional: true - - '@rspack/binding-win32-ia32-msvc@2.0.2': - optional: true - - '@rspack/binding-win32-x64-msvc@2.0.2': - optional: true - - '@rspack/binding@2.0.2': + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 optionalDependencies: - '@rspack/binding-darwin-arm64': 2.0.2 - '@rspack/binding-darwin-x64': 2.0.2 - '@rspack/binding-linux-arm64-gnu': 2.0.2 - '@rspack/binding-linux-arm64-musl': 2.0.2 - '@rspack/binding-linux-x64-gnu': 2.0.2 - '@rspack/binding-linux-x64-musl': 2.0.2 - '@rspack/binding-wasm32-wasi': 2.0.2 - '@rspack/binding-win32-arm64-msvc': 2.0.2 - '@rspack/binding-win32-ia32-msvc': 2.0.2 - '@rspack/binding-win32-x64-msvc': 2.0.2 + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 - '@rspack/core@2.0.2(@swc/helpers@0.5.15)': + '@pkgjs/parseargs@0.11.0': + optional: true + + '@playwright/test@1.56.1': dependencies: - '@rspack/binding': 2.0.2 - optionalDependencies: - '@swc/helpers': 0.5.15 + playwright: 1.56.1 + + '@rolldown/pluginutils@1.0.0-beta.43': {} + + '@rollup/rollup-android-arm-eabi@4.34.9': + optional: true + + '@rollup/rollup-android-arm-eabi@4.44.0': + optional: true + + '@rollup/rollup-android-arm64@4.34.9': + optional: true + + '@rollup/rollup-android-arm64@4.44.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.34.9': + optional: true + + '@rollup/rollup-darwin-arm64@4.44.0': + optional: true + + '@rollup/rollup-darwin-x64@4.34.9': + optional: true + + '@rollup/rollup-darwin-x64@4.44.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.34.9': + optional: true + + '@rollup/rollup-freebsd-arm64@4.44.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.34.9': + optional: true + + '@rollup/rollup-freebsd-x64@4.44.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.44.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.34.9': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.44.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.34.9': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.44.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.34.9': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.44.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.44.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.34.9': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.44.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.44.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.34.9': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.44.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.34.9': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.44.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.34.9': + optional: true + + '@rollup/rollup-linux-x64-musl@4.44.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.34.9': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.44.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.34.9': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.44.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.34.9': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.44.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.10.4': {} '@sindresorhus/merge-streams@4.0.0': {} - '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -4576,300 +5985,484 @@ snapshots: dependencies: tailwindcss: link:packages/tailwindcss - '@tailwindcss/forms@0.5.11(tailwindcss@packages+tailwindcss)': + '@tailwindcss/forms@0.5.10(tailwindcss@packages+tailwindcss)': dependencies: mini-svg-data-uri: 1.4.4 tailwindcss: link:packages/tailwindcss - '@tailwindcss/typography@0.5.20(tailwindcss@packages+tailwindcss)': + '@tailwindcss/typography@0.5.19(tailwindcss@packages+tailwindcss)': dependencies: postcss-selector-parser: 6.0.10 tailwindcss: link:packages/tailwindcss - '@turbo/darwin-64@2.10.8': - optional: true - - '@turbo/darwin-arm64@2.10.8': - optional: true - - '@turbo/linux-64@2.10.8': - optional: true - - '@turbo/linux-arm64@2.10.8': - optional: true - - '@turbo/windows-64@2.10.8': - optional: true - - '@turbo/windows-arm64@2.10.8': - optional: true - - '@tybys/wasm-util@0.10.3': + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 - '@types/bun@1.3.14': + '@types/babel__core@7.20.5': dependencies: - bun-types: 1.3.14 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 - '@types/chai@5.2.3': + '@types/babel__generator@7.6.8': dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 + '@babel/types': 7.28.4 - '@types/deep-eql@4.0.2': {} + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.28.4 + + '@types/braces@3.0.5': {} + + '@types/bun@1.3.0(@types/react@19.2.2)': + dependencies: + bun-types: 1.3.0(@types/react@19.2.2) + transitivePeerDependencies: + - '@types/react' + + '@types/estree@1.0.6': {} '@types/estree@1.0.8': {} - '@types/estree@1.0.9': {} - '@types/json-schema@7.0.15': {} - '@types/node@22.20.1': + '@types/json5@0.0.29': {} + + '@types/node@20.14.13': + dependencies: + undici-types: 5.26.5 + + '@types/node@20.19.1': dependencies: undici-types: 6.21.0 - '@types/node@25.9.1': - dependencies: - undici-types: 7.24.6 - '@types/postcss-import@14.0.3': dependencies: - postcss: 8.5.25 + postcss: 8.4.41 - '@types/react-dom@19.2.3(@types/react@19.2.15)': + '@types/react-dom@19.2.2(@types/react@19.2.2)': dependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.2 - '@types/react@19.2.15': + '@types/react@19.2.2': dependencies: - csstype: 3.2.3 + csstype: 3.1.3 - '@types/semver@7.8.0': {} + '@types/semver@7.7.1': {} - '@vitejs/plugin-react@6.0.2(vite@8.2.0(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))': + '@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4)': dependencies: - '@rolldown/pluginutils': 1.0.1 - vite: 8.2.0(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.11.0 + '@typescript-eslint/type-utils': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/utils': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.11.0 + eslint: 9.37.0(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color - '@vitest/expect@4.1.10': + '@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3)': dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - chai: 6.2.2 - tinyrainbow: 3.1.0 + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.11.0 + '@typescript-eslint/type-utils': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3) + '@typescript-eslint/utils': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.11.0 + eslint: 9.37.0(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color - '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))': + '@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4)': dependencies: - '@vitest/spy': 4.1.10 + '@typescript-eslint/scope-manager': 8.11.0 + '@typescript-eslint/types': 8.11.0 + '@typescript-eslint/typescript-estree': 8.11.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.11.0 + debug: 4.4.3 + eslint: 9.37.0(jiti@2.6.1) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.11.0 + '@typescript-eslint/types': 8.11.0 + '@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.11.0 + debug: 4.4.3 + eslint: 9.37.0(jiti@2.6.1) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.11.0': + dependencies: + '@typescript-eslint/types': 8.11.0 + '@typescript-eslint/visitor-keys': 8.11.0 + + '@typescript-eslint/type-utils@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@typescript-eslint/typescript-estree': 8.11.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4) + debug: 4.4.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/type-utils@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3) + debug: 4.4.3 + ts-api-utils: 1.3.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/types@8.11.0': {} + + '@typescript-eslint/typescript-estree@8.11.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/types': 8.11.0 + '@typescript-eslint/visitor-keys': 8.11.0 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.11.0(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.11.0 + '@typescript-eslint/visitor-keys': 8.11.0 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 1.3.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.11.0 + '@typescript-eslint/types': 8.11.0 + '@typescript-eslint/typescript-estree': 8.11.0(typescript@5.5.4) + eslint: 9.37.0(jiti@2.6.1) + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.11.0 + '@typescript-eslint/types': 8.11.0 + '@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3) + eslint: 9.37.0(jiti@2.6.1) + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@8.11.0': + dependencies: + '@typescript-eslint/types': 8.11.0 + eslint-visitor-keys: 3.4.3 + + '@vitejs/plugin-react@5.1.0(vite@7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.43 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.0.5': + dependencies: + '@vitest/utils': 2.0.5 + pathe: 1.1.2 + + '@vitest/snapshot@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.0 + + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.2.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) + loupe: 3.1.1 + tinyrainbow: 1.2.0 - '@vitest/pretty-format@4.1.10': + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - tinyrainbow: 3.1.0 + acorn: 8.15.0 - '@vitest/runner@4.1.10': - dependencies: - '@vitest/utils': 4.1.10 - pathe: 2.0.3 + acorn@8.14.0: {} - '@vitest/snapshot@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 - magic-string: 0.30.21 - pathe: 2.0.3 + acorn@8.15.0: {} - '@vitest/spy@4.1.10': {} - - '@vitest/utils@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - - '@webassemblyjs/ast@1.14.1': - dependencies: - '@webassemblyjs/helper-numbers': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - - '@webassemblyjs/helper-api-error@1.13.2': {} - - '@webassemblyjs/helper-buffer@1.14.1': {} - - '@webassemblyjs/helper-numbers@1.13.2': - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.13.2 - '@webassemblyjs/helper-api-error': 1.13.2 - '@xtuc/long': 4.2.2 - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - - '@webassemblyjs/helper-wasm-section@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/wasm-gen': 1.14.1 - - '@webassemblyjs/ieee754@1.13.2': - dependencies: - '@xtuc/ieee754': 1.2.0 - - '@webassemblyjs/leb128@1.13.2': - dependencies: - '@xtuc/long': 4.2.2 - - '@webassemblyjs/utf8@1.13.2': {} - - '@webassemblyjs/wasm-edit@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/helper-wasm-section': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-opt': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wast-printer': 1.14.1 - - '@webassemblyjs/wasm-gen@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wasm-opt@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - - '@webassemblyjs/wasm-parser@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-api-error': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wast-printer@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@xtuc/long': 4.2.2 - - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} - - acorn@8.16.0: {} - - ajv-formats@2.1.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - - ajv-keywords@5.1.0(ajv@8.20.0): - dependencies: - ajv: 8.20.0 - fast-deep-equal: 3.1.3 - - ajv@8.20.0: + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} any-promise@1.3.0: {} anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.2 + picomatch: 2.3.1 arg@5.0.2: {} argparse@2.0.1: {} + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.findlastindex@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.flat@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + + array.prototype.flatmap@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-shim-unscopables: 1.0.2 + + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + assertion-error@2.0.1: {} - autoprefixer@10.5.0(postcss@8.5.25): + ast-types-flow@0.0.8: {} + + autoprefixer@10.4.21(postcss@8.4.47): dependencies: - browserslist: 4.28.2 - caniuse-lite: 1.0.30001793 - fraction.js: 5.3.4 + browserslist: 4.24.4 + caniuse-lite: 1.0.30001705 + fraction.js: 4.3.7 + normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.25 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - baseline-browser-mapping@2.10.31: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + axe-core@4.10.2: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} before-after-hook@4.0.0: {} binary-extensions@2.3.0: {} + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 - browserslist@4.28.2: + browserslist@4.24.4: dependencies: - baseline-browser-mapping: 2.10.31 - caniuse-lite: 1.0.30001793 - electron-to-chromium: 1.5.361 - node-releases: 2.0.46 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + caniuse-lite: 1.0.30001705 + electron-to-chromium: 1.5.119 + node-releases: 2.0.19 + update-browserslist-db: 1.1.1(browserslist@4.24.4) - buffer-from@1.1.2: {} + buffer-from@1.1.2: + optional: true - bun-types@1.3.14: + bun-types@1.3.0(@types/react@19.2.2): dependencies: - '@types/node': 25.9.1 + '@types/node': 20.19.1 + '@types/react': 19.2.2 - bun@1.3.14: + bun@1.3.0: optionalDependencies: - '@oven/bun-darwin-aarch64': 1.3.14 - '@oven/bun-darwin-x64': 1.3.14 - '@oven/bun-darwin-x64-baseline': 1.3.14 - '@oven/bun-freebsd-aarch64': 1.3.14 - '@oven/bun-freebsd-x64': 1.3.14 - '@oven/bun-linux-aarch64': 1.3.14 - '@oven/bun-linux-aarch64-android': 1.3.14 - '@oven/bun-linux-aarch64-musl': 1.3.14 - '@oven/bun-linux-x64': 1.3.14 - '@oven/bun-linux-x64-android': 1.3.14 - '@oven/bun-linux-x64-baseline': 1.3.14 - '@oven/bun-linux-x64-musl': 1.3.14 - '@oven/bun-linux-x64-musl-baseline': 1.3.14 - '@oven/bun-windows-aarch64': 1.3.14 - '@oven/bun-windows-x64': 1.3.14 - '@oven/bun-windows-x64-baseline': 1.3.14 + '@oven/bun-darwin-aarch64': 1.3.0 + '@oven/bun-darwin-x64': 1.3.0 + '@oven/bun-darwin-x64-baseline': 1.3.0 + '@oven/bun-linux-aarch64': 1.3.0 + '@oven/bun-linux-aarch64-musl': 1.3.0 + '@oven/bun-linux-x64': 1.3.0 + '@oven/bun-linux-x64-baseline': 1.3.0 + '@oven/bun-linux-x64-musl': 1.3.0 + '@oven/bun-linux-x64-musl-baseline': 1.3.0 + '@oven/bun-windows-x64': 1.3.0 + '@oven/bun-windows-x64-baseline': 1.3.0 - bundle-require@5.1.0(esbuild@0.27.7): + bundle-require@5.1.0(esbuild@0.25.0): dependencies: - esbuild: 0.27.7 + esbuild: 0.25.0 load-tsconfig: 0.2.5 cac@6.7.14: {} + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + callsites@3.1.0: {} + camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001793: {} + caniuse-lite@1.0.30001705: {} - chai@6.2.2: {} + chai@5.1.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 - chardet@2.1.1: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chardet@2.1.0: {} + + check-error@2.1.1: {} chokidar@3.6.0: dependencies: @@ -4885,11 +6478,11 @@ snapshots: chokidar@4.0.3: dependencies: - readdirp: 4.1.2 + readdirp: 4.1.1 - chrome-trace-event@1.0.4: {} - - citty@0.2.2: {} + citty@0.1.6: + dependencies: + consola: 3.2.3 cli-width@4.1.0: {} @@ -4899,124 +6492,642 @@ snapshots: dependencies: typanion: 3.14.0 + clipboardy@4.0.0: + dependencies: + execa: 8.0.1 + is-wsl: 3.1.0 + is64bit: 2.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + colorette@2.0.20: {} - commander@2.20.3: {} + commander@2.20.3: + optional: true commander@4.1.1: {} + concat-map@0.0.1: {} + confbox@0.1.8: {} - consola@3.4.2: {} + consola@3.2.3: {} - content-type@2.0.0: {} + consola@3.4.0: {} convert-source-map@2.0.0: {} - cookie-es@1.2.3: {} + cookie-es@1.2.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crossws@0.3.1: + dependencies: + uncrypto: 0.1.3 crossws@0.3.5: dependencies: uncrypto: 0.1.3 - crossws@0.4.10: {} - cssesc@3.0.0: {} - csstype@3.2.3: {} + csstype@3.1.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.6: + dependencies: + ms: 2.1.2 + + debug@4.4.0: + dependencies: + ms: 2.1.3 debug@4.4.3: dependencies: ms: 2.1.3 - dedent@1.7.2: {} + dedent@1.7.0: {} - defu@6.1.7: {} + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defu@6.1.4: {} destr@2.0.5: {} detect-libc@1.0.3: {} - detect-libc@2.1.2: {} + detect-libc@2.0.4: {} didyoumean@1.2.2: {} dlv@1.1.3: {} - electron-to-chromium@1.5.361: {} + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 - emnapi@1.11.3(node-addon-api@8.7.0): + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.119: {} + + emnapi@1.6.0(node-addon-api@8.3.0): optionalDependencies: - node-addon-api: 8.7.0 + node-addon-api: 8.3.0 - enhanced-resolve@5.24.5: + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.3 + tapable: 2.2.1 + + es-abstract@1.23.3: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.2 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 es-errors@1.3.0: {} - es-module-lexer@2.1.0: {} + es-iterator-helpers@1.1.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + globalthis: 1.0.4 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + iterator.prototype: 1.1.3 + safe-array-concat: 1.1.2 - es-toolkit@1.50.0: {} + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 - esbuild@0.27.7: + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.0.2: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + + es-toolkit@1.39.10: {} + + esbuild@0.21.5: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + optional: true + + esbuild@0.25.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.0 + '@esbuild/android-arm': 0.25.0 + '@esbuild/android-arm64': 0.25.0 + '@esbuild/android-x64': 0.25.0 + '@esbuild/darwin-arm64': 0.25.0 + '@esbuild/darwin-x64': 0.25.0 + '@esbuild/freebsd-arm64': 0.25.0 + '@esbuild/freebsd-x64': 0.25.0 + '@esbuild/linux-arm': 0.25.0 + '@esbuild/linux-arm64': 0.25.0 + '@esbuild/linux-ia32': 0.25.0 + '@esbuild/linux-loong64': 0.25.0 + '@esbuild/linux-mips64el': 0.25.0 + '@esbuild/linux-ppc64': 0.25.0 + '@esbuild/linux-riscv64': 0.25.0 + '@esbuild/linux-s390x': 0.25.0 + '@esbuild/linux-x64': 0.25.0 + '@esbuild/netbsd-arm64': 0.25.0 + '@esbuild/netbsd-x64': 0.25.0 + '@esbuild/openbsd-arm64': 0.25.0 + '@esbuild/openbsd-x64': 0.25.0 + '@esbuild/sunos-x64': 0.25.0 + '@esbuild/win32-arm64': 0.25.0 + '@esbuild/win32-ia32': 0.25.0 + '@esbuild/win32-x64': 0.25.0 escalade@3.2.0: {} - eslint-scope@5.1.1: + escape-string-regexp@4.0.0: {} + + eslint-config-next@15.5.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@next/eslint-plugin-next': 15.5.6 + '@rushstack/eslint-patch': 1.10.4 + '@typescript-eslint/eslint-plugin': 8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/parser': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4) + eslint: 9.37.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.1(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-react: 7.37.2(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-react-hooks: 5.0.0(eslint@9.37.0(jiti@2.6.1)) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-config-next@15.5.6(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3): + dependencies: + '@next/eslint-plugin-next': 15.5.6 + '@rushstack/eslint-patch': 1.10.4 + '@typescript-eslint/eslint-plugin': 8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3) + '@typescript-eslint/parser': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3) + eslint: 9.37.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.1(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-react: 7.37.2(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-react-hooks: 5.0.0(eslint@9.37.0(jiti@2.6.1)) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.15.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + enhanced-resolve: 5.18.3 + eslint: 9.37.0(jiti@2.6.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + fast-glob: 3.3.3 + get-tsconfig: 4.10.0 + is-bun-module: 1.2.1 + is-glob: 4.0.3 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint@9.37.0(jiti@2.6.1)) + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + enhanced-resolve: 5.18.3 + eslint: 9.37.0(jiti@2.6.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + fast-glob: 3.3.3 + get-tsconfig: 4.10.0 + is-bun-module: 1.2.1 + is-glob: 4.0.3 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.37.0(jiti@2.6.1)) + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4) + eslint: 9.37.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3) + eslint: 9.37.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint@9.37.0(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.37.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.15.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + string.prototype.trimend: 1.0.8 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.5.4) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.37.0(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.37.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.15.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + string.prototype.trimend: 1.0.8 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.11.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.6.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.1(eslint@9.37.0(jiti@2.6.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.8 + array.prototype.flatmap: 1.3.2 + ast-types-flow: 0.0.8 + axe-core: 4.10.2 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + es-iterator-helpers: 1.1.0 + eslint: 9.37.0(jiti@2.6.1) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.0.3 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.0.0(eslint@9.37.0(jiti@2.6.1)): + dependencies: + eslint: 9.37.0(jiti@2.6.1) + + eslint-plugin-react@7.37.2(eslint@9.37.0(jiti@2.6.1)): + dependencies: + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.1.0 + eslint: 9.37.0(jiti@2.6.1) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.8 + object.fromentries: 2.0.8 + object.values: 1.2.0 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.11 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 - estraverse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.37.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.4.0 + '@eslint/core': 0.16.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.37.0 + '@eslint/plugin-kit': 0.4.0 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.2 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 esrecurse@4.3.0: dependencies: estraverse: 5.3.0 - estraverse@4.3.0: {} - estraverse@5.3.0: {} estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.9 + '@types/estree': 1.0.8 - events@3.3.0: {} + esutils@2.0.3: {} - expect-type@1.3.0: {} + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 fast-content-type-parse@3.0.0: {} fast-deep-equal@3.1.3: {} - fast-equals@5.4.0: {} + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 fast-glob@3.3.3: dependencies: @@ -5026,45 +7137,56 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-string-truncated-width@3.0.3: {} + fast-json-stable-stringify@2.1.0: {} - fast-string-width@3.0.2: + fast-levenshtein@2.0.6: {} + + fastq@1.17.1: dependencies: - fast-string-truncated-width: 3.0.3 + reusify: 1.0.4 - fast-stringify@4.0.0: {} - - fast-uri@3.1.2: {} - - fast-wrap-ansi@0.2.2: - dependencies: - fast-string-width: 3.0.2 - - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - - fdir@6.5.0(picomatch@4.0.4): + fdir@6.4.6(picomatch@4.0.2): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.2 - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - find-up-simple@1.0.1: {} + find-up-simple@1.0.0: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 - mlly: 1.8.2 - rollup: 4.60.4 + mlly: 1.7.4 + rollup: 4.44.0 - fraction.js@5.3.4: {} + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flatted@3.3.1: {} + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fraction.js@4.3.7: {} fsevents@2.3.2: optional: true @@ -5074,7 +7196,40 @@ snapshots: function-bind@1.1.2: {} - get-port-please@3.2.0: {} + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-func-name@2.0.2: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-port-please@3.1.2: {} + + get-stream@8.0.1: {} + + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + + get-tsconfig@4.10.0: + dependencies: + resolve-pkg-maps: 1.0.0 glob-parent@5.1.2: dependencies: @@ -5084,159 +7239,379 @@ snapshots: dependencies: is-glob: 4.0.3 - globby@16.2.2: + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + + globby@15.0.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 fast-glob: 3.3.3 ignore: 7.0.5 - is-path-inside: 4.0.0 + path-type: 6.0.0 slash: 5.1.0 - unicorn-magic: 0.4.0 + unicorn-magic: 0.3.0 + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 graceful-fs@4.2.11: {} - h3@1.15.11: + graphemer@1.4.0: {} + + h3@1.15.4: dependencies: - cookie-es: 1.2.3 + cookie-es: 1.2.2 crossws: 0.3.5 - defu: 6.1.7 + defu: 6.1.4 destr: 2.0.5 iron-webcrypto: 1.2.1 - node-mock-http: 1.0.4 + node-mock-http: 1.0.2 radix3: 1.1.2 - ufo: 1.6.4 + ufo: 1.6.1 uncrypto: 0.1.3 + has-bigints@1.0.2: {} + has-flag@4.0.0: {} - hasown@2.0.3: + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + hasown@2.0.2: dependencies: function-bind: 1.1.2 http-shutdown@1.2.2: {} - iconv-lite@0.7.2: + human-signals@5.0.0: {} + + iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 + ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + iron-webcrypto@1.2.1: {} + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-arrayish@0.3.2: + optional: true + + is-async-function@2.0.0: + dependencies: + has-tostringtag: 1.0.2 + + is-bigint@1.0.4: + dependencies: + has-bigints: 1.0.2 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 - is-core-module@2.16.2: + is-boolean-object@1.1.2: dependencies: - hasown: 2.0.3 + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-bun-module@1.2.1: + dependencies: + semver: 7.7.3 + + is-callable@1.2.7: {} + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + + is-docker@3.0.0: {} is-extglob@2.1.1: {} + is-finalizationregistry@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + is-number@7.0.0: {} - is-path-inside@4.0.0: {} - - jest-worker@27.5.1: + is-regex@1.1.4: dependencies: - '@types/node': 25.9.1 - merge-stream: 2.0.0 - supports-color: 8.1.1 + call-bind: 1.0.7 + has-tostringtag: 1.0.2 - jiti@1.21.7: {} + is-set@2.0.3: {} - jiti@2.7.0: {} + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + + is-stream@3.0.0: {} + + is-string@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-symbol@1.0.4: + dependencies: + has-symbols: 1.0.3 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-weakmap@2.0.2: {} + + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-weakset@2.0.3: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + is64bit@2.0.0: + dependencies: + system-architecture: 0.1.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.3: + dependencies: + define-properties: 1.2.1 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + reflect.getprototypeof: 1.0.6 + set-function-name: 2.0.2 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.6: {} + + jiti@2.4.2: {} + + jiti@2.6.1: {} joycon@3.1.1: {} - js-yaml@4.3.1: + js-tokens@4.0.0: {} + + js-yaml@4.1.0: dependencies: argparse: 2.0.1 - json-schema-traverse@1.0.0: {} + jsesc@3.1.0: {} - json-with-bigint@3.5.8: {} + json-buffer@3.0.1: {} - lightningcss-android-arm64@1.33.0: - optional: true + json-schema-traverse@0.4.1: {} - lightningcss-darwin-arm64@1.33.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} - lightningcss-darwin-x64@1.33.0: {} - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: {} - - lightningcss-linux-arm64-musl@1.33.0: {} - - lightningcss-linux-x64-gnu@1.33.0: {} - - lightningcss-linux-x64-musl@1.33.0: {} - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: {} - - lightningcss@1.33.0(patch_hash=1d4a8800d60d13d42887b88b3a86576df4b451670308145fb432ec8abbf40930): + json5@1.0.2: dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 + minimist: 1.2.8 - lilconfig@3.1.3: {} + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.2 + object.assign: 4.1.5 + object.values: 1.2.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: {} + + lightningcss-darwin-x64@1.30.2: {} + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: {} + + lightningcss-linux-arm64-musl@1.30.2: {} + + lightningcss-linux-x64-gnu@1.30.2: {} + + lightningcss-linux-x64-musl@1.30.2: {} + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: {} + + lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm): + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lilconfig@2.1.0: {} + + lilconfig@3.1.2: {} lines-and-columns@1.2.4: {} - listhen@1.10.1(@parcel/watcher@2.6.0(patch_hash=705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da)): + listhen@1.9.0: dependencies: - '@parcel/watcher-wasm': 2.5.6 - citty: 0.2.2 - consola: 3.4.2 - crossws: 0.4.10 - defu: 6.1.7 - get-port-please: 3.2.0 - h3: 1.15.11 + '@parcel/watcher': 2.5.0 + '@parcel/watcher-wasm': 2.5.0 + citty: 0.1.6 + clipboardy: 4.0.0 + consola: 3.2.3 + crossws: 0.3.1 + defu: 6.1.4 + get-port-please: 3.1.2 + h3: 1.15.4 http-shutdown: 1.2.2 - jiti: 2.7.0 - node-forge: 1.4.0 - pathe: 2.0.3 - std-env: 4.2.0 - tinyclip: 0.1.15 - ufo: 1.6.4 - untun: 0.2.2 - uqr: 0.1.3 - optionalDependencies: - '@parcel/watcher': 2.6.0(patch_hash=705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da) - transitivePeerDependencies: - - srvx + jiti: 2.4.2 + mlly: 1.7.3 + node-forge: 1.3.1 + pathe: 1.1.2 + std-env: 3.7.0 + ufo: 1.5.4 + untun: 0.1.3 + uqr: 0.1.2 load-tsconfig@0.2.5: {} - magic-string@0.30.21: + locate-path@6.0.0: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + p-locate: 5.0.0 - magic-string@1.1.0: + lodash.merge@4.6.2: {} + + lodash.sortby@4.7.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5244,43 +7619,55 @@ snapshots: merge2@1.4.1: {} - micro-memoize@5.1.1: + micro-memoize@4.1.3: {} + + micromatch@4.0.7: dependencies: - fast-equals: 5.4.0 - fast-stringify: 4.0.0 + braces: 3.0.3 + picomatch: 2.3.1 micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.2 + picomatch: 2.3.1 - mime-db@1.54.0: {} + mimic-fn@4.0.0: {} mini-svg-data-uri@1.4.4: {} - minimizer-webpack-plugin@5.6.1(esbuild@0.27.7)(postcss@8.5.25)(webpack@5.109.2(esbuild@0.27.7)(postcss@8.5.25)): + minimatch@3.1.2: dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - terser: 5.48.0 - webpack: 5.109.2(esbuild@0.27.7)(postcss@8.5.25) - optionalDependencies: - esbuild: 0.27.7 - postcss: 8.5.25 + brace-expansion: 1.1.11 - mlly@1.8.2: + minimatch@9.0.5: dependencies: - acorn: 8.16.0 + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mlly@1.7.3: + dependencies: + acorn: 8.14.0 + pathe: 1.1.2 + pkg-types: 1.3.0 + ufo: 1.6.1 + + mlly@1.7.4: + dependencies: + acorn: 8.15.0 pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.4 + pkg-types: 1.3.0 + ufo: 1.6.1 mri@1.2.0: {} + ms@2.1.2: {} + ms@2.1.3: {} - mute-stream@3.0.0: {} + mute-stream@2.0.0: {} mz@2.7.0: dependencies: @@ -5288,131 +7675,223 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.12: {} + nanoid@3.3.11: {} - nanoid@3.3.16: {} + nanoid@3.3.7: {} - neo-async@2.6.2: {} + natural-compare@1.4.0: {} - next@16.2.7(@playwright/test@1.62.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@15.5.6(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@next/env': 16.2.7 + '@next/env': 15.5.6 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.31 - caniuse-lite: 1.0.30001793 + caniuse-lite: 1.0.30001705 postcss: 8.4.31 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - styled-jsx: 5.1.6(react@19.2.7) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + styled-jsx: 5.1.6(react@19.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.7 - '@next/swc-darwin-x64': 16.2.7 - '@next/swc-linux-arm64-gnu': 16.2.7 - '@next/swc-linux-arm64-musl': 16.2.7 - '@next/swc-linux-x64-gnu': 16.2.7 - '@next/swc-linux-x64-musl': 16.2.7 - '@next/swc-win32-arm64-msvc': 16.2.7 - '@next/swc-win32-x64-msvc': 16.2.7 - '@playwright/test': 1.62.1 - sharp: 0.34.5 + '@next/swc-darwin-arm64': 15.5.6 + '@next/swc-darwin-x64': 15.5.6 + '@next/swc-linux-arm64-gnu': 15.5.6 + '@next/swc-linux-arm64-musl': 15.5.6 + '@next/swc-linux-x64-gnu': 15.5.6 + '@next/swc-linux-x64-musl': 15.5.6 + '@next/swc-win32-arm64-msvc': 15.5.6 + '@next/swc-win32-x64-msvc': 15.5.6 + '@playwright/test': 1.56.1 + sharp: 0.34.3 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros node-addon-api@7.1.1: {} - node-addon-api@8.7.0: {} + node-addon-api@8.2.2: {} - node-forge@1.4.0: {} + node-addon-api@8.3.0: {} + + node-forge@1.3.1: {} + + node-gyp-build@4.8.2: {} node-gyp-build@4.8.4: {} - node-mock-http@1.0.4: {} + node-mock-http@1.0.2: {} - node-releases@2.0.46: {} + node-releases@2.0.19: {} normalize-path@3.0.0: {} + normalize-range@0.1.2: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + object-assign@4.1.1: {} object-hash@3.0.0: {} - obug@2.1.1: {} + object-inspect@1.13.2: {} - obug@2.1.4: {} + object-keys@1.1.1: {} + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + object.entries@1.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + + object.values@1.2.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} package-up@5.0.0: dependencies: - find-up-simple: 1.0.1 + find-up-simple: 1.0.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-type@6.0.0: {} + + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.0: {} + picocolors@1.1.1: {} - picomatch@2.3.2: {} + picomatch@2.3.1: {} - picomatch@4.0.4: {} - - picomatch@4.0.5: {} + picomatch@4.0.2: {} pify@2.3.0: {} - pirates@4.0.7: {} + pirates@4.0.6: {} - pkg-types@1.3.1: + pkg-types@1.3.0: dependencies: confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 + mlly: 1.7.3 + pathe: 1.1.2 - playwright-core@1.62.1: {} + playwright-core@1.56.1: {} - playwright@1.62.1: + playwright@1.56.1: dependencies: - playwright-core: 1.62.1 + playwright-core: 1.56.1 optionalDependencies: fsevents: 2.3.2 - postcss-import@15.1.0(postcss@8.5.16): + possible-typed-array-names@1.0.0: {} + + postcss-import@15.1.0(postcss@8.4.47): dependencies: - postcss: 8.5.16 + postcss: 8.4.47 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.12 + resolve: 1.22.8 - postcss-import@16.1.1(postcss@8.5.25): + postcss-import@16.1.1(postcss@8.4.41): dependencies: - postcss: 8.5.25 + postcss: 8.4.41 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.12 + resolve: 1.22.8 - postcss-js@4.1.0(postcss@8.5.16): + postcss-import@16.1.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + + postcss-js@4.0.1(postcss@8.4.47): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.16 + postcss: 8.4.47 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.16)(yaml@2.9.0): + postcss-load-config@4.0.2(postcss@8.4.47): dependencies: - lilconfig: 3.1.3 + lilconfig: 3.1.2 + yaml: 2.6.0 optionalDependencies: - jiti: 1.21.7 - postcss: 8.5.16 - yaml: 2.9.0 + postcss: 8.4.47 - postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.25)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.1)(yaml@2.6.0): dependencies: - lilconfig: 3.1.3 + lilconfig: 3.1.2 optionalDependencies: - jiti: 2.7.0 - postcss: 8.5.25 - yaml: 2.9.0 + jiti: 2.6.1 + postcss: 8.5.6 + tsx: 4.19.1 + yaml: 2.6.0 - postcss-nested@6.2.0(postcss@8.5.16): + postcss-nested@6.2.0(postcss@8.4.47): dependencies: - postcss: 8.5.16 + postcss: 8.4.47 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.0.10: @@ -5425,7 +7904,7 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.4: + postcss-selector-parser@7.1.0: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -5434,57 +7913,70 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.16: + postcss@8.4.41: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.7 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.25: + postcss@8.4.47: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.7 picocolors: 1.1.1 source-map-js: 1.2.1 - prettier-plugin-embed@0.5.1: + postcss@8.5.6: dependencies: - '@types/estree': 1.0.9 - dedent: 1.7.2 - micro-memoize: 5.1.1 + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-embed@0.5.0: + dependencies: + '@types/estree': 1.0.6 + dedent: 1.7.0 + micro-memoize: 4.1.3 package-up: 5.0.0 tiny-jsonc: 1.0.2 - type-fest: 5.6.0 + type-fest: 4.36.0 transitivePeerDependencies: - babel-plugin-macros - prettier-plugin-organize-imports@4.3.0(prettier@3.9.6)(typescript@5.9.3): + prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.5.4): dependencies: - prettier: 3.9.6 - typescript: 5.9.3 + prettier: 3.6.2 + typescript: 5.5.4 - prettier@3.9.6: {} + prettier@3.6.2: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} queue-microtask@1.2.3: {} radix3@1.1.2: {} - react-dom@19.2.6(react@19.2.6): + react-dom@19.2.0(react@19.2.0): dependencies: - react: 19.2.6 + react: 19.2.0 scheduler: 0.27.0 - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 + react-is@16.13.1: {} - react@19.2.6: {} + react-refresh@0.18.0: {} - react@19.2.7: {} + react@19.2.0: {} read-cache@1.0.0: dependencies: @@ -5492,128 +7984,191 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 2.3.2 + picomatch: 2.3.1 - readdirp@4.1.2: {} + readdirp@4.1.1: {} - require-from-string@2.0.2: {} + reflect.getprototypeof@1.0.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + globalthis: 1.0.4 + which-builtin-type: 1.1.4 + + regexp.prototype.flags@1.5.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} resolve-from@5.0.0: {} - resolve@1.22.12: + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.8: dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - reusify@1.1.0: {} - - rolldown@1.2.1: + resolve@2.0.0-next.5: dependencies: - '@oxc-project/types': 0.142.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.1 - '@rolldown/binding-darwin-arm64': 1.2.1 - '@rolldown/binding-darwin-x64': 1.2.1 - '@rolldown/binding-freebsd-x64': 1.2.1 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.1 - '@rolldown/binding-linux-arm64-gnu': 1.2.1 - '@rolldown/binding-linux-arm64-musl': 1.2.1 - '@rolldown/binding-linux-ppc64-gnu': 1.2.1 - '@rolldown/binding-linux-s390x-gnu': 1.2.1 - '@rolldown/binding-linux-x64-gnu': 1.2.1 - '@rolldown/binding-linux-x64-musl': 1.2.1 - '@rolldown/binding-openharmony-arm64': 1.2.1 - '@rolldown/binding-wasm32-wasi': 1.2.1 - '@rolldown/binding-win32-arm64-msvc': 1.2.1 - '@rolldown/binding-win32-x64-msvc': 1.2.1 + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 - rollup@4.60.4: + reusify@1.0.4: {} + + rollup@4.34.9: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.34.9 + '@rollup/rollup-android-arm64': 4.34.9 + '@rollup/rollup-darwin-arm64': 4.34.9 + '@rollup/rollup-darwin-x64': 4.34.9 + '@rollup/rollup-freebsd-arm64': 4.34.9 + '@rollup/rollup-freebsd-x64': 4.34.9 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.9 + '@rollup/rollup-linux-arm-musleabihf': 4.34.9 + '@rollup/rollup-linux-arm64-gnu': 4.34.9 + '@rollup/rollup-linux-arm64-musl': 4.34.9 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.9 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.9 + '@rollup/rollup-linux-riscv64-gnu': 4.34.9 + '@rollup/rollup-linux-s390x-gnu': 4.34.9 + '@rollup/rollup-linux-x64-gnu': 4.34.9 + '@rollup/rollup-linux-x64-musl': 4.34.9 + '@rollup/rollup-win32-arm64-msvc': 4.34.9 + '@rollup/rollup-win32-ia32-msvc': 4.34.9 + '@rollup/rollup-win32-x64-msvc': 4.34.9 + fsevents: 2.3.3 + + rollup@4.44.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.4 - '@rollup/rollup-android-arm64': 4.60.4 - '@rollup/rollup-darwin-arm64': 4.60.4 - '@rollup/rollup-darwin-x64': 4.60.4 - '@rollup/rollup-freebsd-arm64': 4.60.4 - '@rollup/rollup-freebsd-x64': 4.60.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 - '@rollup/rollup-linux-arm-musleabihf': 4.60.4 - '@rollup/rollup-linux-arm64-gnu': 4.60.4 - '@rollup/rollup-linux-arm64-musl': 4.60.4 - '@rollup/rollup-linux-loong64-gnu': 4.60.4 - '@rollup/rollup-linux-loong64-musl': 4.60.4 - '@rollup/rollup-linux-ppc64-gnu': 4.60.4 - '@rollup/rollup-linux-ppc64-musl': 4.60.4 - '@rollup/rollup-linux-riscv64-gnu': 4.60.4 - '@rollup/rollup-linux-riscv64-musl': 4.60.4 - '@rollup/rollup-linux-s390x-gnu': 4.60.4 - '@rollup/rollup-linux-x64-gnu': 4.60.4 - '@rollup/rollup-linux-x64-musl': 4.60.4 - '@rollup/rollup-openbsd-x64': 4.60.4 - '@rollup/rollup-openharmony-arm64': 4.60.4 - '@rollup/rollup-win32-arm64-msvc': 4.60.4 - '@rollup/rollup-win32-ia32-msvc': 4.60.4 - '@rollup/rollup-win32-x64-gnu': 4.60.4 - '@rollup/rollup-win32-x64-msvc': 4.60.4 + '@rollup/rollup-android-arm-eabi': 4.44.0 + '@rollup/rollup-android-arm64': 4.44.0 + '@rollup/rollup-darwin-arm64': 4.44.0 + '@rollup/rollup-darwin-x64': 4.44.0 + '@rollup/rollup-freebsd-arm64': 4.44.0 + '@rollup/rollup-freebsd-x64': 4.44.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.0 + '@rollup/rollup-linux-arm-musleabihf': 4.44.0 + '@rollup/rollup-linux-arm64-gnu': 4.44.0 + '@rollup/rollup-linux-arm64-musl': 4.44.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.0 + '@rollup/rollup-linux-riscv64-gnu': 4.44.0 + '@rollup/rollup-linux-riscv64-musl': 4.44.0 + '@rollup/rollup-linux-s390x-gnu': 4.44.0 + '@rollup/rollup-linux-x64-gnu': 4.44.0 + '@rollup/rollup-linux-x64-musl': 4.44.0 + '@rollup/rollup-win32-arm64-msvc': 4.44.0 + '@rollup/rollup-win32-ia32-msvc': 4.44.0 + '@rollup/rollup-win32-x64-msvc': 4.44.0 fsevents: 2.3.3 run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + safer-buffer@2.1.2: {} scheduler@0.27.0: {} - schema-utils@4.3.3: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.20.0 - ajv-formats: 2.1.1(ajv@8.20.0) - ajv-keywords: 5.1.0(ajv@8.20.0) + semver@6.3.1: {} - semver@7.8.5: {} + semver@7.7.3: {} - sharp@0.34.5: + set-function-length@1.2.2: dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.8.5 + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + sharp@0.34.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.3 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 optional: true + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + slash@5.1.0: {} source-map-js@1.2.1: {} @@ -5622,41 +8177,114 @@ snapshots: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + optional: true - source-map@0.6.1: {} + source-map@0.6.1: + optional: true - source-map@0.7.6: {} + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 stackback@0.0.2: {} - std-env@4.1.0: {} + std-env@3.7.0: {} - std-env@4.2.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 - styled-jsx@5.1.6(react@19.2.7): + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + + string.prototype.matchall@4.0.11: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.2 + set-function-name: 2.0.2 + side-channel: 1.0.6 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.3 + + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + + strip-bom@3.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(react@19.2.0): dependencies: client-only: 0.0.1 - react: 19.2.7 + react: 19.2.0 - sucrase@3.35.1: + sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/gen-mapping': 0.3.5 commander: 4.1.1 + glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.16 + pirates: 4.0.6 ts-interface-checker: 0.1.13 - supports-color@8.1.1: + supports-color@7.2.0: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} - tagged-tag@1.0.0: {} + system-architecture@0.1.0: {} - tailwindcss@3.4.19(yaml@2.9.0): + tailwindcss@3.4.14: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -5666,32 +8294,32 @@ snapshots: fast-glob: 3.3.3 glob-parent: 6.0.2 is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 + jiti: 1.21.6 + lilconfig: 2.1.0 + micromatch: 4.0.7 normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.16 - postcss-import: 15.1.0(postcss@8.5.16) - postcss-js: 4.1.0(postcss@8.5.16) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.16)(yaml@2.9.0) - postcss-nested: 6.2.0(postcss@8.5.16) + postcss: 8.4.47 + postcss-import: 15.1.0(postcss@8.4.47) + postcss-js: 4.0.1(postcss@8.4.47) + postcss-load-config: 4.0.2(postcss@8.4.47) + postcss-nested: 6.2.0(postcss@8.4.47) postcss-selector-parser: 6.1.2 - resolve: 1.22.12 - sucrase: 3.35.1 + resolve: 1.22.8 + sucrase: 3.35.0 transitivePeerDependencies: - - tsx - - yaml + - ts-node - tapable@2.3.3: {} + tapable@2.2.1: {} - terser@5.48.0: + terser@5.31.6: dependencies: - '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 + '@jridgewell/source-map': 0.3.6 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 + optional: true thenify-all@1.6.0: dependencies: @@ -5705,227 +8333,379 @@ snapshots: tinybench@2.9.0: {} - tinyclip@0.1.15: {} - tinyexec@0.3.2: {} - tinyexec@1.1.2: {} - - tinyglobby@0.2.16: + tinyglobby@0.2.12: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 - tinyglobby@0.2.17: + tinyglobby@0.2.14: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 - tinyrainbow@3.1.0: {} + tinypool@1.0.0: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.0: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} - tree-sitter-javascript@0.23.1(tree-sitter@0.25.1): + tree-sitter-javascript@0.23.1(tree-sitter@0.22.4): dependencies: - node-addon-api: 8.7.0 - node-gyp-build: 4.8.4 + node-addon-api: 8.2.2 + node-gyp-build: 4.8.2 optionalDependencies: - tree-sitter: 0.25.1 + tree-sitter: 0.22.4 - tree-sitter-typescript@0.23.2(tree-sitter@0.25.1): + tree-sitter-typescript@0.23.2(tree-sitter@0.22.4): dependencies: - node-addon-api: 8.7.0 - node-gyp-build: 4.8.4 - tree-sitter-javascript: 0.23.1(tree-sitter@0.25.1) + node-addon-api: 8.2.2 + node-gyp-build: 4.8.2 + tree-sitter-javascript: 0.23.1(tree-sitter@0.22.4) optionalDependencies: - tree-sitter: 0.25.1 + tree-sitter: 0.22.4 - tree-sitter@0.25.1: + tree-sitter@0.22.4: dependencies: - node-addon-api: 8.7.0 + node-addon-api: 8.3.0 node-gyp-build: 4.8.4 + ts-api-utils@1.3.0(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + + ts-api-utils@1.3.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + ts-interface-checker@0.1.13: {} + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@2.8.1: {} - tsup@8.5.1(jiti@2.7.0)(postcss@8.5.25)(typescript@5.9.3)(yaml@2.9.0): + tsup@8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.1)(typescript@5.5.4)(yaml@2.6.0): dependencies: - bundle-require: 5.1.0(esbuild@0.27.7) + bundle-require: 5.1.0(esbuild@0.25.0) cac: 6.7.14 chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.7 + consola: 3.4.0 + debug: 4.4.0 + esbuild: 0.25.0 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.25)(yaml@2.9.0) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.1)(yaml@2.6.0) resolve-from: 5.0.0 - rollup: 4.60.4 - source-map: 0.7.6 - sucrase: 3.35.1 + rollup: 4.34.9 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 tinyexec: 0.3.2 - tinyglobby: 0.2.16 + tinyglobby: 0.2.12 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.25 - typescript: 5.9.3 + postcss: 8.5.6 + typescript: 5.5.4 transitivePeerDependencies: - jiti - supports-color - tsx - yaml - turbo@2.10.8: + tsx@4.19.1: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.10.0 optionalDependencies: - '@turbo/darwin-64': 2.10.8 - '@turbo/darwin-arm64': 2.10.8 - '@turbo/linux-64': 2.10.8 - '@turbo/linux-arm64': 2.10.8 - '@turbo/windows-64': 2.10.8 - '@turbo/windows-arm64': 2.10.8 + fsevents: 2.3.3 + optional: true + + turbo-darwin-64@2.5.4: + optional: true + + turbo-darwin-arm64@2.5.4: + optional: true + + turbo-linux-64@2.5.4: + optional: true + + turbo-linux-arm64@2.5.4: + optional: true + + turbo-windows-64@2.5.4: + optional: true + + turbo-windows-arm64@2.5.4: + optional: true + + turbo@2.5.4: + optionalDependencies: + turbo-darwin-64: 2.5.4 + turbo-darwin-arm64: 2.5.4 + turbo-linux-64: 2.5.4 + turbo-linux-arm64: 2.5.4 + turbo-windows-64: 2.5.4 + turbo-windows-arm64: 2.5.4 typanion@3.14.0: {} - type-fest@5.6.0: + type-check@0.4.0: dependencies: - tagged-tag: 1.0.0 + prelude-ls: 1.2.1 - typescript@5.9.3: {} + type-fest@4.36.0: {} - typescript@6.0.3: {} + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 - ufo@1.6.4: {} + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-byte-offset@1.0.2: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + + typescript@5.5.4: {} + + typescript@5.6.3: {} + + ufo@1.5.4: {} + + ufo@1.6.1: {} + + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 uncrypto@0.1.3: {} + undici-types@5.26.5: {} + undici-types@6.21.0: {} - undici-types@7.24.6: {} + unicorn-magic@0.3.0: {} - unicorn-magic@0.4.0: {} + universal-user-agent@7.0.2: {} - universal-user-agent@7.0.3: {} - - untun@0.2.2: {} - - update-browserslist-db@1.2.3(browserslist@4.28.2): + untun@0.1.3: dependencies: - browserslist: 4.28.2 + citty: 0.1.6 + consola: 3.2.3 + pathe: 1.1.2 + + update-browserslist-db@1.1.1(browserslist@4.24.4): + dependencies: + browserslist: 4.24.4 escalade: 3.2.0 picocolors: 1.1.1 - uqr@0.1.3: {} + uqr@0.1.2: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 util-deprecate@1.0.2: {} - vite@8.2.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0): + vite-node@2.0.5(@types/node@20.19.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6): dependencies: - lightningcss: 1.33.0(patch_hash=1d4a8800d60d13d42887b88b3a86576df4b451670308145fb432ec8abbf40930) - picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.2.1 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 22.20.1 - esbuild: 0.27.7 - fsevents: 2.3.3 - jiti: 2.7.0 - terser: 5.48.0 - yaml: 2.9.0 + cac: 6.7.14 + debug: 4.4.3 + pathe: 1.1.2 + tinyrainbow: 1.2.0 + vite: 5.4.0(@types/node@20.19.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser - vite@8.2.0(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0): + vite@5.4.0(@types/node@20.19.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6): dependencies: - lightningcss: 1.33.0(patch_hash=1d4a8800d60d13d42887b88b3a86576df4b451670308145fb432ec8abbf40930) - picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.2.1 - tinyglobby: 0.2.17 + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.44.0 optionalDependencies: - '@types/node': 25.9.1 - esbuild: 0.27.7 + '@types/node': 20.19.1 fsevents: 2.3.3 - jiti: 2.7.0 - terser: 5.48.0 - yaml: 2.9.0 + lightningcss: 1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) + terser: 5.31.6 - vitest@4.1.10(@types/node@22.20.1)(vite@8.2.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)): + vite@7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0): dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 + esbuild: 0.25.0 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.6 + rollup: 4.44.0 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 20.19.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) + terser: 5.31.6 + tsx: 4.19.1 + yaml: 2.6.0 + + vitest@2.0.5(@types/node@20.19.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6): + dependencies: + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + debug: 4.3.6 + execa: 8.0.1 magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.1.0 + pathe: 1.1.2 + std-env: 3.7.0 tinybench: 2.9.0 - tinyexec: 1.1.2 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.2.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) + tinypool: 1.0.0 + tinyrainbow: 1.2.0 + vite: 5.4.0(@types/node@20.19.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6) + vite-node: 2.0.5(@types/node@20.19.1)(lightningcss@1.30.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.20.1 + '@types/node': 20.19.1 transitivePeerDependencies: - - msw - - watchpack@2.5.2: - dependencies: - graceful-fs: 4.2.11 - - webpack-sources@3.5.1: {} - - webpack@5.109.2(esbuild@0.27.7)(postcss@8.5.25): - dependencies: - '@types/estree': 1.0.9 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - browserslist: 4.28.2 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.5 - es-module-lexer: 2.1.0 - eslint-scope: 5.1.1 - events: 3.3.0 - graceful-fs: 4.2.11 - mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(esbuild@0.27.7)(postcss@8.5.25)(webpack@5.109.2(esbuild@0.27.7)(postcss@8.5.25)) - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.3 - watchpack: 2.5.2 - webpack-sources: 3.5.1 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser + - less - lightningcss - - postcss - - uglify-js + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + webidl-conversions@4.0.2: {} + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + which-builtin-type@1.1.4: + dependencies: + function.prototype.name: 1.1.6 + has-tostringtag: 1.0.2 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.0.2 + is-generator-function: 1.0.10 + is-regex: 1.1.4 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.3 + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - yaml@2.9.0: {} + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + yallist@3.1.1: {} + + yaml@2.6.0: {} + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bcddef126..8f662c07f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,50 +5,15 @@ packages: - 'playgrounds/*' - 'integrations' -patchedDependencies: - '@napi-rs/cli@3.7.4': patches/@napi-rs__cli@3.7.4.patch - '@parcel/watcher@2.6.0': patches/@parcel__watcher@2.6.0.patch - lightningcss@1.33.0: patches/lightningcss@1.33.0.patch - catalog: - '@types/node': 22.20.1 - dedent: 1.7.2 - enhanced-resolve: ^5.24.5 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - lightningcss: 1.33.0 - '@parcel/watcher-darwin-arm64': 2.6.0 - '@parcel/watcher-darwin-x64': 2.6.0 - '@parcel/watcher-linux-arm64-glibc': 2.6.0 - '@parcel/watcher-linux-arm64-musl': 2.6.0 - '@parcel/watcher-linux-x64-glibc': 2.6.0 - '@parcel/watcher-linux-x64-musl': 2.6.0 - '@parcel/watcher-win32-x64': 2.6.0 - '@parcel/watcher': 2.6.0 - postcss: ^8.5.25 - prettier: 3.9.6 - vite: ^8.2.0 - webpack: ^5.109.2 - -onlyBuiltDependencies: - - '@parcel/watcher' - - bun - - esbuild - - sharp - - tree-sitter - - tree-sitter-javascript - - tree-sitter-typescript - -allowBuilds: - '@parcel/watcher': true - bun: true - esbuild: true - sharp: true - tree-sitter: true - tree-sitter-javascript: true - tree-sitter-typescript: true + '@types/node': ^20.19.0 + prettier: 3.6.2 + vite: ^7.0.0 + lightningcss: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f92020c65..aaba1588e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.95.0" +channel = "1.85.0" profile = "default" diff --git a/scripts/pack-packages.mjs b/scripts/pack-packages.mjs index 2b0b9f15d..a3d34da6f 100644 --- a/scripts/pack-packages.mjs +++ b/scripts/pack-packages.mjs @@ -21,25 +21,18 @@ let workspaces = new Map() for (let path of paths) { let pkg = await fs.readFile(path, 'utf8').then(JSON.parse) if (pkg.private) continue - workspaces.set(pkg.name, { - version: pkg.version ?? '', - dir: dirname(path), - hasBundledDependencies: pkg.bundledDependencies?.length > 0 ?? false, - }) + workspaces.set(pkg.name, { version: pkg.version ?? '', dir: dirname(path) }) } // Clean dist folder await fs.rm(path.join(root, 'dist'), { recursive: true, force: true }) Promise.all( - [...workspaces.entries()].map(async ([name, { dir, hasBundledDependencies }]) => { + [...workspaces.entries()].map(async ([name, { dir }]) => { function pack() { return new Promise((resolve) => { - // `bundledDependencies` can only be packed with the hoisted node linker - // since pnpm v10. We opt into it for those packages only, instead of - // changing the node linker for the entire workspace. exec( - `pnpm pack --pack-gzip-level=0 --pack-destination="${path.join(root, 'dist').replace(/\\/g, '\\\\')}"${hasBundledDependencies ? ' --config.node-linker=hoisted' : ''}`, + `pnpm pack --pack-gzip-level=0 --pack-destination="${path.join(root, 'dist').replace(/\\/g, '\\\\')}"`, { cwd: dir }, (err, stdout, stderr) => { if (err) { diff --git a/turbo.json b/turbo.json index c48410b29..87eba2732 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://v2-10-8.turborepo.dev/schema.json", + "$schema": "https://turbo.build/schema.json", "ui": "tui", "tasks": { "@tailwindcss/oxide#build": { diff --git a/vitest.config.mts b/vitest.config.mts deleted file mode 100644 index 3ef999be2..000000000 --- a/vitest.config.mts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - projects: ['./packages/*', '!./packages/tsconfig.base.json'], - exclude: ['**/*.spec.?(c|m)[jt]s?(x)', 'integrations/**/*'], - }, -}) diff --git a/vitest.workspace.ts b/vitest.workspace.ts new file mode 100644 index 000000000..4ccde0667 --- /dev/null +++ b/vitest.workspace.ts @@ -0,0 +1 @@ +export default ['packages/*']