diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 93e104db8..63f79fa10 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,12 +1,48 @@ # Contributing +## Requirements + +Before getting started, ensure your system has access to the following tools: + +- [Node.js](https://nodejs.org/) +- [Rustup](https://rustup.rs/) +- [pnpm](https://pnpm.io/) + +## Getting started + +```sh +# Install dependencies +pnpm install + +# Install Rust toolchain and WASM targets +rustup default stable +rustup target add wasm32-wasip1-threads + +# Build the project +pnpm build +``` + +## Development workflow + +During development, you can run tests in watch mode: + +```sh +pnpm tdd +``` + +The `playgrounds` directory contains example projects you can use to test your changes. To start the Vite playground, use: + +```sh +pnpm build && pnpm vite +``` + ## Bug fixes If you've found a bug in Tailwind that you'd like to fix, [submit a pull request](https://github.com/tailwindlabs/tailwindcss/pulls) with your changes. Include a helpful description of the problem and how your changes address it, and provide tests so we can verify the fix works as expected. ## New features -If there's a new feature you'd like to see added to Tailwind, [share your idea with us](https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas) in our discussion forum to get it on our radar as something to consider for a future release. +If there's a new feature you'd like to see added to Tailwind, [share your idea with us](https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas) in our discussion forum to get it on our radar as something to consider for a future release before starting work on it. **Please note that we don't often accept pull requests for new features.** Adding a new feature to Tailwind requires us to think through the entire problem ourselves to make sure we agree with the proposed API, which means the feature needs to be high on our own priority list for us to be able to give it the attention it needs. @@ -14,7 +50,7 @@ If you open a pull request for a new feature, we're likely to close it not becau ## Coding standards -Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindcss/tailwindcss/blob/main/package.json). You can check your code against these standards by running: +Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindlabs/tailwindcss/blob/main/package.json). You can check your code against these standards by running: ```sh pnpm run lint @@ -28,10 +64,40 @@ pnpm run format ## Running tests -You can run the test suite using the following commands: +You can run the TypeScript and Rust test suites using the following command: ```sh -pnpm build && pnpm test +pnpm test ``` -Please ensure that the tests are passing when submitting a pull request. If you're adding new features to Tailwind, please include tests. +To run the integration tests, use: + +```sh +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: + +```sh +pnpm build && pnpm test:ui +``` + +Please ensure that all tests are passing when submitting a pull request. If you're adding new features to Tailwind CSS, always include tests. + +After a successful build, you can also use the npm package tarballs created inside the `dist/` folder to install your build in other local projects. + +## Pull request process + +When submitting a pull request: + +- Ensure the pull request title and description explain the changes you made and why you made them. +- Include a test plan section that outlines how you tested your contributions. We do not accept contributions without tests. +- Ensure all tests pass. You can add the tag `[ci-all]` in your pull request description to run the test suites across all platforms. + +When a pull request is created, Tailwind CSS maintainers will be notified automatically. + +## Communication + +- **GitHub discussions**: For feature ideas and general questions +- **GitHub issues**: For bug reports +- **GitHub pull requests**: For code contributions diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..848c851b1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://tailwindcss.com/sponsor'] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ce7fafb97..aa0793aa3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,8 +4,26 @@ **Please ask first before starting work on any significant new features.** -It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create an issue to first discuss any significant new features. This includes things like adding new utilities, creating new at-rules, or adding new component examples to the documentation. +It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create a discussion to first discuss any significant new features. -https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md +For more info, check out the contributing guide: + +https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md + +--> + +## Summary + + + +## Test plan + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ebe99d27..5ae59e80f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,12 +8,18 @@ on: permissions: contents: read +env: + NODE_VERSION: 24 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: strategy: fail-fast: false matrix: - node-version: [20] runner: - name: Windows os: windows-latest @@ -21,12 +27,14 @@ jobs: - name: Linux os: namespace-profile-default + # Playwright 1.62+ dropped WebKit support for macOS 14, and hangs + # instead of failing when launching WebKit on a macos-14 runner. - name: macOS - os: macos-14 + os: macos-15 # Exclude windows and macos from being built on feature branches run-all: - - ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') }} + - ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') || github.event.pull_request.user.login == 'depfu[bot]' }} exclude: - run-all: false runner: @@ -41,21 +49,21 @@ jobs: name: ${{ matrix.runner.name }} steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' + persist-credentials: false + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: ${{ env.NODE_VERSION }} # Cargo already skips downloading dependencies if they already exist - name: Cache cargo - uses: actions/cache@v4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | - ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ @@ -64,17 +72,25 @@ jobs: # Cache the `oxide` Rust build - name: Cache oxide build - uses: actions/cache@v4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | - ./target/ ./crates/node/*.node - ./crates/node/index.js + ./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 }}-oxide-${{ hashFiles('./crates/**/*') }} + - name: Setup WASM target + run: rustup target add wasm32-wasip1-threads + - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile - name: Build run: pnpm run build @@ -96,9 +112,13 @@ jobs: - name: Run Playwright tests run: npm run test:ui + notify: + if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }} + needs: tests + runs-on: ubuntu-latest + steps: - name: Notify Discord - if: failure() && github.ref == 'refs/heads/main' - uses: discord-actions/message@v2 + uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2 with: webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }} - message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' + message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index acd73ba37..457c38424 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -8,13 +8,18 @@ on: permissions: contents: read +env: + NODE_VERSION: 24 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: strategy: fail-fast: false matrix: - node-version: [20] - runner: - name: Windows os: windows-latest @@ -30,7 +35,7 @@ jobs: - vite - cli - postcss - - workers + - oxide - webpack # Exclude windows and macos from being built on feature branches @@ -50,21 +55,25 @@ jobs: name: ${{ matrix.runner.name }} / ${{ matrix.integration }} steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' + persist-credentials: false + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - 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 + with: + node-version: ${{ env.NODE_VERSION }} # Cargo already skips downloading dependencies if they already exist - name: Cache cargo - uses: actions/cache@v4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | - ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ @@ -73,15 +82,23 @@ jobs: # Cache the `oxide` Rust build - name: Cache oxide build - uses: actions/cache@v4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | - ./target/ ./crates/node/*.node - ./crates/node/index.js + ./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 }}-oxide-${{ hashFiles('./crates/**/*') }} + - name: Setup WASM target + run: rustup target add wasm32-wasip1-threads + - name: Install dependencies run: pnpm install @@ -96,9 +113,13 @@ jobs: env: GITHUB_WORKSPACE: ${{ github.workspace }} + notify: + if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }} + needs: tests + runs-on: ubuntu-latest + steps: - name: Notify Discord - if: failure() && github.ref == 'refs/heads/main' - uses: discord-actions/message@v2 + uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2 with: webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }} - message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' + message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index ce0ad15aa..24e829afd 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -2,18 +2,29 @@ name: Prepare Release on: workflow_dispatch: + inputs: + dry_run: + description: Skip creating the draft GitHub release + required: false + default: true + type: boolean push: tags: - 'v*' env: APP_NAME: tailwindcss-oxide - NODE_VERSION: 20 + NODE_VERSION: 24 + PNPM_VERSION: '11.9.0' OXIDE_LOCATION: ./crates/node permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: strategy: @@ -43,96 +54,97 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu strip: strip - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + build-flags: --use-napi-cross - os: ubuntu-latest target: aarch64-unknown-linux-gnu - strip: llvm-strip - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + strip: aarch64-linux-gnu-strip + build-flags: --use-napi-cross - os: ubuntu-latest target: armv7-unknown-linux-gnueabihf - strip: llvm-strip - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig + strip: arm-linux-gnueabihf-strip + build-flags: --use-napi-cross - os: ubuntu-latest target: aarch64-unknown-linux-musl - strip: aarch64-linux-musl-strip - download: true - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + strip-zig: true + build-flags: -x - os: ubuntu-latest target: x86_64-unknown-linux-musl strip: strip - download: true - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + build-flags: -x - name: Build ${{ matrix.target }} (OXIDE) + name: Build ${{ matrix.target }} (oxide) runs-on: ${{ matrix.os }} - container: ${{ matrix.container }} timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: ${{ env.PNPM_VERSION }} - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' + package-manager-cache: false - # 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: | - ./oxide/target/ - ./crates/node/*.node - ./crates/node/index.js - ./crates/node/index.d.ts - key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} - - - name: Install Node.JS - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install Rust (Stable) - if: ${{ matrix.download }} + - name: Install gcc-arm-linux-gnueabihf + if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }} run: | - rustup default stable + 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' }} + 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 - name: Setup rust target run: rustup target add ${{ matrix.target }} - name: Install dependencies - run: pnpm install --ignore-scripts --filter=!./playgrounds/* + run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* - name: Build release - run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build + run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }} env: RUST_TARGET: ${{ matrix.target }} JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }} - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 - if: ${{ matrix.strip }} - run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node + if: ${{ matrix.strip || matrix.strip-zig }} + env: + STRIP_COMMAND: ${{ matrix.strip }} + STRIP_ZIG: ${{ matrix.strip-zig }} + run: | + if [ "$STRIP_ZIG" = "true" ]; then + for file in ${{ env.OXIDE_LOCATION }}/*.node; do + zig objcopy --strip-all "$file" "$file.stripped" + mv "$file.stripped" "$file" + done + exit 0 + fi + + eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node" - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: bindings-${{ matrix.target }} path: ${{ env.OXIDE_LOCATION }}/*.node @@ -142,9 +154,11 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Build FreeBSD - uses: cross-platform-actions/action@v0.25.0 + uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0 env: DEBUG: napi:* RUSTUP_HOME: /usr/local/rustup @@ -160,23 +174,23 @@ jobs: shell: bash run: | sudo pkg install -y -f curl node libnghttp2 npm - sudo npm install -g pnpm@9.6.0 --unsafe-perm=true + sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init chmod +x rustup-init ./rustup-init -y --profile minimal source "$HOME/.cargo/env" - pnpm install --ignore-scripts --filter=!./playgrounds/* || true + pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true echo "~~~~ rustc --version ~~~~" rustc --version echo "~~~~ node -v ~~~~" node -v echo "~~~~ pnpm --version ~~~~" pnpm --version - pnpm run --filter ${{ env.OXIDE_LOCATION }} build + pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform strip -x ${{ env.OXIDE_LOCATION }}/*.node ls -la ${{ env.OXIDE_LOCATION }} - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: bindings-x86_64-unknown-freebsd path: ${{ env.OXIDE_LOCATION }}/*.node @@ -187,18 +201,17 @@ jobs: name: Build and release Tailwind CSS permissions: - contents: write # for softprops/action-gh-release to create GitHub release - # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions - id-token: write + contents: write # Required for creating releases needs: - build - build-freebsd steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 20 + persist-credentials: false - run: git fetch --tags -f @@ -207,43 +220,25 @@ jobs: run: | echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: ${{ env.PNPM_VERSION }} - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' registry-url: 'https://registry.npmjs.org' + 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: | - ./oxide/target/ - ./crates/node/*.node - ./crates/node/index.js - ./crates/node/index.d.ts - 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 + run: pnpm --filter=!./playgrounds/* install --frozen-lockfile - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: path: ${{ env.OXIDE_LOCATION }} @@ -281,25 +276,32 @@ jobs: echo "$RELEASE_NOTES" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - - name: Upload Standalone Artifacts - uses: actions/upload-artifact@v4 + - name: Upload standalone artifacts + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: tailwindcss-standalone path: packages/@tailwindcss-standalone/dist/ - - name: Prepare GitHub Release - uses: softprops/action-gh-release@v2 + - name: Upload npm package tarballs + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 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 + 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 diff --git a/.github/workflows/release-insiders.yml b/.github/workflows/release-insiders.yml deleted file mode 100644 index 5ad182607..000000000 --- a/.github/workflows/release-insiders.yml +++ /dev/null @@ -1,290 +0,0 @@ -name: Release Insiders - -on: - push: - branches: [main] - -permissions: - contents: read - -env: - APP_NAME: tailwindcss-oxide - NODE_VERSION: 20 - OXIDE_LOCATION: ./crates/node - RELEASE_CHANNEL: insiders - -jobs: - build: - strategy: - matrix: - include: - # Windows - - os: windows-latest - target: x86_64-pc-windows-msvc - - os: windows-latest - target: aarch64-pc-windows-msvc - # macOS - - os: macos-latest - target: x86_64-apple-darwin - strip: strip -x # Must use -x on macOS. This produces larger results on linux. - - os: macos-latest - target: aarch64-apple-darwin - page-size: 14 - strip: strip -x # Must use -x on macOS. This produces larger results on linux. - # Android - - os: ubuntu-latest - target: aarch64-linux-android - strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip - - os: ubuntu-latest - target: armv7-linux-androideabi - strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip - # Linux - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - strip: strip - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian - - os: ubuntu-latest - target: aarch64-unknown-linux-gnu - strip: llvm-strip - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - - os: ubuntu-latest - target: armv7-unknown-linux-gnueabihf - strip: llvm-strip - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig - - os: ubuntu-latest - target: aarch64-unknown-linux-musl - strip: aarch64-linux-musl-strip - download: true - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine - - os: ubuntu-latest - target: x86_64-unknown-linux-musl - strip: strip - download: true - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine - - name: Build ${{ matrix.target }} (OXIDE) - runs-on: ${{ matrix.os }} - container: ${{ matrix.container }} - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' - - # 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: | - ./oxide/target/ - ./crates/node/*.node - ./crates/node/index.js - ./crates/node/index.d.ts - 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 - 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 - 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: | - ./oxide/target/ - ./crates/node/*.node - ./crates/node/index.js - ./crates/node/index.d.ts - key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} - - - 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: Publish - run: pnpm --recursive publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks - 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: 'play.tailwindcss.com', - ref: 'master', - workflow_id: 'upgrade-tailwindcss.yml' - }) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a936e25e3..2c957d3af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,18 +1,34 @@ name: Release on: + push: + branches: [main] release: types: [published] workflow_dispatch: + inputs: + channel: + description: Release channel to publish + required: true + default: insiders + type: choice + options: + - insiders + - release permissions: contents: read env: APP_NAME: tailwindcss-oxide - NODE_VERSION: 20 + NODE_VERSION: 24 + PNPM_VERSION: '11.9.0' OXIDE_LOCATION: ./crates/node +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: strategy: @@ -42,96 +58,97 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu strip: strip - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + build-flags: --use-napi-cross - os: ubuntu-latest target: aarch64-unknown-linux-gnu - strip: llvm-strip - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + strip: aarch64-linux-gnu-strip + build-flags: --use-napi-cross - os: ubuntu-latest target: armv7-unknown-linux-gnueabihf - strip: llvm-strip - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig + strip: arm-linux-gnueabihf-strip + build-flags: --use-napi-cross - os: ubuntu-latest target: aarch64-unknown-linux-musl - strip: aarch64-linux-musl-strip - download: true - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + strip-zig: true + build-flags: -x - os: ubuntu-latest target: x86_64-unknown-linux-musl strip: strip - download: true - container: - image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + build-flags: -x - name: Build ${{ matrix.target }} (OXIDE) + name: Build ${{ matrix.target }} (oxide) runs-on: ${{ matrix.os }} - container: ${{ matrix.container }} timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: ${{ env.PNPM_VERSION }} - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' + package-manager-cache: false - # 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: | - ./oxide/target/ - ./crates/node/*.node - ./crates/node/index.js - ./crates/node/index.d.ts - key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} - - - name: Install Node.JS - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install Rust (Stable) - if: ${{ matrix.download }} + - name: Install gcc-arm-linux-gnueabihf + if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }} run: | - rustup default stable + 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' }} + 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 - name: Setup rust target run: rustup target add ${{ matrix.target }} - name: Install dependencies - run: pnpm install --ignore-scripts --filter=!./playgrounds/* + run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* - name: Build release - run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build + run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }} env: RUST_TARGET: ${{ matrix.target }} JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }} - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 - if: ${{ matrix.strip }} - run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node + if: ${{ matrix.strip || matrix.strip-zig }} + env: + STRIP_COMMAND: ${{ matrix.strip }} + STRIP_ZIG: ${{ matrix.strip-zig }} + run: | + if [ "$STRIP_ZIG" = "true" ]; then + for file in ${{ env.OXIDE_LOCATION }}/*.node; do + zig objcopy --strip-all "$file" "$file.stripped" + mv "$file.stripped" "$file" + done + exit 0 + fi + + eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node" - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: bindings-${{ matrix.target }} path: ${{ env.OXIDE_LOCATION }}/*.node @@ -141,9 +158,11 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Build FreeBSD - uses: cross-platform-actions/action@v0.25.0 + uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0 env: DEBUG: napi:* RUSTUP_HOME: /usr/local/rustup @@ -159,7 +178,7 @@ jobs: shell: bash run: | sudo pkg install -y -f curl node libnghttp2 npm - sudo npm install -g pnpm@9.6.0 --unsafe-perm=true + sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init chmod +x rustup-init ./rustup-init -y --profile minimal @@ -170,12 +189,12 @@ jobs: node -v echo "~~~~ pnpm --version ~~~~" pnpm --version - pnpm install --ignore-scripts --filter=!./playgrounds/* || true - pnpm run --filter ${{ env.OXIDE_LOCATION }} build + pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true + pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform strip -x ${{ env.OXIDE_LOCATION }}/*.node ls -la ${{ env.OXIDE_LOCATION }} - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: bindings-x86_64-unknown-freebsd path: ${{ env.OXIDE_LOCATION }}/*.node @@ -183,10 +202,10 @@ jobs: release: runs-on: macos-14 timeout-minutes: 15 - name: Build and release Tailwind CSS + name: Build and publish Tailwind CSS permissions: - contents: write # for softprops/action-gh-release to create GitHub release + contents: read # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions id-token: write @@ -195,47 +214,51 @@ jobs: - build-freebsd steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 20 + persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: ${{ env.PNPM_VERSION }} - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' registry-url: 'https://registry.npmjs.org' + package-manager-cache: false - # Cargo already skips downloading dependencies if they already exist - - name: Cache cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} + # npm trusted publishing validates the caller workflow filename, so all npm publishes live here. + # This workflow rebuilds the publish artifacts instead of depending on prepare-release.yml. + - name: Resolve release metadata + env: + INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }} + run: | + if [[ "${{ github.event_name }}" == "release" || "$INPUT_CHANNEL" == "release" ]]; then + release_channel=$(node ./scripts/release-channel.js) - # Cache the `oxide` Rust build - - name: Cache oxide build - uses: actions/cache@v4 - with: - path: | - ./oxide/target/ - ./crates/node/*.node - ./crates/node/index.js - ./crates/node/index.d.ts - key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }} + echo "RELEASE_KIND=release" >> $GITHUB_ENV + echo "RELEASE_CHANNEL=$release_channel" >> $GITHUB_ENV + echo "FEATURES_ENV=stable" >> $GITHUB_ENV + else + sha_short=$(git rev-parse --short HEAD) + + echo "RELEASE_KIND=insiders" >> $GITHUB_ENV + echo "RELEASE_CHANNEL=insiders" >> $GITHUB_ENV + echo "SHA_SHORT=$sha_short" >> $GITHUB_ENV + echo "INSIDERS_VERSION=0.0.0-insiders.$sha_short" >> $GITHUB_ENV + fi + + - name: Setup WASM target + run: rustup target add wasm32-wasip1-threads - name: Install dependencies - run: pnpm --filter=!./playgrounds/* install + run: pnpm --filter=!./playgrounds/* install --frozen-lockfile - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: path: ${{ env.OXIDE_LOCATION }} @@ -255,10 +278,19 @@ jobs: cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/ cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/ + - name: 'Version based on commit: ${{ env.INSIDERS_VERSION }}' + if: env.RELEASE_KIND == 'insiders' + run: pnpm run version-packages ${INSIDERS_VERSION} + - name: Build Tailwind CSS + if: env.RELEASE_KIND == 'insiders' + run: pnpm run build + + - name: Build Tailwind CSS + if: env.RELEASE_KIND == 'release' run: pnpm run build env: - FEATURES_ENV: stable + FEATURES_ENV: ${{ env.FEATURES_ENV }} - name: Run pre-publish optimizations scripts run: node ./scripts/pre-publish-optimizations.mjs @@ -266,25 +298,61 @@ jobs: - name: Lock pre-release versions run: node ./scripts/lock-pre-release-versions.mjs - - name: Calculate environment variables - run: | - echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV - echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./packages/tailwindcss/package.json`).version);')" >> $GITHUB_ENV + - name: Upload npm package tarballs + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: npm-package-tarballs + path: dist/*.tgz - name: Publish - run: pnpm --recursive publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${RELEASE_CHANNEL} --no-git-checks + # The wasm package needs a special npm config that isn't read when pnpm --recursive is used + pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${RELEASE_CHANNEL} --no-git-checks --config.node-linker=hoisted; popd; - name: Trigger Tailwind Play update - if: env.RELEASE_CHANNEL == 'latest' - uses: actions/github-script@v7 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }} script: | await github.rest.actions.createWorkflowDispatch({ owner: 'tailwindlabs', - repo: 'play.tailwindcss.com', - ref: 'master', + repo: 'upgrades', + ref: 'main', workflow_id: 'upgrade-tailwindcss.yml' }) + + notify: + if: ${{ always() && (needs.build.result == 'failure' || needs.build-freebsd.result == 'failure' || needs.release.result == 'failure') }} + needs: + - build + - build-freebsd + - release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Resolve release label + id: release + env: + INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }} + RELEASE_TAG: ${{ github.event.release.tag_name || '' }} + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + tag_name="${RELEASE_TAG:-${GITHUB_REF_NAME}}" + echo "label=release ${tag_name}" >> $GITHUB_OUTPUT + elif [[ "$INPUT_CHANNEL" == "release" ]]; then + version=$(node -p "require('./packages/tailwindcss/package.json').version") + echo "label=release v${version}" >> $GITHUB_OUTPUT + else + sha_short=$(git rev-parse --short HEAD) + echo "label=insiders release 0.0.0-insiders.${sha_short}" >> $GITHUB_OUTPUT + fi + + - name: Notify Discord + uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2 + with: + webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }} + message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) for `${{ steps.release.outputs.label }}` has failed.' diff --git a/.npmrc b/.npmrc deleted file mode 100644 index ded82e2f6..000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -auto-install-peers = true diff --git a/.prettierignore b/.prettierignore index 3de4530dd..4f50b932f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,5 +4,6 @@ pnpm-lock.yaml target/ crates/node/index.d.ts crates/node/index.js +crates/ignore/ .next .fingerprint diff --git a/CHANGELOG.md b/CHANGELOG.md index 19ecfd3de..e5e6f6f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,490 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### 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)) + +## [4.1.16] - 2025-10-23 + +### Fixed + +- Discard candidates with an empty data type ([#19172](https://github.com/tailwindlabs/tailwindcss/pull/19172)) +- Fix canonicalization of arbitrary variants with attribute selectors ([#19176](https://github.com/tailwindlabs/tailwindcss/pull/19176)) +- Fix invalid colors due to nested `&` ([#19184](https://github.com/tailwindlabs/tailwindcss/pull/19184)) +- Improve canonicalization for `& > :pseudo` and `& :pseudo` arbitrary variants ([#19178](https://github.com/tailwindlabs/tailwindcss/pull/19178)) + +## [4.1.15] - 2025-10-20 + +### Fixed + +- Fix Safari devtools rendering issue due to `color-mix` fallback ([#19069](https://github.com/tailwindlabs/tailwindcss/pull/19069)) +- Suppress Lightning CSS warnings about `:deep`, `:slotted`, and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094)) +- Fix resolving theme keys when starting with the name of another theme key in JS configs and plugins ([#19097](https://github.com/tailwindlabs/tailwindcss/pull/19097)) +- Allow named groups in combination with `not-*`, `has-*`, and `in-*` ([#19100](https://github.com/tailwindlabs/tailwindcss/pull/19100)) +- Prevent important utilities from affecting other utilities ([#19110](https://github.com/tailwindlabs/tailwindcss/pull/19110)) +- Don’t index into strings with the `theme(…)` function ([#19111](https://github.com/tailwindlabs/tailwindcss/pull/19111)) +- Fix parsing issue when `\t` is used in at-rules ([#19130](https://github.com/tailwindlabs/tailwindcss/pull/19130)) +- Upgrade: Canonicalize utilities containing `0` values ([#19095](https://github.com/tailwindlabs/tailwindcss/pull/19095)) +- Upgrade: Migrate deprecated `break-words` to `wrap-break-word` ([#19157](https://github.com/tailwindlabs/tailwindcss/pull/19157)) + +### Changed + +- Remove the `postinstall` script from oxide ([#19149])(https://github.com/tailwindlabs/tailwindcss/pull/19149) + +## [4.1.14] - 2025-10-01 + +### Fixed + +- Handle `'` syntax in ClojureScript when extracting classes ([#18888](https://github.com/tailwindlabs/tailwindcss/pull/18888)) +- Handle `@variant` inside `@custom-variant` ([#18885](https://github.com/tailwindlabs/tailwindcss/pull/18885)) +- Merge suggestions when using `@utility` ([#18900](https://github.com/tailwindlabs/tailwindcss/pull/18900)) +- Ensure that file system watchers created when using the CLI are always cleaned up ([#18905](https://github.com/tailwindlabs/tailwindcss/pull/18905)) +- Do not generate `grid-column` utilities when configuring `grid-column-start` or `grid-column-end` ([#18907](https://github.com/tailwindlabs/tailwindcss/pull/18907)) +- Do not generate `grid-row` utilities when configuring `grid-row-start` or `grid-row-end` ([#18907](https://github.com/tailwindlabs/tailwindcss/pull/18907)) +- Prevent duplicate CSS when overwriting a static utility with a theme key ([#18056](https://github.com/tailwindlabs/tailwindcss/pull/18056)) +- Show Lightning CSS warnings (if any) when optimizing/minifying ([#18918](https://github.com/tailwindlabs/tailwindcss/pull/18918)) +- Use `default` export condition for `@tailwindcss/vite` ([#18948](https://github.com/tailwindlabs/tailwindcss/pull/18948)) +- Re-throw errors from PostCSS nodes ([#18373](https://github.com/tailwindlabs/tailwindcss/pull/18373)) +- Detect classes in markdown inline directives ([#18967](https://github.com/tailwindlabs/tailwindcss/pull/18967)) +- Ensure files with only `@theme` produce no output when built ([#18979](https://github.com/tailwindlabs/tailwindcss/pull/18979)) +- Support Maud templates when extracting classes ([#18988](https://github.com/tailwindlabs/tailwindcss/pull/18988)) +- Upgrade: Do not migrate `variant = 'outline'` during upgrades ([#18922](https://github.com/tailwindlabs/tailwindcss/pull/18922)) +- Upgrade: Show version mismatch (if any) when running upgrade tool ([#19028](https://github.com/tailwindlabs/tailwindcss/pull/19028)) +- 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 + +- Drop warning from browser build ([#18731](https://github.com/tailwindlabs/tailwindcss/issues/18731)) +- Drop exact duplicate declarations when emitting CSS ([#18809](https://github.com/tailwindlabs/tailwindcss/issues/18809)) + +### Fixed + +- Don't transition `visibility` when using `transition` ([#18795](https://github.com/tailwindlabs/tailwindcss/pull/18795)) +- Discard matched variants with unknown named values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799)) +- Discard matched variants with non-string values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799)) +- Show suggestions for known `matchVariant` values ([#18798](https://github.com/tailwindlabs/tailwindcss/pull/18798)) +- Replace deprecated `clip` with `clip-path` in `sr-only` ([#18769](https://github.com/tailwindlabs/tailwindcss/pull/18769)) +- Hide internal fields from completions in `matchUtilities` ([#18820](https://github.com/tailwindlabs/tailwindcss/pull/18820)) +- Ignore `.vercel` folders by default (can be overridden by `@source …` rules) ([#18855](https://github.com/tailwindlabs/tailwindcss/pull/18855)) +- Consider variants starting with `@-` to be invalid (e.g. `@-2xl:flex`) ([#18869](https://github.com/tailwindlabs/tailwindcss/pull/18869)) +- Do not allow custom variants to start or end with a `-` or `_` ([#18867](https://github.com/tailwindlabs/tailwindcss/pull/18867), [#18872](https://github.com/tailwindlabs/tailwindcss/pull/18872)) +- Upgrade: Migrate `aria` theme keys to `@custom-variant` ([#18815](https://github.com/tailwindlabs/tailwindcss/pull/18815)) +- Upgrade: Migrate `data` theme keys to `@custom-variant` ([#18816](https://github.com/tailwindlabs/tailwindcss/pull/18816)) +- Upgrade: Migrate `supports` theme keys to `@custom-variant` ([#18817](https://github.com/tailwindlabs/tailwindcss/pull/18817)) + +## [4.1.12] - 2025-08-13 + +### Fixed + +- Don't consider the global important state in `@apply` ([#18404](https://github.com/tailwindlabs/tailwindcss/pull/18404)) +- Add missing suggestions for `flex-` utilities ([#18642](https://github.com/tailwindlabs/tailwindcss/pull/18642)) +- Fix trailing `)` from interfering with extraction in Clojure keywords ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345)) +- Detect classes inside Elixir charlist, word list, and string sigils ([#18432](https://github.com/tailwindlabs/tailwindcss/pull/18432)) +- Track source locations through `@plugin` and `@config` ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345)) +- Allow boolean values of `process.env.DEBUG` in `@tailwindcss/node` ([#18485](https://github.com/tailwindlabs/tailwindcss/pull/18485)) +- Ignore consecutive semicolons in the CSS parser ([#18532](https://github.com/tailwindlabs/tailwindcss/pull/18532)) +- Center the dropdown icon added to an input with a paired datalist by default ([#18511](https://github.com/tailwindlabs/tailwindcss/pull/18511)) +- Extract candidates in Slang templates ([#18565](https://github.com/tailwindlabs/tailwindcss/pull/18565)) +- Improve error messages when encountering invalid functional utility names ([#18568](https://github.com/tailwindlabs/tailwindcss/pull/18568)) +- Discard CSS AST objects with `false` or `undefined` properties ([#18571](https://github.com/tailwindlabs/tailwindcss/pull/18571)) +- Allow users to disable URL rebasing in `@tailwindcss/postcss` via `transformAssetUrls: false` ([#18321](https://github.com/tailwindlabs/tailwindcss/pull/18321)) +- Fix false-positive migrations in `addEventListener` and JavaScript variable names ([#18718](https://github.com/tailwindlabs/tailwindcss/pull/18718)) +- Fix Standalone CLI showing default Bun help when run via symlink on Windows ([#18723](https://github.com/tailwindlabs/tailwindcss/pull/18723)) +- Read from `--border-color-*` theme keys in `divide-*` utilities for backwards compatibility ([#18704](https://github.com/tailwindlabs/tailwindcss/pull/18704/)) +- Don't scan `.hdr` and `.exr` files for classes by default ([#18734](https://github.com/tailwindlabs/tailwindcss/pull/18734)) + +## [4.1.11] - 2025-06-26 + +### Fixed + +- Add heuristic to skip candidate migrations inside `emit(…)` ([#18330](https://github.com/tailwindlabs/tailwindcss/pull/18330)) +- Extract candidates with variants in Clojure/ClojureScript keywords ([#18338](https://github.com/tailwindlabs/tailwindcss/pull/18338)) +- Document `--watch=always` in the CLI's usage ([#18337](https://github.com/tailwindlabs/tailwindcss/pull/18337)) +- Add support for Vite 7 to `@tailwindcss/vite` ([#18384](https://github.com/tailwindlabs/tailwindcss/pull/18384)) + +## [4.1.10] - 2025-06-11 + +### Fixed + +- Fix incorrectly generated CSS when using percentages in arbitrary values with calc (e.g. `w-[calc(100%-var(--offset))]`) ([#18289](https://github.com/tailwindlabs/tailwindcss/pull/18289)) + +## [4.1.9] - 2025-06-11 + +### Fixed + +- Correctly parse custom properties with strings containing semicolons ([#18251](https://github.com/tailwindlabs/tailwindcss/pull/18251)) +- Upgrade: Migrate arbitrary modifiers without percentage signs to bare values (e.g. `/[0.16]` → `/16`) ([#18184](https://github.com/tailwindlabs/tailwindcss/pull/18184)) +- Upgrade: Migrate CSS variable shorthands where fallback value contains function call ([#18184](https://github.com/tailwindlabs/tailwindcss/pull/18184)) +- Upgrade: Migrate negative arbitrary values to negative bare values (e.g. `mb-[-32rem]` → `-mb-128`) ([#18212](https://github.com/tailwindlabs/tailwindcss/pull/18212)) +- Upgrade: Do not migrate `blur` in `wire:model.blur` ([#18216](https://github.com/tailwindlabs/tailwindcss/pull/18216)) +- Don't add spaces around CSS dashed idents when formatting math expressions ([#18220](https://github.com/tailwindlabs/tailwindcss/pull/18220)) + +## [4.1.8] - 2025-05-27 + +### Added + +- Improve error messages when `@apply` fails ([#18059](https://github.com/tailwindlabs/tailwindcss/pull/18059)) + +### Fixed + +- Upgrade: Do not migrate declarations that look like candidates in ` + `, + 'src/input.css': css` + @import 'tailwindcss'; + + .foo { + flex-shrink: 1; + } + + .bar { + @apply !underline; + } + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm exec upgrade') + + expect(await fs.dumpFiles('./src/**/*.{css,vue}')).toMatchInlineSnapshot(` + " + --- ./src/index.vue --- + + + + + --- ./src/input.css --- + @import 'tailwindcss'; + + .foo { + flex-shrink: 1; + } + + .bar { + @apply underline!; + } + " + `) + }, +) + function withBOM(text: string): string { return '\uFEFF' + text } diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 1c3f5d598..cab0d3d62 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { describe } from 'vitest' -import { css, html, json, test, ts } from '../utils' +import { css, html, json, test, ts, txt } from '../utils' test( `upgrade JS config files with flat theme values, darkMode, and content fields`, @@ -30,13 +30,23 @@ test( 400: '#f87171', 500: 'red', }, + superRed: '#ff0000', steel: 'rgb(70 130 180 / )', smoke: 'rgba(245, 245, 245, var(--smoke-alpha, ))', }, + ringColor: { + DEFAULT: '#c0ffee', + }, + opacity: { + superOpaque: '0.95', + }, fontSize: { xs: ['0.75rem', { lineHeight: '1rem' }], sm: ['0.875rem', { lineHeight: '1.5rem' }], base: ['1rem', { lineHeight: '2rem' }], + lg: ['1.125rem', '2.5rem'], + xl: ['1.5rem', '3rem', 'invalid'], + '2xl': ['2rem'], }, width: { px: '1px', @@ -141,9 +151,10 @@ test( } `, 'src/index.html': html` -
+
`, 'node_modules/my-external-lib/src/template.html': html`
@@ -153,14 +164,15 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.{css,js,html}')).toMatchInlineSnapshot(` " --- src/index.html ---
+ class="tracking-superWide leading-superLoose" + >
+
--- src/input.css --- @import 'tailwindcss'; @@ -178,9 +190,15 @@ test( --color-red-500: #ef4444; --color-red-600: #dc2626; + --color-superRed: #ff0000; --color-steel: rgb(70 130 180); --color-smoke: rgba(245, 245, 245, var(--smoke-alpha, 1)); + --default-ring-color: #c0ffee; + + --opacity-*: initial; + --opacity-superOpaque: 95%; + --text-*: initial; --text-xs: 0.75rem; --text-xs--line-height: 1rem; @@ -188,6 +206,11 @@ test( --text-sm--line-height: 1.5rem; --text-base: 1rem; --text-base--line-height: 2rem; + --text-lg: 1.125rem; + --text-lg--line-height: 2.5rem; + --text-xl: 1.5rem; + --text-xl--line-height: 3rem; + --text-2xl: 2rem; --width-*: initial; --width-0: 0%; @@ -247,9 +270,9 @@ test( --animate-spin-clockwise: spin-clockwise 1s linear infinite; --animate-spin-counterclockwise: spin-counterclockwise 1s linear infinite; - --tracking-super-wide: 0.25em; + --tracking-superWide: 0.25em; - --leading-super-loose: 3; + --leading-superLoose: 3; @keyframes spin-clockwise { 0% { @@ -299,6 +322,105 @@ test( }, ) +test( + 'skips gitignored template files even when they are explicitly referenced in `content`', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + '.gitignore': txt` + node_modules + src/ignored + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + module.exports = { + content: [ + './src/migrate-me.html', + './src/ignored/do-not-migrate-me.html', + './node_modules/my-external-lib/template.html', + ], + theme: {}, + plugins: [], + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + 'src/migrate-me.html': html` +
+ `, + 'src/ignored/do-not-migrate-me.html': html` +
+ `, + 'node_modules/my-external-lib/template.html': html` +
+ `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm exec upgrade') + + expect( + await fs.dumpFiles( + '{src/**/*.{css,html},node_modules/my-external-lib/template.html,.gitignore}', + ), + ).toMatchInlineSnapshot(` + " + --- .gitignore --- + node_modules + src/ignored + + --- src/input.css --- + @import 'tailwindcss'; + + @source './ignored/do-not-migrate-me.html'; + @source '../node_modules/my-external-lib/template.html'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + + --- src/migrate-me.html --- +
+ + --- node_modules/my-external-lib/template.html --- +
+ + --- src/ignored/do-not-migrate-me.html --- +
+ " + `) + }, +) + test( 'upgrades JS config files with plugins', { @@ -362,7 +484,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -464,7 +586,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(` " @@ -472,17 +594,17 @@ test( @import 'tailwindcss'; @theme { - --color-gray-50: oklch(98.5% 0 0); - --color-gray-100: oklch(97% 0 0); - --color-gray-200: oklch(92.2% 0 0); - --color-gray-300: oklch(87% 0 0); - --color-gray-400: oklch(70.8% 0 0); - --color-gray-500: oklch(55.6% 0 0); - --color-gray-600: oklch(43.9% 0 0); - --color-gray-700: oklch(37.1% 0 0); - --color-gray-800: oklch(26.9% 0 0); - --color-gray-900: oklch(20.5% 0 0); - --color-gray-950: oklch(14.5% 0 0); + --color-gray-50: oklch(98.5% 0 none); + --color-gray-100: oklch(97% 0 none); + --color-gray-200: oklch(92.2% 0 none); + --color-gray-300: oklch(87% 0 none); + --color-gray-400: oklch(70.8% 0 none); + --color-gray-500: oklch(55.6% 0 none); + --color-gray-600: oklch(43.9% 0 none); + --color-gray-700: oklch(37.1% 0 none); + --color-gray-800: oklch(26.9% 0 none); + --color-gray-900: oklch(20.5% 0 none); + --color-gray-950: oklch(14.5% 0 none); } /* @@ -550,7 +672,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -628,7 +750,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -702,7 +824,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -812,7 +934,7 @@ test( }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(` " @@ -910,7 +1032,7 @@ test( }, }, async ({ root, exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade', { + await exec('pnpm exec upgrade', { cwd: path.join(root, 'frontend'), }) @@ -947,6 +1069,252 @@ test( }, ) +test( + 'migrate aria theme keys to custom variants', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + export default { + content: { + relative: true, + files: ['./src/**/*.html'], + }, + theme: { + extend: { + aria: { + // Built-in (not really, but visible because of intellisense) + busy: 'busy="true"', + + // Automatically handled by bare values + foo: 'foo="true"', + + // Quotes are optional in CSS for these kinds of attribute + // selectors + bar: 'bar=true', + + // Not automatically handled by bare values because names differ + baz: 'qux="true"', + + // Completely custom + asc: 'sort="ascending"', + desc: 'sort="descending"', + }, + }, + }, + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm exec upgrade') + + expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @custom-variant aria-baz (&[aria-qux="true"]); + @custom-variant aria-asc (&[aria-sort="ascending"]); + @custom-variant aria-desc (&[aria-sort="descending"]); + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, +) + +test( + 'migrate data theme keys to custom variants', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + export default { + content: { + relative: true, + files: ['./src/**/*.html'], + }, + theme: { + extend: { + data: { + // Automatically handled by bare values + foo: 'foo', + + // Not automatically handled by bare values because names differ + bar: 'baz', + + // Custom + checked: 'ui~="checked"', + }, + }, + }, + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm exec upgrade') + + expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @custom-variant data-bar (&[data-baz]); + @custom-variant data-checked (&[data-ui~="checked"]); + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, +) + +test( + 'migrate supports theme keys to custom variants', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + export default { + content: { + relative: true, + files: ['./src/**/*.html'], + }, + theme: { + extend: { + supports: { + // Automatically handled by bare values (using CSS variable as the value) + foo: 'foo: var(--foo)', // parentheses are optional + bar: '(bar: var(--bar))', + + // Not automatically handled by bare values because names differ + foo: 'bar: var(--foo)', // parentheses are optional + bar: '(qux: var(--bar))', + + // Custom + grid: 'display: grid', + }, + }, + }, + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm exec upgrade') + + expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @custom-variant supports-foo { + @supports (bar: var(--foo)) { + @slot; + } + } + @custom-variant supports-bar { + @supports ((qux: var(--bar))) { + @slot; + } + } + @custom-variant supports-grid { + @supports (display: grid) { + @slot; + } + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, +) + describe('border compatibility', () => { test( 'migrate border compatibility', @@ -978,7 +1346,7 @@ describe('border compatibility', () => { }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -1040,7 +1408,7 @@ describe('border compatibility', () => { }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -1102,7 +1470,7 @@ describe('border compatibility', () => { }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -1141,7 +1509,7 @@ describe('border compatibility', () => { }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -1212,7 +1580,7 @@ describe('border compatibility', () => { }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -1315,18 +1683,18 @@ describe('border compatibility', () => { }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(` " --- src/index.html ---
--- src/input.css --- @@ -1425,18 +1793,18 @@ describe('border compatibility', () => { }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(` " --- src/index.html ---
--- src/input.css --- @@ -1527,7 +1895,7 @@ describe('border compatibility', () => { }, }, async ({ exec, fs, expect }) => { - await exec('npx @tailwindcss/upgrade') + await exec('pnpm exec upgrade') expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(` " @@ -1577,3 +1945,243 @@ describe('border compatibility', () => { }, ) }) + +test( + `future and experimental keys are supported`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + future: { + hoverOnlyWhenSupported: true, + respectDefaultRingColorOpacity: true, + disableColorOpacityUtilitiesByDefault: true, + relativeContentPathsByDefault: true, + }, + experimental: { + generalizedModifiers: true, + }, + theme: { + colors: { + red: { + 400: '#f87171', + 500: 'red', + }, + }, + }, + plugins: [], + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm exec upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @custom-variant dark (&:where(.dark, .dark *)); + + @theme { + --color-*: initial; + --color-red-400: #f87171; + --color-red-500: red; + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('') + }, +) + +test( + `unknown future keys dont migrate the config`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + future: { + something: true, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm exec upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @config '../tailwind.config.ts'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(` + "--- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + future: { + something: true, + }, + } satisfies Config" + `) + }, +) + +test( + `unknown experimental keys dont migrate the config`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + experimental: { + something: true, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm exec upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @config '../tailwind.config.ts'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(` + "--- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + experimental: { + something: true, + }, + } satisfies Config" + `) + }, +) diff --git a/integrations/upgrade/upgrade-errors.test.ts b/integrations/upgrade/upgrade-errors.test.ts new file mode 100644 index 000000000..fdc60828e --- /dev/null +++ b/integrations/upgrade/upgrade-errors.test.ts @@ -0,0 +1,254 @@ +import { stripVTControlCharacters } from 'node:util' +import { css, html, js, json, test } from '../utils' + +test( + 'upgrades half-upgraded v3 project to v4 (pnpm)', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'tailwind.config.js': js` + /** @type {import('tailwindcss').Config} */ + module.exports = { + content: ['./src/**/*.{html,js}'], + } + `, + 'src/index.html': html` +
Hello World
+ `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + retry: 0, + }, + async ({ exec, expect }) => { + // Ensure we are in a git repo + await exec('git init') + await exec('git add --all') + await exec('git commit -m "before migration"') + + // Fully upgrade to v4 + await exec('pnpm exec upgrade') + + // Undo all changes to the current repo. This will bring the repo back to a + // v3 state, but the `node_modules` will now have v4 installed. + await exec('git reset --hard HEAD') + + // Re-running the upgrade should result in an error + return expect(() => { + return exec('pnpm exec upgrade', {}, { ignoreStdErr: true }).catch((e) => { + // Replacing the current version with a hardcoded `v4` to make it stable + // when we release new minor/patch versions. + return Promise.reject( + stripVTControlCharacters(e.message.replace(/\d+\.\d+\.\d+/g, '4.0.0')), + ) + }) + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "Command failed: pnpm exec upgrade + ≈ tailwindcss v4.0.0 + + │ ↳ Upgrading from Tailwind CSS \`v4.0.0\` + + │ ↳ Version mismatch + │ + │ \`\`\`diff + │ - "tailwindcss": "^3" (expected version in \`package.json\`) + │ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`) + │ \`\`\` + │ + │ Make sure to run \`pnpm install\` and try again. + + " + `) + }, +) + +test( + 'upgrades half-upgraded v3 project to v4 (bun)', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/cli": "workspace:^", + "bun": "^1.0.0" + } + } + `, + 'tailwind.config.js': js` + /** @type {import('tailwindcss').Config} */ + module.exports = { + content: ['./src/**/*.{html,js}'], + } + `, + 'src/index.html': html` +
Hello World
+ `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + retry: 0, + }, + async ({ exec, expect }) => { + // Use `bun` to install dependencies + await exec('rm ./pnpm-lock.yaml') + try { + await exec('npx bun install', {}, { ignoreStdErr: true }) + } catch (e) { + // When preparing for a release, the version in `package.json` will point + // to a non-existent version because it's not published yet. + // TODO: Find a better approach to handle this and actually test it even + // on release branches. Note: the pnpm version _does_ work because of + // overrides in the package.json file. + if (`${e}`.includes('No version matching')) return + throw e + } + + // Ensure we are in a git repo + await exec('git init') + await exec('git add --all') + await exec('git commit -m "before migration"') + + // Fully upgrade to v4 + await exec('npx @tailwindcss/upgrade') + + // Undo all changes to the current repo. This will bring the repo back to a + // v3 state, but the `node_modules` will now have v4 installed. + await exec('git reset --hard HEAD') + + // Re-running the upgrade should result in an error + return expect(() => { + return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => { + // Replacing the current version with a hardcoded `v4` to make it stable + // when we release new minor/patch versions. + return Promise.reject( + stripVTControlCharacters(e.message.replace(/\d+\.\d+\.\d+/g, '4.0.0')), + ) + }) + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "Command failed: npx @tailwindcss/upgrade + ≈ tailwindcss v4.0.0 + + │ ↳ Upgrading from Tailwind CSS \`v4.0.0\` + + │ ↳ Version mismatch + │ + │ \`\`\`diff + │ - "tailwindcss": "^3" (expected version in \`package.json\`) + │ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`) + │ \`\`\` + │ + │ Make sure to run \`bun install\` and try again. + + " + `) + }, +) + +test( + 'upgrades half-upgraded v3 project to v4 (npm)', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'tailwind.config.js': js` + /** @type {import('tailwindcss').Config} */ + module.exports = { + content: ['./src/**/*.{html,js}'], + } + `, + 'src/index.html': html` +
Hello World
+ `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + retry: 0, + }, + async ({ exec, expect }) => { + // Use `npm` to install dependencies + await exec('rm ./pnpm-lock.yaml') + await exec('rm -rf ./node_modules') + try { + await exec('npm install', {}, { ignoreStdErr: true }) + } catch (e) { + // When preparing for a release, the version in `package.json` will point + // to a non-existent version because it's not published yet. + // TODO: Find a better approach to handle this and actually test it even + // on release branches. Note: the pnpm version _does_ work because of + // overrides in the package.json file. + if (`${e}`.includes('npm error code ETARGET')) return + throw e + } + + // Ensure we are in a git repo + await exec('git init') + await exec('git add --all') + await exec('git commit -m "before migration"') + + // Fully upgrade to v4 + await exec('npx @tailwindcss/upgrade') + + // Undo all changes to the current repo. This will bring the repo back to a + // v3 state, but the `node_modules` will now have v4 installed. + await exec('git reset --hard HEAD') + + // Re-running the upgrade should result in an error + return expect(() => { + return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => { + // Replacing the current version with a hardcoded `v4` to make it stable + // when we release new minor/patch versions. + return Promise.reject( + stripVTControlCharacters(e.message.replace(/\d+\.\d+\.\d+/g, '4.0.0')), + ) + }) + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "Command failed: npx @tailwindcss/upgrade + ≈ tailwindcss v4.0.0 + + │ ↳ Upgrading from Tailwind CSS \`v4.0.0\` + + │ ↳ Version mismatch + │ + │ \`\`\`diff + │ - "tailwindcss": "^3" (expected version in \`package.json\`) + │ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`) + │ \`\`\` + │ + │ Make sure to run \`npm install\` and try again. + + " + `) + }, +) diff --git a/integrations/utils.ts b/integrations/utils.ts index a1165b1da..9863918d1 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -1,20 +1,27 @@ import dedent from 'dedent' import fastGlob from 'fast-glob' -import { exec, spawn } from 'node:child_process' +import { exec, execFile, spawn, type ChildProcess } from 'node:child_process' import fs from 'node:fs/promises' +import { createServer } from 'node:net' import { platform, tmpdir } from 'node:os' import path from 'node:path' -import { stripVTControlCharacters } from 'node:util' +import { promisify, stripVTControlCharacters } from 'node:util' +import { RawSourceMap, SourceMapConsumer } from 'source-map-js' import { test as defaultTest, type ExpectStatic } from 'vitest' +import * as Yaml from 'yaml' +import { createLineTable } from '../packages/tailwindcss/src/source-maps/line-table' import { escape } from '../packages/tailwindcss/src/utils/escape' const REPO_ROOT = path.join(__dirname, '..') +const ROOT_PNPM_WORKSPACE = Yaml.parse( + await fs.readFile(path.join(REPO_ROOT, 'pnpm-workspace.yaml'), 'utf8'), +) const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((name) => name.replace('tailwindcss-', '@tailwindcss/').replace('.tgz', ''), ) interface SpawnedProcess { - dispose: () => void + dispose: () => Promise flush: () => void onStdout: (predicate: (message: string) => boolean) => Promise onStderr: (predicate: (message: string) => boolean) => Promise @@ -35,24 +42,29 @@ interface TestConfig { [filePath: string]: string | Uint8Array } + timeout?: number installDependencies?: boolean + retry?: number } interface TestContext { root: string expect: ExpectStatic exec(command: string, options?: ChildProcessOptions, execOptions?: ExecOptions): Promise spawn(command: string, options?: ChildProcessOptions): Promise + parseSourceMap(opts: string | SourceMapOptions): SourceMap fs: { write(filePath: string, content: string, encoding?: BufferEncoding): Promise + symlink(dst: string, src: string): Promise create(filePaths: string[]): Promise read(filePath: string): Promise + delete(filePath: string): Promise glob(pattern: string): Promise<[string, string][]> dumpFiles(pattern: string): Promise expectFileToContain( filePath: string, contents: string | RegExp | (string | RegExp)[], ): Promise - expectFileNotToContain(filePath: string, contents: string | string[]): Promise + expectFileNotToContain(filePath: string, contents: string[]): Promise } } type TestCallback = (context: TestContext) => Promise | void @@ -60,11 +72,14 @@ interface TestFlags { only?: boolean skip?: boolean debug?: boolean + concurrent?: boolean } type SpawnActor = { predicate: (message: string) => boolean; resolve: () => void } -const IS_WINDOWS = platform() === 'win32' +export const IS_WINDOWS = platform() === 'win32' + +const execFileAsync = promisify(execFile) const TEST_TIMEOUT = IS_WINDOWS ? 120000 : 60000 const ASSERTION_TIMEOUT = IS_WINDOWS ? 10000 : 5000 @@ -78,16 +93,16 @@ export function test( name: string, config: TestConfig, testCallback: TestCallback, - { only = false, skip = false, debug = false }: TestFlags = {}, + { only = false, skip = false, debug = false, concurrent = false }: TestFlags = {}, ) { return defaultTest( name, { - timeout: TEST_TIMEOUT, - retry: process.env.CI ? 2 : 0, + timeout: config.timeout ?? TEST_TIMEOUT, + retry: config.retry ?? (process.env.CI ? 2 : 0), only: only || (!process.env.CI && debug), skip, - concurrent: true, + concurrent, }, async (options) => { let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT @@ -104,12 +119,46 @@ export function test( let context = { root, expect: options.expect, + parseSourceMap, async exec( command: string, childProcessOptions: ChildProcessOptions = {}, execOptions: ExecOptions = {}, ) { let cwd = childProcessOptions.cwd ?? root + let originalCommand = command + + // Avoid `pnpm exec upgrade` on Windows because the upgrader runs + // `pnpm add`, which rewrites Windows `.cmd` shims while the current + // shim is still executing. + // + // Pretty sure this is a pnpm v11 bug on Windows. + if (IS_WINDOWS && command.includes('pnpm exec upgrade')) { + for (let base = cwd; ; base = path.dirname(base)) { + let upgradeBin = path.join( + base, + 'node_modules', + '@tailwindcss', + 'upgrade', + 'dist', + 'index.mjs', + ) + + try { + await fs.access(upgradeBin) + command = command.replace('pnpm exec upgrade', `node "${upgradeBin}"`) + break + } catch (error: any) { + if (error?.code !== 'ENOENT') throw error + } + + let parent = path.dirname(base) + if (parent === base) { + throw new Error(`Unable to resolve @tailwindcss/upgrade from ${cwd}`) + } + } + } + if (debug && cwd !== root) { let relative = path.relative(root, cwd) if (relative[0] !== '.') relative = `./${relative}` @@ -122,10 +171,18 @@ export function test( { cwd, ...childProcessOptions, - env: childProcessOptions.env, + env: { + ...process.env, + ...childProcessOptions.env, + }, }, (error, stdout, stderr) => { if (error) { + if (command !== originalCommand) { + error.message = error.message.replace(command, originalCommand) + let mappedError = error as any + mappedError.cmd = originalCommand + } if (execOptions.ignoreStdErr !== true) console.error(stderr) if (only || debug) { console.error(stdout) @@ -162,6 +219,7 @@ export function test( if (debug) console.log(`>& ${command}`) let child = spawn(command, { cwd, + detached: !IS_WINDOWS, shell: true, ...childProcessOptions, env: { @@ -170,22 +228,26 @@ export function test( }, }) - function dispose() { - if (!child.kill()) { - child.kill('SIGKILL') - } + let disposed = false - let timer = setTimeout( - () => - rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)), - ASSERTION_TIMEOUT, + async function dispose() { + if (disposed) return disposePromise + disposed = true + + await killProcessTree(child) + + let timer = setTimeout(() => { + forceKillProcessTree(child) + rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)) + }, ASSERTION_TIMEOUT) + disposePromise.then( + () => clearTimeout(timer), + () => clearTimeout(timer), ) - disposePromise.finally(() => { - clearTimeout(timer) - }) return disposePromise } disposables.push(dispose) + function onExit() { resolveDisposal?.() } @@ -276,11 +338,7 @@ export function test( } }, fs: { - async write( - filename: string, - content: string | Uint8Array, - encoding: BufferEncoding = 'utf8', - ): Promise { + async write(filename, content, encoding = 'utf8') { let full = path.join(root, filename) let dir = path.dirname(full) await fs.mkdir(dir, { recursive: true }) @@ -291,6 +349,8 @@ export function test( if (filename.endsWith('package.json')) { content = await overwriteVersionsInPackageJson(content) + } else if (filename.endsWith('pnpm-workspace.yaml')) { + content = overwriteVersionsInPnpmWorkspace(content) } // Ensure that files written on Windows use \r\n line ending @@ -301,7 +361,19 @@ export function test( await fs.writeFile(full, content, encoding) }, - async create(filenames: string[]): Promise { + async symlink(target, src) { + let targetAbsolute = path.join(root, target) + let targetParent = path.dirname(targetAbsolute) + await fs.mkdir(targetParent, { recursive: true }) + + let srcAbsolute = path.join(root, src) + let srcParent = path.dirname(srcAbsolute) + await fs.mkdir(srcParent, { recursive: true }) + + await fs.symlink(targetAbsolute, srcAbsolute) + }, + + async create(filenames) { for (let filename of filenames) { let full = path.join(root, filename) @@ -311,7 +383,11 @@ export function test( } }, - async read(filePath: string) { + async delete(filename) { + await fs.unlink(path.join(root, filename)) + }, + + async read(filePath) { let content = await fs.readFile(path.resolve(root, filePath), 'utf8') // Ensure that files read on Windows have \r\n line endings removed @@ -321,7 +397,7 @@ export function test( return content }, - async glob(pattern: string) { + async glob(pattern) { let files = await fastGlob(pattern, { cwd: root }) return Promise.all( files.map(async (file) => { @@ -334,7 +410,7 @@ export function test( }), ) }, - async dumpFiles(pattern: string) { + async dumpFiles(pattern) { let files = await context.fs.glob(pattern) return `\n${files .slice() @@ -393,21 +469,44 @@ export function test( node_modules/ ` + // Disable Git's automatic line ending conversion in these throwaway test + // repositories. Tools like pnpm write files (e.g. `pnpm-lock.yaml`) with + // LF endings, which on Windows (where `core.autocrlf=true` is common) + // makes Git emit noisy "LF will be replaced by CRLF" warnings when it + // touches those files. + config.fs['.gitattributes'] ??= txt` + * -text + ` + + // Ensure there is always a root `pnpm-workspace.yaml` so that transitive + // dependency overrides (and the build allow list) are injected into it. + // As of pnpm v10, these can no longer live in the `pnpm.overrides` field + // of `package.json`. + config.fs['pnpm-workspace.yaml'] ??= '' + for (let [filename, content] of Object.entries(config.fs)) { - await context.fs.write(filename, content) + if (content.toString().startsWith('symlink:')) { + // The symlink path is relative to the target destination's path + let target = path.join( + filename, + content.toString().slice('symlink:'.length), // Relative path + ) + + await context.fs.symlink(target, filename) + } else { + await context.fs.write(filename, content) + } } let shouldInstallDependencies = config.installDependencies ?? true try { - // In debug mode, the directory is going to be inside the pnpm workspace - // of the tailwindcss package. This means that `pnpm install` will run - // pnpm install on the workspace instead (expect if the root dir defines - // a separate workspace). We work around this by using the - // `--ignore-workspace` flag. + // Every test project gets its own root `pnpm-workspace.yaml` (see + // above). This makes the test directory its own workspace root, so even + // in debug mode — where the directory lives inside this repository's + // pnpm workspace — `pnpm install` won't attach to the parent workspace. if (shouldInstallDependencies) { - let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml'] - await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`) + await context.exec(`pnpm install`) } } catch (error: any) { console.error(error) @@ -419,11 +518,18 @@ export function test( let disposables: (() => Promise)[] = [] async function dispose() { - await Promise.all(disposables.map((dispose) => dispose())) + let results = await Promise.allSettled(disposables.map((dispose) => dispose())) if (!debug) { await gracefullyRemove(root) } + + let errors = results.flatMap((result) => + result.status === 'rejected' ? [result.reason] : [], + ) + if (errors.length > 0) { + throw new AggregateError(errors, 'Failed to clean up spawned processes') + } } options.onTestFinished(dispose) @@ -433,7 +539,7 @@ export function test( try { await context.exec('git init', { cwd: root }) await context.exec('git add --all', { cwd: root }) - await context.exec('git commit -m "before migration"', { cwd: root }) + await context.exec('git commit -m "before migration" --allow-empty', { cwd: root }) } catch (error: any) { console.error(error) console.error(error.stdout?.toString()) @@ -452,6 +558,9 @@ test.only = (name: string, config: TestConfig, testCallback: TestCallback) => { test.skip = (name: string, config: TestConfig, testCallback: TestCallback) => { return test(name, config, testCallback, { skip: true }) } +test.concurrent = (name: string, config: TestConfig, testCallback: TestCallback) => { + return test(name, config, testCallback, { concurrent: true }) +} test.debug = (name: string, config: TestConfig, testCallback: TestCallback) => { return test(name, config, testCallback, { debug: true }) } @@ -475,27 +584,43 @@ async function overwriteVersionsInPackageJson(content: string): Promise } } + return JSON.stringify(json, null, 2) +} + +function overwriteVersionsInPnpmWorkspace(content: string): string { + let workspace = content.trim() === '' ? {} : Yaml.parse(content) + + // Inherit information from the root workspace + workspace.allowBuilds = { + ...ROOT_PNPM_WORKSPACE.allowBuilds, + ...workspace.allowBuilds, + } + // Inject transitive dependency overwrite. This is necessary because // @tailwindcss/vite internally depends on a specific version of // @tailwindcss/oxide and we instead want to resolve it to the locally built // version. - json.pnpm ||= {} - json.pnpm.overrides ||= {} + // + // As of pnpm v10, the `pnpm.overrides` field in `package.json` is no longer + // read. Overrides have to be defined in `pnpm-workspace.yaml` instead. + workspace.overrides ||= {} for (let pkg of PUBLIC_PACKAGES) { if (pkg === 'tailwindcss') { // We want to be explicit about the `tailwindcss` package so our tests can // also import v3 without conflicting v4 tarballs. - json.pnpm.overrides['@tailwindcss/node>tailwindcss'] = resolveVersion(pkg) - json.pnpm.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg) - json.pnpm.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg) - json.pnpm.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg) - json.pnpm.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg) + workspace.overrides['@tailwindcss/node>tailwindcss'] = resolveVersion(pkg) + workspace.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg) + workspace.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg) + workspace.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg) + workspace.overrides['@tailwindcss/turbopack>tailwindcss'] = resolveVersion(pkg) + workspace.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg) + workspace.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg) } else { - json.pnpm.overrides[pkg] = resolveVersion(pkg) + workspace.overrides[pkg] = resolveVersion(pkg) } } - return JSON.stringify(json, null, 2) + return Yaml.stringify(workspace) } function resolveVersion(dependency: string) { @@ -545,6 +670,7 @@ export async function retryAssertion( try { return await fn() } catch (err) { + Error.captureStackTrace(err, retryAssertion) error = err await new Promise((resolve) => setTimeout(resolve, delay)) } @@ -560,7 +686,7 @@ export async function fetchStyles(base: string, path = '/'): Promise { let index = await fetch(`${base}${path}`) let html = await index.text() - let linkRegex = /]*>([\s\S]*?)<\/style>/gi let stylesheets: string[] = [] @@ -591,10 +717,71 @@ export async function fetchStyles(base: string, path = '/'): Promise { } return stylesheets.reduce((acc, css) => { - return acc + '\n' + css + if (acc.length > 0) acc += '\n' + acc += css + return acc }, '') } +export async function getRandomPort() { + return new Promise((resolve, reject) => { + let server = createServer() + server.unref() + server.on('error', reject) + server.listen(0, '127.0.0.1', () => { + let address = server.address() + server.close(() => { + if (address && typeof address === 'object') { + resolve(address.port) + } else { + reject(new Error('Unable to allocate random port')) + } + }) + }) + }) +} + +async function killProcessTree(child: ChildProcess) { + if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined) { + return + } + + if (IS_WINDOWS) { + await execFileAsync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { + timeout: ASSERTION_TIMEOUT, + windowsHide: true, + }).catch(() => {}) + return + } + + try { + process.kill(-child.pid, 'SIGTERM') + } catch (error: any) { + if (error?.code !== 'ESRCH') { + child.kill() + } + } +} + +function forceKillProcessTree(child: ChildProcess) { + if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined) { + return + } + + if (IS_WINDOWS) { + execFile('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true }, () => {}) + return + } + + try { + process.kill(-child.pid, 'SIGKILL') + } catch (error: any) { + if (error?.code !== 'ESRCH') { + child.kill('SIGKILL') + } + } +} + async function gracefullyRemove(dir: string) { // Skip removing the directory in CI because it can stall on Windows if (!process.env.CI) { @@ -603,3 +790,82 @@ async function gracefullyRemove(dir: string) { }) } } + +const SOURCE_MAP_COMMENT = /^\/\*# sourceMappingURL=data:application\/json;base64,(.*) \*\/$/ + +export interface SourceMap { + at( + line: number, + column: number, + ): { + source: string | null + original: string + generated: string + } +} + +interface SourceMapOptions { + /** + * A raw source map + * + * This may be a string or an object. Strings will be decoded. + */ + map: string | object + + /** + * The content of the generated file the source map is for + */ + content: string + + /** + * The encoding of the source map + * + * Can be used to decode a base64 map (e.g. an inline source map URI) + */ + encoding?: BufferEncoding +} + +function parseSourceMap(opts: string | SourceMapOptions): SourceMap { + if (typeof opts === 'string') { + let lines = opts.trimEnd().split('\n') + let comment = lines.at(-1) ?? '' + let map = String(comment).match(SOURCE_MAP_COMMENT)?.[1] ?? null + if (!map) throw new Error('No source map comment found') + + return parseSourceMap({ + map, + content: lines.slice(0, -1).join('\n'), + encoding: 'base64', + }) + } + + let rawMap: RawSourceMap + let content = opts.content + + if (typeof opts.map === 'object') { + rawMap = opts.map as RawSourceMap + } else { + rawMap = JSON.parse(Buffer.from(opts.map, opts.encoding ?? 'utf-8').toString()) + } + + let map = new SourceMapConsumer(rawMap) + let generatedTable = createLineTable(content) + + return { + at(line: number, column: number) { + let pos = map.originalPositionFor({ line, column }) + let source = pos.source ? map.sourceContentFor(pos.source) : null + let originalTable = createLineTable(source ?? '') + let originalOffset = originalTable.findOffset(pos) + let generatedOffset = generatedTable.findOffset({ line, column }) + + return { + source: pos.source, + original: source + ? source.slice(originalOffset, originalOffset + 10).trim() + '...' + : '(none)', + generated: content.slice(generatedOffset, generatedOffset + 10).trim() + '...', + } + }, + } +} diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts index 43406624c..f828b5071 100644 --- a/integrations/vite/astro.test.ts +++ b/integrations/vite/astro.test.ts @@ -1,4 +1,4 @@ -import { candidate, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils' +import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils' test( 'dev mode', @@ -129,3 +129,73 @@ test( await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`overline`]) }, ) + +// https://github.com/tailwindlabs/tailwindcss/issues/19677 +test( + 'import aliases should work in + + +

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 new file mode 100644 index 000000000..8ba1b6b73 --- /dev/null +++ b/integrations/vite/bundled-dev.test.ts @@ -0,0 +1,193 @@ +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/config.test.ts b/integrations/vite/config.test.ts index 3bf560e07..0ac68f5e9 100644 --- a/integrations/vite/config.test.ts +++ b/integrations/vite/config.test.ts @@ -12,7 +12,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^6" + "vite": "^7" } } `, @@ -76,7 +76,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^6" + "vite": "^7" } } `, @@ -140,7 +140,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^6" + "vite": "^7" } } `, @@ -219,7 +219,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^6" + "vite": "^7" } } `, diff --git a/integrations/vite/css-modules.test.ts b/integrations/vite/css-modules.test.ts index 668281cb4..a680ab9b7 100644 --- a/integrations/vite/css-modules.test.ts +++ b/integrations/vite/css-modules.test.ts @@ -14,8 +14,8 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" } } `, diff --git a/integrations/vite/html-style-blocks.test.ts b/integrations/vite/html-style-blocks.test.ts index 4a86f96dc..22bb91d78 100644 --- a/integrations/vite/html-style-blocks.test.ts +++ b/integrations/vite/html-style-blocks.test.ts @@ -12,7 +12,7 @@ test( }, "devDependencies": { "@tailwindcss/vite": "workspace:^", - "vite": "^6" + "vite": "^7" } } `, diff --git a/integrations/vite/ignored-packages.test.ts b/integrations/vite/ignored-packages.test.ts index f7f85ff14..1efa9dc61 100644 --- a/integrations/vite/ignored-packages.test.ts +++ b/integrations/vite/ignored-packages.test.ts @@ -11,7 +11,7 @@ const WORKSPACE = { "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^6" + "vite": "^7" } } `, diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index 4c64f220e..c346f8e37 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -7,6 +7,7 @@ import { html, js, json, + jsx, retryAssertion, test, ts, @@ -33,8 +34,8 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" } } `, @@ -110,8 +111,8 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" } } `, @@ -293,6 +294,113 @@ 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', { @@ -311,8 +419,8 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" } } `, @@ -472,6 +580,352 @@ 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`, { @@ -490,8 +944,8 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" } } `, @@ -576,8 +1030,8 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" } } `, @@ -678,8 +1132,8 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" } } `, @@ -724,12 +1178,92 @@ 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.toThrowError('The `source(../i-do-not-exist)` does not exist') + ).rejects.toThrow('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( @@ -744,7 +1278,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^6" + "vite": "^7" } } `, @@ -816,7 +1350,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^6" + "vite": "^7" } } `, @@ -879,7 +1413,7 @@ test( "@tailwindcss/vite": "workspace:^", "tailwindcss": "workspace:^", "plotly.js": "^3", - "vite": "^6" + "vite": "^7" } } `, @@ -898,6 +1432,14 @@ 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 }) => { @@ -914,3 +1456,271 @@ test( function firstLine(str: string) { return str.split('\n')[0] } + +test( + 'optimize option: disabled', + { + 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 }, + plugins: [tailwindcss({ optimize: false })], + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + }, + }, + async ({ exec, expect, fs }) => { + await exec('pnpm vite build') + + // 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; + } + " + `) + }, +) + +test( + 'optimize option: enabled with minify disabled', + { + 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 }, + plugins: [tailwindcss({ optimize: { minify: false } })], + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + }, + }, + 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) + }) + }, +) diff --git a/integrations/vite/multi-root.test.ts b/integrations/vite/multi-root.test.ts index d1a1337a4..4020aff0f 100644 --- a/integrations/vite/multi-root.test.ts +++ b/integrations/vite/multi-root.test.ts @@ -12,7 +12,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^6" + "vite": "^7" } } `, @@ -96,7 +96,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^6" + "vite": "^7" } } `, diff --git a/integrations/vite/nuxt.test.ts b/integrations/vite/nuxt.test.ts index ab14628c7..20f367020 100644 --- a/integrations/vite/nuxt.test.ts +++ b/integrations/vite/nuxt.test.ts @@ -1,4 +1,15 @@ -import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' +import { + candidate, + css, + fetchStyles, + getRandomPort, + html, + json, + retryAssertion, + test, + ts, + yaml, +} from '../utils' const SETUP = { fs: { @@ -7,12 +18,18 @@ const SETUP = { "type": "module", "dependencies": { "@tailwindcss/vite": "workspace:^", - "nuxt": "^3.13.1", + "nuxt": "3.16.0", + "nitropack": "2.11.0", "tailwindcss": "workspace:^", "vue": "latest" } } `, + 'pnpm-workspace.yaml': yaml` + # + overrides: + nuxi: 3.28.0 + `, 'nuxt.config.ts': ts` import tailwindcss from '@tailwindcss/vite' @@ -78,7 +95,8 @@ 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 process = await spawn(`pnpm nuxt preview --port 8724`, { + let port = await getRandomPort() + let process = await spawn(`pnpm nuxt preview --port ${port}`, { env: { TEST: 'false', NODE_ENV: 'development', diff --git a/integrations/vite/other-transforms.test.ts b/integrations/vite/other-transforms.test.ts index b1cb5819c..d56396065 100644 --- a/integrations/vite/other-transforms.test.ts +++ b/integrations/vite/other-transforms.test.ts @@ -13,8 +13,8 @@ function createSetup(transformer: 'postcss' | 'lightningcss') { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" } } `, diff --git a/integrations/vite/preact.test.ts b/integrations/vite/preact.test.ts new file mode 100644 index 000000000..8e4deec2b --- /dev/null +++ b/integrations/vite/preact.test.ts @@ -0,0 +1,165 @@ +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 5b9574fd6..98b0c928d 100644 --- a/integrations/vite/react-router.test.ts +++ b/integrations/vite/react-router.test.ts @@ -102,13 +102,101 @@ 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 6072c37da..fc9aca5c7 100644 --- a/integrations/vite/resolvers.test.ts +++ b/integrations/vite/resolvers.test.ts @@ -1,5 +1,661 @@ import { describe } from 'vitest' -import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils' +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; + } + } + " + `) + }, +) describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { test( @@ -14,7 +670,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} "vite": "^5.3.5" } } @@ -82,7 +738,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} "vite": "^5.3.5" } } diff --git a/integrations/vite/source-maps.test.ts b/integrations/vite/source-maps.test.ts new file mode 100644 index 000000000..fe26d9a7c --- /dev/null +++ b/integrations/vite/source-maps.test.ts @@ -0,0 +1,157 @@ +import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' + +test( + `dev build`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "lightningcss": "^1", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + plugins: [tailwindcss()], + css: { + devSourcemap: true, + }, + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + /* */ + `, + }, + }, + async ({ fs, spawn, expect, parseSourceMap }) => { + // Source maps only work in development mode in Vite + 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) + }) + + let styles = await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + + // Wait until we have the right CSS + expect(styles).toContain(candidate`flex`) + + return styles + }) + + // Make sure we can find a source map + let map = parseSourceMap(styles) + + expect(map.at(1, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '/*! tailwi...', + }) + + expect(map.at(2, 0)).toMatchObject({ + source: expect.stringContaining('utilities.css'), + original: '@tailwind...', + generated: '.flex {...', + }) + + expect(map.at(3, 2)).toMatchObject({ + source: expect.stringContaining('utilities.css'), + original: '@tailwind...', + generated: 'display: f...', + }) + + expect(map.at(4, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '}...', + }) + }, +) + +// 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 d4d227b8e..a1d660816 100644 --- a/integrations/vite/ssr.test.ts +++ b/integrations/vite/ssr.test.ts @@ -1,121 +1,72 @@ +import { describe } from 'vitest' import { candidate, css, html, json, test, ts } from '../utils' -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" +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}" + } } - } - `, - '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()], - }) - `, - ...WORKSPACE, + 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!
+ \` + `, + }, }, - }, - 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`, - ]) - }, -) - -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, + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`m-2`, + candidate`overline`, + candidate`m-3`, + ]) }, - }, - 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 1efb38987..e186c3f85 100644 --- a/integrations/vite/svelte.test.ts +++ b/integrations/vite/svelte.test.ts @@ -14,7 +14,7 @@ test( "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5", "@tailwindcss/vite": "workspace:^", - "vite": "^6" + "vite": "^7" } } `, @@ -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\{/, ]) @@ -125,7 +125,7 @@ test( "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5", "@tailwindcss/vite": "workspace:^", - "vite": "^6" + "vite": "^7" } } `, diff --git a/integrations/vite/sveltekit.test.ts b/integrations/vite/sveltekit.test.ts new file mode 100644 index 000000000..92e855da9 --- /dev/null +++ b/integrations/vite/sveltekit.test.ts @@ -0,0 +1,80 @@ +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/url-rewriting.test.ts b/integrations/vite/url-rewriting.test.ts index 4077df16d..500fe6d71 100644 --- a/integrations/vite/url-rewriting.test.ts +++ b/integrations/vite/url-rewriting.test.ts @@ -15,9 +15,9 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} "@tailwindcss/vite": "workspace:^", - "vite": "^6" + "vite": "^7" } } `, diff --git a/integrations/vite/vue.test.ts b/integrations/vite/vue.test.ts index 7b5da2904..b04f57c6b 100644 --- a/integrations/vite/vue.test.ts +++ b/integrations/vite/vue.test.ts @@ -1,4 +1,5 @@ -import { candidate, html, json, test, ts } from '../utils' +import { stripVTControlCharacters } from 'node:util' +import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' test( 'production build', @@ -14,7 +15,7 @@ test( "devDependencies": { "@vitejs/plugin-vue": "^5.1.2", "@tailwindcss/vite": "workspace:^", - "vite": "^6" + "vite": "^7" } } `, @@ -71,3 +72,309 @@ test( await fs.expectFileToContain(files[0][0], ['.bar{']) }, ) + +{ + 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`', + { + 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.ts': ts` + import { createApp } from 'vue' + import App from './App.vue' + + createApp(App).mount('#app') + `, + 'src/App.vue': html` + + + + `, + }, + }, + async ({ exec, expect }) => { + expect.assertions(1) + + try { + await exec('pnpm vite build', {}, { ignoreStdErr: true }) + } catch (error) { + let [, message] = + /error during build:([\s\S]*?)file:/g.exec( + stripVTControlCharacters(error.message.replace(/\r?\n/g, '\n')), + ) ?? [] + expect(message.trim()).toMatchInlineSnapshot( + `"[@tailwindcss/vite:generate:build] Cannot apply unknown utility class \`text-red-500\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive"`, + ) + } + }, +) + +// 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.ts b/integrations/vitest.config.mts similarity index 100% rename from integrations/vitest.config.ts rename to integrations/vitest.config.mts diff --git a/integrations/webpack/loader.test.ts b/integrations/webpack/loader.test.ts new file mode 100644 index 000000000..9c3d870af --- /dev/null +++ b/integrations/webpack/loader.test.ts @@ -0,0 +1,495 @@ +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 bfdb5058d..7d41a469e 100644 --- a/package.json +++ b/package.json @@ -48,23 +48,17 @@ }, "license": "MIT", "devDependencies": { - "@playwright/test": "^1.51.1", + "@playwright/test": "^1.62.1", "@types/node": "catalog:", - "postcss": "8.5.3", - "postcss-import": "^16.1.0", + "postcss": "catalog:", + "postcss-import": "^16.1.1", "prettier": "catalog:", - "prettier-plugin-embed": "^0.5.0", - "prettier-plugin-organize-imports": "^4.0.0", - "tsup": "^8.4.0", - "turbo": "^2.4.4", - "typescript": "^5.5.4", - "vitest": "^2.0.5" + "prettier-plugin-embed": "^0.5.1", + "prettier-plugin-organize-imports": "^4.3.0", + "tsup": "^8.5.1", + "turbo": "^2.10.8", + "typescript": "^5.9.3", + "vitest": "^4.1.10" }, - "packageManager": "pnpm@9.6.0", - "pnpm": { - "patchedDependencies": { - "lightningcss@1.29.2": "patches/lightningcss@1.29.2.patch", - "@parcel/watcher@2.5.1": "patches/@parcel__watcher@2.5.1.patch" - } - } + "packageManager": "pnpm@11.9.0" } diff --git a/packages/@tailwindcss-browser/README.md b/packages/@tailwindcss-browser/README.md index 95ec9d87d..5f532607d 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

--- @@ -27,14 +27,10 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ## Community -For help, discussion about best practices, or any other conversation that would benefit from being searchable: +For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) - -For chatting with others using the framework: - -[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-browser/package.json b/packages/@tailwindcss-browser/package.json index a73068fe7..70af78c1e 100644 --- a/packages/@tailwindcss-browser/package.json +++ b/packages/@tailwindcss-browser/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/browser", - "version": "4.1.3", + "version": "4.3.3", "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.1", - "listhen": "^1.9.0", + "h3": "^1.15.11", + "listhen": "^1.10.1", "tailwindcss": "workspace:*" } } diff --git a/packages/@tailwindcss-browser/playwright.config.ts b/packages/@tailwindcss-browser/playwright.config.ts index c3ad05b91..c6059dd82 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', + reporter: [['html', { open: 'never' }]], /* 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,10 +30,11 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, + // 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: 'firefox', use: { diff --git a/packages/@tailwindcss-browser/src/index.ts b/packages/@tailwindcss-browser/src/index.ts index e7688a121..49c7e4675 100644 --- a/packages/@tailwindcss-browser/src/index.ts +++ b/packages/@tailwindcss-browser/src/index.ts @@ -2,12 +2,6 @@ import * as tailwindcss from 'tailwindcss' import * as assets from './assets' import { Instrumentation } from './instrumentation' -// Warn users about using the browser build in production as early as possible. -// It can take time for the script to do its work so this must be at the top. -console.warn( - 'The browser build of Tailwind CSS should not be used in production. To use Tailwind CSS in production, use the Tailwind CLI, Vite plugin, or PostCSS plugin: https://tailwindcss.com/docs/installation', -) - /** * The type used by ` + // ``` + if ( + // Whitespace before the candidate + location.contents[location.start - 1]?.match(/\s/) && + // A colon followed by whitespace after the candidate + location.contents.slice(location.end, location.end + 2)?.match(/^:\s/) + ) { + // Compute all `` tag +// - All `` +// - No nested `', offset) + if (endTag === -1) return ranges + offset = endTag + 1 + + ranges.push(startTag, endTag) + } +}) + +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 + + for (let i = 0; i < line.length; i++) { + let char = line.charCodeAt(i) + switch (char) { + // Escaped character, skip the next character + case BACKSLASH: + i++ + break + + case SINGLE_QUOTE: + case DOUBLE_QUOTE: + case BACKTICK: + // Found matching quote, we are outside of a string + if (currentQuote === char) { + currentQuote = null + } + + // Found a quote, we are inside a string + else if (currentQuote === null) { + currentQuote = char + } + break + } + } + + 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-value-to-bare-value.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.test.ts deleted file mode 100644 index 77e275360..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' -import { migrateArbitraryValueToBareValue } from './migrate-arbitrary-value-to-bare-value' - -test.each([ - ['aspect-[12/34]', 'aspect-12/34'], - ['aspect-[1.2/34]', 'aspect-[1.2/34]'], - ['col-start-[7]', 'col-start-7'], - ['flex-[2]', 'flex-2'], // `flex` is implemented as static and functional utilities - - ['grid-cols-[subgrid]', 'grid-cols-subgrid'], - ['grid-rows-[subgrid]', 'grid-rows-subgrid'], - - // Only 50-200% (inclusive) are valid: - // https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch#percentage - ['font-stretch-[50%]', 'font-stretch-50%'], - ['font-stretch-[50.5%]', 'font-stretch-[50.5%]'], - ['font-stretch-[201%]', 'font-stretch-[201%]'], - ['font-stretch-[49%]', 'font-stretch-[49%]'], - // Should stay as-is - ['font-stretch-[1/2]', 'font-stretch-[1/2]'], - - // Bare value with % is valid for these utilities - ['from-[28%]', 'from-28%'], - ['via-[28%]', 'via-28%'], - ['to-[28%]', 'to-28%'], - ['from-[28.5%]', 'from-[28.5%]'], - ['via-[28.5%]', 'via-[28.5%]'], - ['to-[28.5%]', 'to-[28.5%]'], - - // This test in itself is a bit flawed because `text-[1/2]` currently - // generates something. Converting it to `text-1/2` doesn't produce anything. - ['text-[1/2]', 'text-[1/2]'], - - // Leading is special, because `leading-[123]` is the direct value of 123, but - // `leading-123` maps to `calc(--spacing(123))`. - ['leading-[1]', 'leading-none'], - ['leading-[123]', 'leading-[123]'], - - ['data-[selected]:flex', 'data-selected:flex'], - ['data-[foo=bar]:flex', 'data-[foo=bar]:flex'], - - ['supports-[gap]:flex', 'supports-gap:flex'], - ['supports-[display:grid]:flex', 'supports-[display:grid]:flex'], - - ['group-data-[selected]:flex', 'group-data-selected:flex'], - ['group-data-[foo=bar]:flex', 'group-data-[foo=bar]:flex'], - ['group-has-data-[selected]:flex', 'group-has-data-selected:flex'], - - ['aria-[selected]:flex', 'aria-[selected]:flex'], - ['aria-[selected="true"]:flex', 'aria-selected:flex'], - ['aria-[selected*="true"]:flex', 'aria-[selected*="true"]:flex'], - - ['group-aria-[selected]:flex', 'group-aria-[selected]:flex'], - ['group-aria-[selected="true"]:flex', 'group-aria-selected:flex'], - ['group-has-aria-[selected]:flex', 'group-has-aria-[selected]:flex'], - - ['max-lg:hover:data-[selected]:flex!', 'max-lg:hover:data-selected:flex!'], - [ - 'data-[selected]:aria-[selected="true"]:aspect-[12/34]', - 'data-selected:aria-selected:aspect-12/34', - ], -])('%s => %s', async (candidate, result) => { - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { - base: __dirname, - }) - - expect(migrateArbitraryValueToBareValue(designSystem, {}, candidate)).toEqual(result) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.ts deleted file mode 100644 index 6b01bb743..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate' -import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' -import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { isPositiveInteger } from '../../../../tailwindcss/src/utils/infer-data-type' -import { segment } from '../../../../tailwindcss/src/utils/segment' -import { printCandidate } from './candidates' - -export function migrateArbitraryValueToBareValue( - designSystem: DesignSystem, - _userConfig: Config, - rawCandidate: string, -): string { - for (let candidate of parseCandidate(rawCandidate, designSystem)) { - let clone = structuredClone(candidate) - let changed = false - - // Convert [subgrid] to subgrid - if ( - clone.kind === 'functional' && - clone.value?.kind === 'arbitrary' && - clone.value.value === 'subgrid' && - (clone.root === 'grid-cols' || clone.root == 'grid-rows') - ) { - changed = true - clone.value = { - kind: 'named', - value: 'subgrid', - fraction: null, - } - } - - // Convert utilities that accept bare values ending in % - if ( - clone.kind === 'functional' && - clone.value?.kind === 'arbitrary' && - clone.value.dataType === null && - (clone.root === 'from' || - clone.root === 'via' || - clone.root === 'to' || - clone.root === 'font-stretch') - ) { - if (clone.value.value.endsWith('%') && isPositiveInteger(clone.value.value.slice(0, -1))) { - let percentage = parseInt(clone.value.value) - if ( - clone.root === 'from' || - clone.root === 'via' || - clone.root === 'to' || - (clone.root === 'font-stretch' && percentage >= 50 && percentage <= 200) - ) { - changed = true - clone.value = { - kind: 'named', - value: clone.value.value, - fraction: null, - } - } - } - } - - // Convert arbitrary values with positive integers to bare values - // Convert arbitrary values with fractions to bare values - else if ( - clone.kind === 'functional' && - clone.value?.kind === 'arbitrary' && - clone.value.dataType === null - ) { - if (clone.root === 'leading') { - // leading-[1] -> leading-none - if (clone.value.value === '1') { - changed = true - clone.value = { - kind: 'named', - value: 'none', - fraction: null, - } - } - - // Keep leading-[] as leading-[] - else { - continue - } - } - - let parts = segment(clone.value.value, '/') - if (parts.every((part) => isPositiveInteger(part))) { - changed = true - - let currentValue = clone.value - let currentModifier = clone.modifier - - // E.g.: `col-start-[12]` - // ^^ - if (parts.length === 1) { - clone.value = { - kind: 'named', - value: clone.value.value, - fraction: null, - } - } - - // E.g.: `aspect-[12/34]` - // ^^ ^^ - else { - clone.value = { - kind: 'named', - value: parts[0], - fraction: clone.value.value, - } - clone.modifier = { - kind: 'named', - value: parts[1], - } - } - - // Double check that the new value compiles correctly - if (designSystem.compileAstNodes(clone).length === 0) { - clone.value = currentValue - clone.modifier = currentModifier - changed = false - } - } - } - - for (let variant of variants(clone)) { - // Convert `data-[selected]` to `data-selected` - if ( - variant.kind === 'functional' && - variant.root === 'data' && - variant.value?.kind === 'arbitrary' && - !variant.value.value.includes('=') - ) { - changed = true - variant.value = { - kind: 'named', - value: variant.value.value, - } - } - - // Convert `aria-[selected="true"]` to `aria-selected` - else if ( - variant.kind === 'functional' && - variant.root === 'aria' && - variant.value?.kind === 'arbitrary' && - (variant.value.value.endsWith('=true') || - variant.value.value.endsWith('="true"') || - variant.value.value.endsWith("='true'")) - ) { - let [key, _value] = segment(variant.value.value, '=') - if ( - // aria-[foo~="true"] - key[key.length - 1] === '~' || - // aria-[foo|="true"] - key[key.length - 1] === '|' || - // aria-[foo^="true"] - key[key.length - 1] === '^' || - // aria-[foo$="true"] - key[key.length - 1] === '$' || - // aria-[foo*="true"] - key[key.length - 1] === '*' - ) { - continue - } - - changed = true - variant.value = { - kind: 'named', - value: variant.value.value.slice(0, variant.value.value.indexOf('=')), - } - } - - // Convert `supports-[gap]` to `supports-gap` - else if ( - variant.kind === 'functional' && - variant.root === 'supports' && - variant.value?.kind === 'arbitrary' && - /^[a-z-][a-z0-9-]*$/i.test(variant.value.value) - ) { - changed = true - variant.value = { - kind: 'named', - value: variant.value.value, - } - } - } - - return changed ? printCandidate(designSystem, clone) : rawCandidate - } - - return rawCandidate -} - -function* variants(candidate: Candidate) { - function* inner(variant: Variant): Iterable { - yield variant - if (variant.kind === 'compound') { - yield* inner(variant.variant) - } - } - - for (let variant of candidate.variants) { - yield* inner(variant) - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts new file mode 100644 index 000000000..258ba1b33 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts @@ -0,0 +1,55 @@ +import { cloneCandidate } from '../../../../tailwindcss/src/candidate' +import { + PRE_COMPUTED_VARIANTS_KEY, + prepareDesignSystemStorage, + VARIANT_SIGNATURE_KEY, +} from '../../../../tailwindcss/src/canonicalize-candidates' +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import type { Writable } from '../../../../tailwindcss/src/types' +import { replaceObject } from '../../../../tailwindcss/src/utils/replace-object' +import { walkVariants } from '../../utils/walk-variants' + +export function migrateArbitraryVariants( + baseDesignSystem: DesignSystem, + _userConfig: Config | null, + 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) + + for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { + // We are only interested in the variants + if (readonlyCandidate.variants.length <= 0) return rawCandidate + + // The below logic makes use of mutation. Since candidates in the + // DesignSystem are cached, we can't mutate them directly. + let candidate = cloneCandidate(readonlyCandidate) as Writable + + for (let [variant] of walkVariants(candidate)) { + if (variant.kind === 'compound') continue + + let targetString = designSystem.printVariant(variant) + let targetSignature = signatures.get(targetString) + 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 + + let foundVariant = foundVariants[0] + let parsedVariant = designSystem.parseVariant(foundVariant) + if (parsedVariant === null) continue + + replaceObject(variant, parsedVariant) + } + + return designSystem.printCandidate(candidate) + } + + return rawCandidate +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.test.ts index d26c185cc..5da651d51 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.test.ts @@ -40,6 +40,11 @@ test.each([ ['w-[--spacing(5)]', 'w-[--spacing(5)]'], ['bg-[--theme(--color-red-500)]', 'bg-[--theme(--color-red-500)]'], + // Fallback values should be included inside the `var(…)` function + ['bg-[--my-color,red]', 'bg-(--my-color,red)'], + // Fallback values can contain CSS functions + ['bg-[--my-color,theme(spacing.1)]', 'bg-(--my-color,theme(spacing.1))'], + // Some properties never had var() injection in v3. ['[scroll-timeline-name:--myTimeline]', '[scroll-timeline-name:--myTimeline]'], ['[timeline-scope:--myScope]', '[timeline-scope:--myScope]'], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts index dfb702ed8..0ea3bd691 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts @@ -1,18 +1,19 @@ -import { walk, WalkAction } from '../../../../tailwindcss/src/ast' -import { type Candidate, type Variant } from '../../../../tailwindcss/src/candidate' +import { cloneCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { printCandidate } from './candidates' +import type { Writable } from '../../../../tailwindcss/src/types' +import * as ValueParser from '../../../../tailwindcss/src/value-parser' +import { walk, WalkAction } from '../../../../tailwindcss/src/walk' export function migrateAutomaticVarInjection( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { // The below logic makes extended use of mutation. Since candidates in the // DesignSystem are cached, we can't mutate them directly. - let candidate = structuredClone(readonlyCandidate) as Candidate + let candidate = cloneCandidate(readonlyCandidate) as Writable let didChange = false @@ -66,7 +67,7 @@ export function migrateAutomaticVarInjection( } if (didChange) { - return printCandidate(designSystem, candidate) + return designSystem.printCandidate(candidate) } } return rawCandidate @@ -74,9 +75,23 @@ export function migrateAutomaticVarInjection( function injectVar(value: string): { value: string; didChange: boolean } { let didChange = false - if (value.startsWith('--') && !value.includes('(')) { - value = `var(${value})` - didChange = true + if (value.startsWith('--')) { + // E.g.: + // + // - `--my-color` → `var(--my-color)` Convert variable + // - `--my-color,red` → `var(--my-color,red)` Convert variable with fallback + // - `--theme(color.red)` → `--theme(color.red)` Do not convert functions + // + if ( + // No `(` definitely means there is no function + !value.includes('(') || + // There could be a function call in the fallback value, but it cannot be + // top-level, so we can safely check the first part + ValueParser.parse(value)[0]?.kind !== 'function' + ) { + value = `var(${value})` + didChange = true + } } else if (value.startsWith(' --')) { value = value.slice(1) didChange = true diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.test.ts deleted file mode 100644 index df0ca747b..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' -import { migrateBgGradient } from './migrate-bg-gradient' - -test.each([ - ['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'], - - ['max-lg:hover:bg-gradient-to-t', 'max-lg:hover:bg-linear-to-t'], -])('%s => %s', async (candidate, result) => { - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { - base: __dirname, - }) - - expect(migrateBgGradient(designSystem, {}, candidate)).toEqual(result) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.ts deleted file mode 100644 index a056adae6..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' -import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { printCandidate } from './candidates' - -const DIRECTIONS = ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] - -export function migrateBgGradient( - designSystem: DesignSystem, - _userConfig: Config, - rawCandidate: string, -): string { - for (let candidate of designSystem.parseCandidate(rawCandidate)) { - if (candidate.kind === 'static' && candidate.root.startsWith('bg-gradient-to-')) { - let direction = candidate.root.slice(15) - - if (!DIRECTIONS.includes(direction)) { - continue - } - - return printCandidate(designSystem, { - ...candidate, - root: `bg-linear-to-${direction}`, - }) - } - } - return rawCandidate -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.test.ts new file mode 100644 index 000000000..ed5be23c0 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.test.ts @@ -0,0 +1,24 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' +import { migrateCamelcaseInNamedValue } from './migrate-camelcase-in-named-value' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) + +test.each([ + ['text-superRed', 'text-super-red'], + ['text-red/superOpaque', 'text-red/super-opaque'], + ['text-superRed/superOpaque', 'text-super-red/super-opaque'], + + // Should not migrate named values in modifiers + ['group-hover/superGroup:underline', 'group-hover/superGroup:underline'], + + ['hover:text-superRed', 'hover:text-super-red'], + ['hover:text-red/superOpaque', 'hover:text-red/super-opaque'], + ['hover:text-superRed/superOpaque', 'hover:text-super-red/super-opaque'], +])('%s => %s', async (candidate, result) => { + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + expect(migrateCamelcaseInNamedValue(designSystem, {}, candidate)).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts new file mode 100644 index 000000000..9cfbc7de0 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts @@ -0,0 +1,54 @@ +import { cloneCandidate } from '../../../../tailwindcss/src/candidate' +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import * as version from '../../utils/version' + +// Converts named values to use kebab-case. This is necessary because the +// upgrade tool also renames the theme values to kebab-case, so `text-superRed` +// will have its theme value renamed to `--color-super-red` and thus the utility +// will be renamed to `text-super-red`. +export function migrateCamelcaseInNamedValue( + designSystem: DesignSystem, + _userConfig: Config | null, + rawCandidate: string, +): string { + if (!version.isMajor(3)) return rawCandidate + + for (let candidate of designSystem.parseCandidate(rawCandidate)) { + if (candidate.kind !== 'functional') continue + let clone = cloneCandidate(candidate) + let didChange = false + + if ( + candidate.value && + clone.value && + candidate.value.kind === 'named' && + clone.value.kind === 'named' && + candidate.value.value.match(/[A-Z]/) + ) { + clone.value.value = camelToKebab(candidate.value.value) + didChange = true + } + + if ( + candidate.modifier && + clone.modifier && + candidate.modifier.kind === 'named' && + clone.modifier.kind === 'named' && + candidate.modifier.value.match(/[A-Z]/) + ) { + clone.modifier.value = camelToKebab(candidate.modifier.value) + didChange = true + } + + if (didChange) { + return designSystem.printCandidate(clone) + } + } + + return rawCandidate +} + +function camelToKebab(str: string): string { + return str.replace(/([A-Z])/g, '-$1').toLowerCase() +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.test.ts new file mode 100644 index 000000000..57fe80c22 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.test.ts @@ -0,0 +1,27 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' +import { migrateCanonicalizeCandidate } from './migrate-canonicalize-candidate' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) + +test.each([ + // Normalize whitespace in arbitrary properties + ['[display:flex]', '[display:flex]'], + ['[display:_flex]', '[display:flex]'], + ['[display:flex_]', '[display:flex]'], + ['[display:_flex_]', '[display:flex]'], + + // Normalize whitespace in `calc` expressions + ['w-[calc(100%-2rem)]', 'w-[calc(100%-2rem)]'], + ['w-[calc(100%_-_2rem)]', 'w-[calc(100%-2rem)]'], + + // Normalize the important modifier + ['!flex', 'flex!'], + ['flex!', 'flex!'], +])('%s => %s', async (candidate, result) => { + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + expect(migrateCanonicalizeCandidate(designSystem, {}, candidate)).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.ts new file mode 100644 index 000000000..32d126467 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.ts @@ -0,0 +1,29 @@ +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' + +// Canonicalize the value to its minimal form. This will normalize whitespace, +// and print the important modifier `!` in the correct place. +// +// E.g.: +// +// ``` +// [display:_flex_] => [display:flex] +// [display:_flex] => [display:flex] +// [display:flex_] => [display:flex] +// [display:flex] => [display:flex] +// ``` +// +export function migrateCanonicalizeCandidate( + designSystem: DesignSystem, + _userConfig: Config | null, + rawCandidate: string, +) { + for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { + let canonicalizedCandidate = designSystem.printCandidate(readonlyCandidate) + if (canonicalizedCandidate !== rawCandidate) { + return canonicalizedCandidate + } + } + + return rawCandidate +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.test.ts index 107c901c0..bebd43343 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.test.ts @@ -1,7 +1,9 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migrateEmptyArbitraryValues } from './migrate-handle-empty-arbitrary-values' import { migratePrefix } from './migrate-prefix' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) test.each([ ['group-[]:flex', 'group-[&]:flex'], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.ts index 0c4fddb11..0058814d9 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.ts @@ -3,7 +3,7 @@ import type { DesignSystem } from '../../../../tailwindcss/src/design-system' export function migrateEmptyArbitraryValues( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { // We can parse the candidate, nothing to do diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.test.ts deleted file mode 100644 index bc84538aa..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' -import { migrateImportant } from './migrate-important' - -test.each([ - ['!flex', 'flex!'], - ['min-[calc(1000px+12em)]:!flex', 'min-[calc(1000px+12em)]:flex!'], - ['md:!block', 'md:block!'], - - // Does not change non-important candidates - ['bg-blue-500', 'bg-blue-500'], - ['min-[calc(1000px+12em)]:flex', 'min-[calc(1000px+12em)]:flex'], -])('%s => %s', async (candidate, result) => { - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { - base: __dirname, - }) - - expect( - migrateImportant(designSystem, {}, candidate, { - contents: `"${candidate}"`, - start: 1, - end: candidate.length + 1, - }), - ).toEqual(result) -}) - -test('does not match false positives', async () => { - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { - base: __dirname, - }) - - expect( - migrateImportant(designSystem, {}, '!border', { - contents: `let notBorder = !border\n`, - start: 16, - end: 16 + '!border'.length, - }), - ).toEqual('!border') -}) - -test('does not replace classes in invalid positions', async () => { - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { - base: __dirname, - }) - - function shouldNotReplace(example: string, candidate = '!border') { - expect( - migrateImportant(designSystem, {}, candidate, { - contents: example, - start: example.indexOf(candidate), - end: example.indexOf(candidate) + candidate.length, - }), - ).toEqual(candidate) - } - - shouldNotReplace(`let notBorder = !border \n`) - shouldNotReplace(`{ "foo": !border.something + ""}\n`) - shouldNotReplace(`
\n`) - shouldNotReplace(`
\n`) - shouldNotReplace(`
\n`) - shouldNotReplace(`
\n`) - shouldNotReplace(`
\n`) - shouldNotReplace(`
\n`) - shouldNotReplace(`
\n`) - shouldNotReplace(`
\n`) - shouldNotReplace(`
\n`) - shouldNotReplace(`
\n`) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts deleted file mode 100644 index 576557fb9..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { parseCandidate } from '../../../../tailwindcss/src/candidate' -import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' -import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { printCandidate } from './candidates' -import { isSafeMigration } from './is-safe-migration' - -// In v3 the important modifier `!` sits in front of the utility itself, not -// before any of the variants. In v4, we want it to be at the end of the utility -// so that it's always in the same location regardless of whether you used -// variants or not. -// -// So this: -// -// !flex md:!block -// -// Should turn into: -// -// flex! md:block! -export function migrateImportant( - designSystem: DesignSystem, - _userConfig: Config, - rawCandidate: string, - location?: { - contents: string - start: number - end: number - }, -): string { - nextCandidate: for (let candidate of parseCandidate(rawCandidate, designSystem)) { - if (candidate.important && candidate.raw[candidate.raw.length - 1] !== '!') { - // The important migration is one of the most broad migrations with a high - // potential of matching false positives since `!` is a valid character in - // most programming languages. Since v4 is technically backward compatible - // with v3 in that it can read `!` in the front of the utility too, we err - // on the side of caution and only migrate candidates that we are certain - // are inside of a string. - if (location && !isSafeMigration(location)) { - continue nextCandidate - } - - // The printCandidate function will already put the exclamation mark in - // the right place, so we just need to mark this candidate as requiring a - // migration. - return printCandidate(designSystem, candidate) - } - } - - return rawCandidate -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts index b2bd44d07..2bd7d2fd5 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts @@ -1,16 +1,15 @@ -import { parseCandidate } from '../../../../tailwindcss/src/candidate' +import { cloneCandidate, parseCandidate } from '../../../../tailwindcss/src/candidate' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { segment } from '../../../../tailwindcss/src/utils/segment' -import { printCandidate } from './candidates' export function migrateLegacyArbitraryValues( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let candidate of parseCandidate(rawCandidate, designSystem)) { - let clone = structuredClone(candidate) + let clone = cloneCandidate(candidate) let changed = false // Convert commas to spaces. E.g.: [auto,1fr] to [auto_1fr] @@ -23,7 +22,7 @@ export function migrateLegacyArbitraryValues( clone.value.value = segment(clone.value.value, ',').join(' ') } - return changed ? printCandidate(designSystem, clone) : rawCandidate + return changed ? designSystem.printCandidate(clone) : rawCandidate } return rawCandidate diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts index ad589cbee..5b204386f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts @@ -1,6 +1,8 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migrateLegacyClasses } from './migrate-legacy-classes' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) test.each([ ['shadow', 'shadow-sm'], @@ -25,6 +27,8 @@ test.each([ ['ring', 'ring-3'], + ['outline', 'outline-solid'], + ['blur!', 'blur-sm!'], ['hover:blur', 'hover:blur-sm'], ['hover:blur!', 'hover:blur-sm!'], @@ -39,38 +43,3 @@ test.each([ expect(await migrateLegacyClasses(designSystem, {}, candidate)).toEqual(result) }) - -test('does not replace classes in invalid positions', async () => { - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { - base: __dirname, - }) - - async function shouldNotReplace(example: string, candidate = 'shadow') { - expect( - await migrateLegacyClasses(designSystem, {}, candidate, { - contents: example, - start: example.indexOf(candidate), - end: example.indexOf(candidate) + candidate.length, - }), - ).toEqual(candidate) - } - - await shouldNotReplace(`let notShadow = shadow \n`) - await shouldNotReplace(`{ "foo": shadow.something + ""}\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - await shouldNotReplace(`
\n`) - - // Next.js Image placeholder cases - await shouldNotReplace(``, 'blur') - await shouldNotReplace(``, 'blur') - await shouldNotReplace(``, 'blur') -}) 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 8f3835c8f..2b088aa2e 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts @@ -1,12 +1,12 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import path from 'node:path' import url from 'node:url' -import type { Candidate } from '../../../../tailwindcss/src/candidate' +import { cloneCandidate, type Candidate } from '../../../../tailwindcss/src/candidate' 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 { printCandidate } from './candidates' -import { isSafeMigration } from './is-safe-migration' +import * as version from '../../utils/version' +import { baseCandidate } from './candidates' const __filename = url.fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -33,6 +33,8 @@ const LEGACY_CLASS_MAP = new Map([ ['backdrop-blur-sm', 'backdrop-blur-xs'], ['ring', 'ring-3'], + + ['outline', 'outline-solid'], ]) const THEME_KEYS = new Map([ @@ -57,7 +59,7 @@ const THEME_KEYS = new Map([ ['backdrop-blur-sm', '--backdrop-blur-sm'], ['backdrop-blur-xs', '--backdrop-blur-xs'], - ['ring', '--ring-width'], + ['ring', '--default-ring-width'], ['ring-3', '--ring-width-3'], ]) @@ -67,24 +69,26 @@ const DESIGN_SYSTEMS = new DefaultMap((base) => { export async function migrateLegacyClasses( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, - location?: { - contents: string - start: number - end: number - }, ): Promise { + // These migrations are only safe when migrating from v3 to v4. + // + // Migrating from `rounded` to `rounded-sm` once is fine (v3 -> v4). But if we + // migrate again (v4 -> v4), then `rounded-sm` would be migrated to + // `rounded-xs` which is incorrect because we already migrated this. + if (!version.isMajor(3)) { + return rawCandidate + } + let defaultDesignSystem = await DESIGN_SYSTEMS.get(__dirname) function* migrate(rawCandidate: string) { for (let candidate of designSystem.parseCandidate(rawCandidate)) { // Create a base candidate string from the candidate. // E.g.: `hover:blur!` -> `blur` - let baseCandidate = structuredClone(candidate) as Candidate - baseCandidate.variants = [] - baseCandidate.important = false - let baseCandidateString = printCandidate(designSystem, baseCandidate) + let base = baseCandidate(candidate) + let baseCandidateString = designSystem.printCandidate(base) // Find the new base candidate string. `blur` -> `blur-sm` let newBaseCandidateString = LEGACY_CLASS_MAP.get(baseCandidateString) @@ -96,12 +100,11 @@ export async function migrateLegacyClasses( // Re-apply the variants and important flag from the original candidate. // E.g.: `hover:blur!` -> `blur` -> `blur-sm` -> `hover:blur-sm!` - let newCandidate = structuredClone(newBaseCandidate) as Candidate + let newCandidate = cloneCandidate(newBaseCandidate) as Candidate newCandidate.variants = candidate.variants newCandidate.important = candidate.important yield [ - candidate, newCandidate, THEME_KEYS.get(baseCandidateString), THEME_KEYS.get(newBaseCandidateString), @@ -109,34 +112,7 @@ export async function migrateLegacyClasses( } } - for (let [fromCandidate, toCandidate, fromThemeKey, toThemeKey] of migrate(rawCandidate)) { - // Every utility that has a simple representation (e.g.: `blur`, `radius`, - // etc.`) without variants or special characters _could_ be a potential - // problem during the migration. - let isPotentialProblematicClass = (() => { - if (fromCandidate.variants.length > 0) { - return false - } - - if (fromCandidate.kind === 'arbitrary') { - return false - } - - if (fromCandidate.kind === 'static') { - return !fromCandidate.root.includes('-') - } - - if (fromCandidate.kind === 'functional') { - return fromCandidate.value === null || !fromCandidate.root.includes('-') - } - - return false - })() - - if (location && isPotentialProblematicClass && !isSafeMigration(location)) { - continue - } - + for (let [toCandidate, fromThemeKey, toThemeKey] of migrate(rawCandidate)) { if (fromThemeKey && toThemeKey) { // Migrating something that resolves to a value in the theme. let customFrom = designSystem.resolveThemeValue(fromThemeKey, true) @@ -161,7 +137,7 @@ export async function migrateLegacyClasses( } } - return printCandidate(designSystem, toCandidate) + return designSystem.printCandidate(toCandidate) } return rawCandidate diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-max-width-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-max-width-screen.ts index 92fec5099..cc9b7e9e2 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-max-width-screen.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-max-width-screen.ts @@ -1,10 +1,9 @@ import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { printCandidate } from './candidates' export function migrateMaxWidthScreen( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let candidate of designSystem.parseCandidate(rawCandidate)) { @@ -13,7 +12,7 @@ export function migrateMaxWidthScreen( candidate.root === 'max-w' && candidate.value?.value.startsWith('screen-') ) { - return printCandidate(designSystem, { + return designSystem.printCandidate({ ...candidate, value: { ...candidate.value, diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts index cc4e5a4a5..b1887ae0b 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts @@ -1,33 +1,32 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import type { UserConfig } from '../../../../tailwindcss/src/compat/config/types' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import * as versions from '../../utils/version' +import { migrateArbitraryVariants } from './migrate-arbitrary-variants' import { migrateEmptyArbitraryValues } from './migrate-handle-empty-arbitrary-values' import { migrateModernizeArbitraryValues } from './migrate-modernize-arbitrary-values' import { migratePrefix } from './migrate-prefix' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) + +const css = String.raw + +function migrate(designSystem: DesignSystem, userConfig: UserConfig | null, rawCandidate: string) { + for (let migration of [ + migrateEmptyArbitraryValues, + migratePrefix, + migrateModernizeArbitraryValues, + migrateArbitraryVariants, + (designSystem: DesignSystem, _: UserConfig | null, rawCandidate: string) => { + return designSystem.canonicalizeCandidates([rawCandidate]).pop() ?? rawCandidate + }, + ]) { + rawCandidate = migration(designSystem, userConfig, rawCandidate) + } + return rawCandidate +} test.each([ - // Arbitrary variants - ['[[data-visible]]:flex', 'data-visible:flex'], - ['[&[data-visible]]:flex', 'data-visible:flex'], - ['[[data-visible]&]:flex', 'data-visible:flex'], - ['[&>[data-visible]]:flex', '*:data-visible:flex'], - ['[&_>_[data-visible]]:flex', '*:data-visible:flex'], - ['[&>*]:flex', '*:flex'], - ['[&_>_*]:flex', '*:flex'], - - ['[&_[data-visible]]:flex', '**:data-visible:flex'], - ['[&_*]:flex', '**:flex'], - - ['[&:first-child]:flex', 'first:flex'], - ['[&:not(:first-child)]:flex', 'not-first:flex'], - - // in-* variants - ['[p_&]:flex', 'in-[p]:flex'], - ['[.foo_&]:flex', 'in-[.foo]:flex'], - ['[[data-visible]_&]:flex', 'in-data-visible:flex'], - // Multiple selectors, should stay as-is - ['[[data-foo][data-bar]_&]:flex', '[[data-foo][data-bar]_&]:flex'], - // Using `>` instead of ` ` should not be transformed: - ['[figure>&]:my-0', '[figure>&]:my-0'], // Some extreme examples of what happens in the wild: ['group-[]:flex', 'in-[.group]:flex'], ['group-[]/name:flex', 'in-[.group\\/name]:flex'], @@ -42,65 +41,18 @@ test.each([ ['has-group-[]/name:flex', 'has-in-[.group\\/name]:flex'], ['not-group-[]:flex', 'not-in-[.group]:flex'], ['not-group-[]/name:flex', 'not-in-[.group\\/name]:flex'], - - // nth-child - ['[&:nth-child(2)]:flex', 'nth-2:flex'], - ['[&:not(:nth-child(2))]:flex', 'not-nth-2:flex'], - - ['[&:nth-child(-n+3)]:flex', 'nth-[-n+3]:flex'], - ['[&:not(:nth-child(-n+3))]:flex', 'not-nth-[-n+3]:flex'], - ['[&:nth-child(-n_+_3)]:flex', 'nth-[-n+3]:flex'], - ['[&:not(:nth-child(-n_+_3))]:flex', 'not-nth-[-n+3]:flex'], - - // nth-last-child - ['[&:nth-last-child(2)]:flex', 'nth-last-2:flex'], - ['[&:not(:nth-last-child(2))]:flex', 'not-nth-last-2:flex'], - - ['[&:nth-last-child(-n+3)]:flex', 'nth-last-[-n+3]:flex'], - ['[&:not(:nth-last-child(-n+3))]:flex', 'not-nth-last-[-n+3]:flex'], - ['[&:nth-last-child(-n_+_3)]:flex', 'nth-last-[-n+3]:flex'], - ['[&:not(:nth-last-child(-n_+_3))]:flex', 'not-nth-last-[-n+3]:flex'], - - // nth-child odd/even - ['[&:nth-child(odd)]:flex', 'odd:flex'], - ['[&:not(:nth-child(odd))]:flex', 'even:flex'], - ['[&:nth-child(even)]:flex', 'even:flex'], - ['[&:not(:nth-child(even))]:flex', 'odd:flex'], - - // Keep multiple attribute selectors as-is - ['[[data-visible][data-dark]]:flex', '[[data-visible][data-dark]]:flex'], - - // Complex attribute selectors with operators, quotes and insensitivity flags - ['[[data-url*="example"]]:flex', 'data-[url*="example"]:flex'], - ['[[data-url$=".com"_i]]:flex', 'data-[url$=".com"_i]:flex'], - ['[[data-url$=.com_i]]:flex', 'data-[url$=.com_i]:flex'], - - // Attribute selector wrapped in `&:is(…)` - ['[&:is([data-visible])]:flex', 'data-visible:flex'], - - // Compound arbitrary variants - ['has-[[data-visible]]:flex', 'has-data-visible:flex'], - ['has-[&:is([data-visible])]:flex', 'has-data-visible:flex'], - ['has-[&>[data-visible]]:flex', 'has-[&>[data-visible]]:flex'], - - ['has-[[data-slot=description]]:flex', 'has-data-[slot=description]:flex'], - ['has-[&:is([data-slot=description])]:flex', 'has-data-[slot=description]:flex'], - - ['has-[[aria-visible="true"]]:flex', 'has-aria-visible:flex'], - ['has-[[aria-visible]]:flex', 'has-aria-[visible]:flex'], - - ['has-[&:not(:nth-child(even))]:flex', 'has-odd:flex'], ])('%s => %s (%#)', async (candidate, result) => { - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { - base: __dirname, - }) + let designSystem = await __unstable__loadDesignSystem( + css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + `, + { base: __dirname }, + ) - expect( - [migrateEmptyArbitraryValues, migrateModernizeArbitraryValues].reduce( - (acc, step) => step(designSystem, {}, acc), - candidate, - ), - ).toEqual(result) + expect(migrate(designSystem, {}, candidate)).toEqual(result) }) test.each([ @@ -125,14 +77,15 @@ test.each([ ['not-group-[]:tw-flex', 'tw:not-in-[.tw\\:group]:flex'], ['not-group-[]/name:tw-flex', 'tw:not-in-[.tw\\:group\\/name]:flex'], ])('%s => %s (%#)', async (candidate, result) => { - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss" prefix(tw);', { - base: __dirname, - }) + let designSystem = await __unstable__loadDesignSystem( + css` + @import 'tailwindcss' prefix(tw); + @theme { + --*: initial; + } + `, + { base: __dirname }, + ) - expect( - [migrateEmptyArbitraryValues, migratePrefix, migrateModernizeArbitraryValues].reduce( - (acc, step) => step(designSystem, { prefix: 'tw-' }, acc), - candidate, - ), - ).toEqual(result) + expect(migrate(designSystem, { prefix: 'tw-' }, candidate)).toEqual(result) }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts index f0eec7485..91718b323 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts @@ -1,28 +1,19 @@ -import SelectorParser from 'postcss-selector-parser' -import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate' +import { cloneCandidate, parseCandidate } from '../../../../tailwindcss/src/candidate' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { isPositiveInteger } from '../../../../tailwindcss/src/utils/infer-data-type' -import { printCandidate } from './candidates' - -function memcpy(target: T, source: U): U { - // Clear out the target object, otherwise inspecting the final object will - // look very confusing. - for (let key in target) delete target[key] - - return Object.assign(target, source) -} +import { replaceObject } from '../../../../tailwindcss/src/utils/replace-object' +import { walkVariants } from '../../utils/walk-variants' export function migrateModernizeArbitraryValues( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let candidate of parseCandidate(rawCandidate, designSystem)) { - let clone = structuredClone(candidate) + let clone = cloneCandidate(candidate) let changed = false - for (let [variant, parent] of variants(clone)) { + for (let [variant] of walkVariants(clone)) { // Forward modifier from the root to the compound variant if ( variant.kind === 'compound' && @@ -48,7 +39,7 @@ export function migrateModernizeArbitraryValues( // `group-[]` if (variant.modifier === null) { changed = true - memcpy( + replaceObject( variant, designSystem.parseVariant( designSystem.theme.prefix @@ -61,7 +52,7 @@ export function migrateModernizeArbitraryValues( // `group-[]/name` else if (variant.modifier.kind === 'named') { changed = true - memcpy( + replaceObject( variant, designSystem.parseVariant( designSystem.theme.prefix @@ -72,331 +63,10 @@ export function migrateModernizeArbitraryValues( } continue } - - // Expecting an arbitrary variant - if (variant.kind === 'arbitrary') { - // Expecting a non-relative arbitrary variant - if (variant.relative) continue - - let ast = SelectorParser().astSync(variant.selector) - - // Expecting a single selector node - if (ast.nodes.length !== 1) continue - - // `[&>*]` can be replaced with `*` - if ( - // Only top-level, so `has-[&>*]` is not supported - parent === null && - // [&_>_*]:flex - // ^ ^ ^ - ast.nodes[0].length === 3 && - ast.nodes[0].nodes[0].type === 'nesting' && - ast.nodes[0].nodes[0].value === '&' && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === '>' && - ast.nodes[0].nodes[2].type === 'universal' - ) { - changed = true - memcpy(variant, designSystem.parseVariant('*')) - continue - } - - // `[&_*]` can be replaced with `**` - if ( - // Only top-level, so `has-[&_*]` is not supported - parent === null && - // [&_*]:flex - // ^ ^ - ast.nodes[0].length === 3 && - ast.nodes[0].nodes[0].type === 'nesting' && - ast.nodes[0].nodes[0].value === '&' && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === ' ' && - ast.nodes[0].nodes[2].type === 'universal' - ) { - changed = true - memcpy(variant, designSystem.parseVariant('**')) - continue - } - - // `in-*` variant. If the selector ends with ` &`, we can convert it to an - // `in-*` variant. - // - // E.g.: `[[data-visible]_&]` => `in-data-visible` - if ( - // Only top-level, so `in-[&_[data-visible]]` is not supported - parent === null && - // [[data-visible]___&]:flex - // ^^^^^^^^^^^^^^ ^ ^ - ast.nodes[0].nodes.length === 3 && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === ' ' && - ast.nodes[0].nodes[2].type === 'nesting' - ) { - ast.nodes[0].nodes.pop() // Remove the nesting node - ast.nodes[0].nodes.pop() // Remove the combinator - - changed = true - // When handling a compound like `in-[[data-visible]]`, we will first - // handle `[[data-visible]]`, then the parent `in-*` part. This means - // that we can convert `[[data-visible]_&]` to `in-[[data-visible]]`. - // - // Later this gets converted to `in-data-visible`. - memcpy(variant, designSystem.parseVariant(`in-[${ast.toString()}]`)) - continue - } - - let prefixedVariant: Variant | null = null - - // Handling a child combinator. E.g.: `[&>[data-visible]]` => `*:data-visible` - if ( - // Only top-level, so `has-[&>[data-visible]]` is not supported - parent === null && - // [&_>_[data-visible]]:flex - // ^ ^ ^^^^^^^^^^^^^^ - ast.nodes[0].length === 3 && - ast.nodes[0].nodes[0].type === 'nesting' && - ast.nodes[0].nodes[0].value === '&' && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === '>' && - ast.nodes[0].nodes[2].type === 'attribute' - ) { - ast.nodes[0].nodes = [ast.nodes[0].nodes[2]] - prefixedVariant = designSystem.parseVariant('*') - } - - // Handling a grand child combinator. E.g.: `[&_[data-visible]]` => `**:data-visible` - if ( - // Only top-level, so `has-[&_[data-visible]]` is not supported - parent === null && - // [&_[data-visible]]:flex - // ^ ^^^^^^^^^^^^^^ - ast.nodes[0].length === 3 && - ast.nodes[0].nodes[0].type === 'nesting' && - ast.nodes[0].nodes[0].value === '&' && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === ' ' && - ast.nodes[0].nodes[2].type === 'attribute' - ) { - ast.nodes[0].nodes = [ast.nodes[0].nodes[2]] - prefixedVariant = designSystem.parseVariant('**') - } - - // Filter out `&`. E.g.: `&[data-foo]` => `[data-foo]` - let selectorNodes = ast.nodes[0].filter((node) => node.type !== 'nesting') - - // Expecting a single selector (normal selector or attribute selector) - if (selectorNodes.length !== 1) continue - - let target = selectorNodes[0] - if (target.type === 'pseudo' && target.value === ':is') { - // Expecting a single selector node - if (target.nodes.length !== 1) continue - - // Expecting a single attribute selector - if (target.nodes[0].nodes.length !== 1) continue - - // Unwrap the selector from inside `&:is(…)` - target = target.nodes[0].nodes[0] - } - - // Expecting a pseudo selector - if (target.type === 'pseudo') { - let targetNode = target - let compoundNot = false - if (target.value === ':not') { - compoundNot = true - if (target.nodes.length !== 1) continue - if (target.nodes[0].type !== 'selector') continue - if (target.nodes[0].nodes.length !== 1) continue - if (target.nodes[0].nodes[0].type !== 'pseudo') continue - - targetNode = target.nodes[0].nodes[0] - } - - let newVariant = ((value) => { - // - if (value === ':first-letter') return 'first-letter' - else if (value === ':first-line') return 'first-line' - // - else if (value === ':file-selector-button') return 'file' - else if (value === ':placeholder') return 'placeholder' - else if (value === ':backdrop') return 'backdrop' - // Positional - else if (value === ':first-child') return 'first' - else if (value === ':last-child') return 'last' - else if (value === ':only-child') return 'only' - else if (value === ':first-of-type') return 'first-of-type' - else if (value === ':last-of-type') return 'last-of-type' - else if (value === ':only-of-type') return 'only-of-type' - // State - else if (value === ':visited') return 'visited' - else if (value === ':target') return 'target' - // Forms - else if (value === ':default') return 'default' - else if (value === ':checked') return 'checked' - else if (value === ':indeterminate') return 'indeterminate' - else if (value === ':placeholder-shown') return 'placeholder-shown' - else if (value === ':autofill') return 'autofill' - else if (value === ':optional') return 'optional' - else if (value === ':required') return 'required' - else if (value === ':valid') return 'valid' - else if (value === ':invalid') return 'invalid' - else if (value === ':in-range') return 'in-range' - else if (value === ':out-of-range') return 'out-of-range' - else if (value === ':read-only') return 'read-only' - // Content - else if (value === ':empty') return 'empty' - // Interactive - else if (value === ':focus-within') return 'focus-within' - else if (value === ':focus') return 'focus' - else if (value === ':focus-visible') return 'focus-visible' - else if (value === ':active') return 'active' - else if (value === ':enabled') return 'enabled' - else if (value === ':disabled') return 'disabled' - // - if ( - value === ':nth-child' && - targetNode.nodes.length === 1 && - targetNode.nodes[0].nodes.length === 1 && - targetNode.nodes[0].nodes[0].type === 'tag' && - targetNode.nodes[0].nodes[0].value === 'odd' - ) { - if (compoundNot) { - compoundNot = false - return 'even' - } - return 'odd' - } - if ( - value === ':nth-child' && - targetNode.nodes.length === 1 && - targetNode.nodes[0].nodes.length === 1 && - targetNode.nodes[0].nodes[0].type === 'tag' && - targetNode.nodes[0].nodes[0].value === 'even' - ) { - if (compoundNot) { - compoundNot = false - return 'odd' - } - return 'even' - } - - for (let [selector, variantName] of [ - [':nth-child', 'nth'], - [':nth-last-child', 'nth-last'], - [':nth-of-type', 'nth-of-type'], - [':nth-last-of-type', 'nth-of-last-type'], - ]) { - if (value === selector && targetNode.nodes.length === 1) { - if ( - targetNode.nodes[0].nodes.length === 1 && - targetNode.nodes[0].nodes[0].type === 'tag' && - isPositiveInteger(targetNode.nodes[0].nodes[0].value) - ) { - return `${variantName}-${targetNode.nodes[0].nodes[0].value}` - } - - return `${variantName}-[${targetNode.nodes[0].toString()}]` - } - } - - return null - })(targetNode.value) - - if (newVariant === null) continue - - // Add `not-` prefix - if (compoundNot) newVariant = `not-${newVariant}` - - let parsed = designSystem.parseVariant(newVariant) - if (parsed === null) continue - - // Update original variant - changed = true - memcpy(variant, parsed) - } - - // Expecting an attribute selector - else if (target.type === 'attribute') { - // Attribute selectors - let attributeKey = target.attribute - let attributeValue = target.value - ? target.quoted - ? `${target.quoteMark}${target.value}${target.quoteMark}` - : target.value - : null - - // Insensitive attribute selectors. E.g.: `[data-foo="value" i]` - // ^ - if (target.insensitive && attributeValue) { - attributeValue += ' i' - } - - let operator = target.operator ?? '=' - - // Migrate `data-*` - if (attributeKey.startsWith('data-')) { - changed = true - attributeKey = attributeKey.slice(5) // Remove `data-` - memcpy(variant, { - kind: 'functional', - root: 'data', - modifier: null, - value: - attributeValue === null - ? { kind: 'named', value: attributeKey } - : { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` }, - } satisfies Variant) - } - - // Migrate `aria-*` - else if (attributeKey.startsWith('aria-')) { - changed = true - attributeKey = attributeKey.slice(5) // Remove `aria-` - memcpy(variant, { - kind: 'functional', - root: 'aria', - modifier: null, - value: - attributeValue === null - ? { kind: 'arbitrary', value: attributeKey } // aria-[foo] - : operator === '=' && target.value === 'true' && !target.insensitive - ? { kind: 'named', value: attributeKey } // aria-[foo="true"] or aria-[foo='true'] or aria-[foo=true] - : { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` }, // aria-[foo~="true"], aria-[foo|="true"], … - } satisfies Variant) - } - } - - if (prefixedVariant) { - let idx = clone.variants.indexOf(variant) - if (idx === -1) continue - - // Ensure we inject the prefixed variant - clone.variants.splice(idx, 1, variant, prefixedVariant) - } - } } - return changed ? printCandidate(designSystem, clone) : rawCandidate + return changed ? designSystem.printCandidate(clone) : rawCandidate } return rawCandidate } - -function* variants(candidate: Candidate) { - function* inner( - variant: Variant, - parent: Extract | null = null, - ): Iterable<[Variant, Extract | null]> { - yield [variant, parent] - - if (variant.kind === 'compound') { - yield* inner(variant.variant, variant) - } - } - - for (let variant of candidate.variants) { - yield* inner(variant, null) - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts index 04d9282ab..f47e181af 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts @@ -1,6 +1,8 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { describe, expect, test } from 'vitest' +import { describe, expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migratePrefix } from './migrate-prefix' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) describe('for projects with configured prefix', () => { test.each([ diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts index 95bb4c69c..776032850 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts @@ -1,21 +1,34 @@ +import { decl } from '../../../../tailwindcss/src/ast' import { parseCandidate, type Candidate } from '../../../../tailwindcss/src/candidate' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { segment } from '../../../../tailwindcss/src/utils/segment' -import { printCandidate } from './candidates' +import * as version from '../../utils/version' let seenDesignSystems = new WeakSet() export function migratePrefix( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, rawCandidate: string, ): string { if (!designSystem.theme.prefix) return rawCandidate + if (!userConfig) return rawCandidate + if (!version.isMajor(3)) return rawCandidate if (!seenDesignSystems.has(designSystem)) { - designSystem.utilities.functional('group', () => null) - designSystem.utilities.functional('peer', () => null) + designSystem.utilities.functional('group', (value) => [ + // To ensure that `@apply group` works when computing a signature + decl('--phantom-class', 'group'), + // To ensure `group` and `group/foo` are considered different classes + decl('--phantom-modifier', value.modifier?.value), + ]) + designSystem.utilities.functional('peer', (value) => [ + // To ensure that `@apply peer` works when computing a signature + decl('--phantom-class', 'peer'), + // To ensure `peer` and `peer/foo` are considered different classes + decl('--phantom-modifier', value.modifier?.value), + ]) seenDesignSystems.add(designSystem) } @@ -45,7 +58,7 @@ export function migratePrefix( if (!candidate) return rawCandidate - return printCandidate(designSystem, candidate) + return designSystem.printCandidate(candidate) } // Parses a raw candidate with v3 compatible prefix syntax. This won't match if diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts index cd54234ef..456671dd7 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts @@ -1,6 +1,8 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migrateSimpleLegacyClasses } from './migrate-simple-legacy-classes' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) test.each([ ['overflow-ellipsis', 'text-ellipsis'], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts index dd6599152..415ed524c 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts @@ -1,10 +1,10 @@ import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { printCandidate } from './candidates' +import * as version from '../../utils/version' // Classes that used to exist in Tailwind CSS v3, but do not exist in Tailwind // CSS v4 anymore. -const LEGACY_CLASS_MAP = { +const LEGACY_CLASS_MAP: Record = { 'overflow-ellipsis': 'text-ellipsis', 'flex-grow': 'grow', @@ -15,16 +15,33 @@ const LEGACY_CLASS_MAP = { 'decoration-clone': 'box-decoration-clone', 'decoration-slice': 'box-decoration-slice', - 'outline-none': 'outline-hidden', + // Since v4.1.0 + 'bg-left-top': 'bg-top-left', + 'bg-left-bottom': 'bg-bottom-left', + 'bg-right-top': 'bg-top-right', + 'bg-right-bottom': 'bg-bottom-right', + 'object-left-top': 'object-top-left', + 'object-left-bottom': 'object-bottom-left', + 'object-right-top': 'object-top-right', + 'object-right-bottom': 'object-bottom-right', } let seenDesignSystems = new WeakSet() export function migrateSimpleLegacyClasses( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { + // `outline-none` in v3 has the same meaning as `outline-hidden` in v4. However, + // `outline-none` in v4 _also_ exists but has a different meaning. + // + // We can only migrate `outline-none` to `outline-hidden` if we are migrating a + // v3 project to v4. + if (version.isMajor(3)) { + LEGACY_CLASS_MAP['outline-none'] = 'outline-hidden' + } + // Prepare design system with the unknown legacy classes if (!seenDesignSystems.has(designSystem)) { for (let old in LEGACY_CLASS_MAP) { @@ -35,7 +52,7 @@ export function migrateSimpleLegacyClasses( for (let candidate of designSystem.parseCandidate(rawCandidate)) { if (candidate.kind === 'static' && Object.hasOwn(LEGACY_CLASS_MAP, candidate.root)) { - return printCandidate(designSystem, { + return designSystem.printCandidate({ ...candidate, root: LEGACY_CLASS_MAP[candidate.root as keyof typeof LEGACY_CLASS_MAP], }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.test.ts deleted file mode 100644 index e30109429..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' -import { migrateThemeToVar } from './migrate-theme-to-var' - -let css = String.raw - -test.each([ - // Keep candidates that don't contain `theme(…)` or `theme(…, …)` - ['[color:red]', '[color: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)]', '[color:var(--color-red-500)]'], // Arbitrary property - ['[color:theme(colors.red.500)]/50', '[color:var(--color-red-500)]/50'], // Arbitrary property + modifier - ['bg-[theme(colors.red.500)]', 'bg-(--color-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-[theme(colors.foo.1000,var(--color-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))]', - '[color:--theme(--color-red-500/50,--theme(--color-blue-500/50))]', - ], - [ - '[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50', - '[color:--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%)]', '[color:var(--color-red-500)]/75'], - - // Arbitrary property, with numbers (0-1) without a unit - ['[color:theme(colors.red.500/.12)]', '[color:var(--color-red-500)]/12'], - ['[color:theme(colors.red.500/0.12)]', '[color:var(--color-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%)]', '[color:var(--color-red-500)]/[12.34%]'], - ['[color:theme(colors.red.500/var(--opacity))]', '[color:var(--color-red-500)]/(--opacity)'], - ['[color:theme(colors.red.500/.12345)]', '[color:var(--color-red-500)]/[12.345]'], - ['[color:theme(colors.red.500/50.25%)]', '[color:var(--color-red-500)]/[50.25%]'], - - // Arbitrary value - ['bg-[theme(colors.red.500/75%)]', 'bg-(--color-red-500)/75'], - ['bg-[theme(colors.red.500/12.34%)]', 'bg-(--color-red-500)/[12.34%]'], - - // Arbitrary property that already contains a modifier - ['[color:theme(colors.red.500/50%)]/50', '[color:--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-[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-(--container-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', - ], -])('%s => %s', async (candidate, result) => { - let designSystem = await __unstable__loadDesignSystem( - css` - @import 'tailwindcss'; - `, - { - base: __dirname, - }, - ) - - expect(migrateThemeToVar(designSystem, {}, candidate)).toEqual(result) -}) - -test('extended space scale converts to var or calc', async () => { - let designSystem = await __unstable__loadDesignSystem( - css` - @import 'tailwindcss'; - @theme { - --spacing-2: 2px; - --spacing-miami: 0.875rem; - } - `, - { - base: __dirname, - }, - ) - expect(migrateThemeToVar(designSystem, {}, '[--value:theme(spacing.1)]')).toEqual( - '[--value:--spacing(1)]', - ) - expect(migrateThemeToVar(designSystem, {}, '[--value:theme(spacing.2)]')).toEqual( - '[--value:var(--spacing-2)]', - ) - expect(migrateThemeToVar(designSystem, {}, '[--value:theme(spacing.miami)]')).toEqual( - '[--value:var(--spacing-miami)]', - ) - expect(migrateThemeToVar(designSystem, {}, '[--value:theme(spacing.nyc)]')).toEqual( - '[--value:theme(spacing.nyc)]', - ) -}) - -test('custom space scale converts to var', async () => { - let designSystem = await __unstable__loadDesignSystem( - css` - @import 'tailwindcss'; - @theme { - --spacing-*: initial; - --spacing-1: 0.25rem; - --spacing-2: 0.5rem; - } - `, - { - base: __dirname, - }, - ) - expect(migrateThemeToVar(designSystem, {}, '[--value:theme(spacing.1)]')).toEqual( - '[--value:var(--spacing-1)]', - ) - expect(migrateThemeToVar(designSystem, {}, '[--value:theme(spacing.2)]')).toEqual( - '[--value:var(--spacing-2)]', - ) - expect(migrateThemeToVar(designSystem, {}, '[--value:theme(spacing.3)]')).toEqual( - '[--value:theme(spacing.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 3abb4c96f..e566e7d6e 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 @@ -1,17 +1,11 @@ -import { - parseCandidate, - type Candidate, - type CandidateModifier, - type Variant, -} from '../../../../tailwindcss/src/candidate' +import { type CandidateModifier } from '../../../../tailwindcss/src/candidate' import { keyPathToCssProperty } from '../../../../tailwindcss/src/compat/apply-config-to-theme' -import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { isValidSpacingMultiplier } from '../../../../tailwindcss/src/utils/infer-data-type' import { segment } from '../../../../tailwindcss/src/utils/segment' import { toKeyPath } from '../../../../tailwindcss/src/utils/to-key-path' import * as ValueParser from '../../../../tailwindcss/src/value-parser' -import { printCandidate } from './candidates' +import { walk, WalkAction } from '../../../../tailwindcss/src/walk' export const enum Convert { All = 0, @@ -19,68 +13,6 @@ export const enum Convert { MigrateThemeOnly = 1 << 1, } -export function migrateThemeToVar( - designSystem: DesignSystem, - _userConfig: Config, - rawCandidate: string, -): string { - let convert = createConverter(designSystem) - - for (let candidate of parseCandidate(rawCandidate, designSystem)) { - let clone = structuredClone(candidate) - let changed = false - - if (clone.kind === 'arbitrary') { - let [newValue, modifier] = convert( - clone.value, - clone.modifier === null ? Convert.MigrateModifier : Convert.All, - ) - if (newValue !== clone.value) { - changed = true - clone.value = newValue - - if (modifier !== null) { - clone.modifier = modifier - } - } - } else if (clone.kind === 'functional' && clone.value?.kind === 'arbitrary') { - let [newValue, modifier] = convert( - clone.value.value, - clone.modifier === null ? Convert.MigrateModifier : Convert.All, - ) - if (newValue !== clone.value.value) { - changed = true - clone.value.value = newValue - - if (modifier !== null) { - clone.modifier = modifier - } - } - } - - // Handle variants - for (let variant of variants(clone)) { - if (variant.kind === 'arbitrary') { - let [newValue] = convert(variant.selector, Convert.MigrateThemeOnly) - if (newValue !== variant.selector) { - changed = true - variant.selector = newValue - } - } else if (variant.kind === 'functional' && variant.value?.kind === 'arbitrary') { - let [newValue] = convert(variant.value.value, Convert.MigrateThemeOnly) - if (newValue !== variant.value.value) { - changed = true - variant.value.value = newValue - } - } - } - - return changed ? printCandidate(designSystem, clone) : rawCandidate - } - - return rawCandidate -} - export function createConverter(designSystem: DesignSystem, { prettyPrint = false } = {}) { function convert(input: string, options = Convert.All): [string, CandidateModifier | null] { let ast = ValueParser.parse(input) @@ -96,7 +28,7 @@ export function createConverter(designSystem: DesignSystem, { prettyPrint = fals let themeModifierCount = 0 // Analyze AST - ValueParser.walk(ast, (node) => { + walk(ast, (node) => { if (node.kind !== 'function') return if (node.value !== 'theme') return @@ -104,19 +36,19 @@ export function createConverter(designSystem: DesignSystem, { prettyPrint = fals themeUsageCount += 1 // Figure out if a modifier is used - ValueParser.walk(node.nodes, (child) => { + walk(node.nodes, (child) => { // If we see a `,`, it means that we have a fallback value if (child.kind === 'separator' && child.value.includes(',')) { - return ValueParser.ValueWalkAction.Stop + return WalkAction.Stop } // If we see a `/`, we have a modifier - else if (child.kind === 'separator' && child.value.trim() === '/') { + else if (child.kind === 'word' && child.value === '/') { themeModifierCount += 1 - return ValueParser.ValueWalkAction.Stop + return WalkAction.Stop } - return ValueParser.ValueWalkAction.Skip + return WalkAction.Skip }) }) @@ -198,6 +130,10 @@ export function createConverter(designSystem: DesignSystem, { prettyPrint = fals let variable = `--${keyPathToCssProperty(toKeyPath(path))}` as const if (!designSystem.theme.get([variable])) return null + if (designSystem.theme.prefix) { + return `--${designSystem.theme.prefix}-${variable.slice(2)}` + } + return variable } @@ -237,7 +173,7 @@ function substituteFunctionsInValue( ast: ValueParser.ValueAstNode[], handle: (value: string, fallback?: string) => string | null, ) { - ValueParser.walk(ast, (node, { parent, replaceWith }) => { + walk(ast, (node, ctx) => { if (node.kind === 'function' && node.value === 'theme') { if (node.nodes.length < 1) return @@ -275,10 +211,10 @@ function substituteFunctionsInValue( fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path) if (replacement === null) return - if (parent) { - let idx = parent.nodes.indexOf(node) - 1 + { + let idx = ctx.index - 1 while (idx !== -1) { - let previous = parent.nodes[idx] + let previous = ctx.siblings[idx] // Skip the space separator if (previous.kind === 'separator' && previous.value.trim() === '') { idx -= 1 @@ -306,7 +242,7 @@ function substituteFunctionsInValue( } } - replaceWith(ValueParser.parse(replacement)) + return WalkAction.Replace(ValueParser.parse(replacement)) } }) @@ -332,16 +268,3 @@ function eventuallyUnquote(value: string) { return unquoted } - -function* variants(candidate: Candidate) { - function* inner(variant: Variant): Iterable { - yield variant - if (variant.kind === 'compound') { - yield* inner(variant.variant) - } - } - - for (let variant of candidate.variants) { - yield* inner(variant) - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts index 61589b8fe..1b14d533f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts @@ -1,7 +1,9 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migrateVariantOrder } from './migrate-variant-order' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) let css = dedent diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts index ab18b6b78..3fd84af7c 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts @@ -1,14 +1,24 @@ -import { walk, type AstNode } from '../../../../tailwindcss/src/ast' +import { type AstNode } from '../../../../tailwindcss/src/ast' import { type Variant } from '../../../../tailwindcss/src/candidate' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { printCandidate } from './candidates' +import { walk } from '../../../../tailwindcss/src/walk' +import * as version from '../../utils/version' export function migrateVariantOrder( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { + // This migration is only needed for Tailwind CSS v3 + // + // Changing the variant order when migrating from v3 to v4 is fine, but + // migrating v4 to v4 would make it unsafe because the variant order would + // flip-flop every time you run the migration. + if (!version.isMajor(3)) { + return rawCandidate + } + for (let candidate of designSystem.parseCandidate(rawCandidate)) { if (candidate.variants.length <= 1) { continue @@ -46,7 +56,7 @@ export function migrateVariantOrder( continue } - return printCandidate(designSystem, { ...candidate, variants: newOrder }) + return designSystem.printCandidate({ ...candidate, variants: newOrder }) } return rawCandidate } diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.test.ts new file mode 100644 index 000000000..24bb3d5ef --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.test.ts @@ -0,0 +1,129 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { describe, expect, test, vi } from 'vitest' +import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' +import * as versions from '../../utils/version' +import { migrateCandidate as migrate } from './migrate' +vi.spyOn(versions, 'isMajor').mockReturnValue(false) + +const designSystems = new DefaultMap((base: string) => { + return new DefaultMap((input: string) => { + return __unstable__loadDesignSystem(input, { base }) + }) +}) + +const css = String.raw + +describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', (strategy) => { + let testName = '%s => %s (%#)' + if (strategy === 'with-variant') { + testName = testName.replaceAll('%s', 'focus:%s') + } else if (strategy === 'important') { + testName = testName.replaceAll('%s', '%s!') + } else if (strategy === 'prefix') { + testName = testName.replaceAll('%s', 'tw:%s') + } + + // Basic input with minimal design system to keep the tests fast + let input = css` + @import 'tailwindcss' ${strategy === 'prefix' ? 'prefix(tw)' : ''}; + @theme { + --*: 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); + } + ` + + test.each([ + // Arbitrary property to named functional utility + ['[color:red]', 'text-red-500'], + + // Promote data types to more specific utility if it exists + ['bg-(position:--my-value)', 'bg-position-(--my-value)'], + + // Promote inferred data type to more specific utility if it exists + ['bg-[123px]', 'bg-position-[123px]'], + + // Do not migrate bare values or arbitrary values to named values that are + // deprecated + ['order-[0]', 'order-0'], + ['order-0', 'order-0'], + + // Migrate deprecated named values to bare values + ['order-none', 'order-0'], + + // `-0` should not be migrated to `0`. + // + // This used to be a bug that translate `mt-[0px]` into `-mt-[0px]` because + // `-mt-[0px]` translates to `margin-top: calc(0px * -1);` and therefore we + // handle the `0px * -1` case which translates to `0px` not `-0px`. + // + // This translation is actually fine, because now, we will prefer the + // non-negative version first so we can replace `-mt-[0px]` with `mt-0`. + ['mt-[0px]', 'mt-0'], + ['-mt-[0px]', 'mt-0'], + + // Shorthand CSS Variables should be converted to the new syntax, even if + // the fallback contains functions. The fallback should also be migrated to + // the newest syntax. + ['bg-[--my-color,theme(colors.red.500)]', 'bg-(--my-color,var(--color-red-500))'], + + // Both the positive and negative versions of arbitrary utilities should be + // converted to the bare value version. + ['mb-[32rem]', 'mb-128'], + ['mb-[-32rem]', '-mb-128'], + ])(testName, async (candidate, result) => { + if (strategy === 'with-variant') { + candidate = `focus:${candidate}` + result = `focus:${result}` + } else if (strategy === 'important') { + candidate = `${candidate}!` + result = `${result}!` + } else if (strategy === 'prefix') { + // Not only do we need to prefix the candidate, we also have to make + // sure that we prefix all CSS variables. + candidate = `tw:${candidate.replaceAll('var(--', 'var(--tw-')}` + result = `tw:${result.replaceAll('var(--', 'var(--tw-')}` + } + + let designSystem = await designSystems.get(__dirname).get(input) + let migrated = await migrate(designSystem, {}, candidate) + expect(migrated).toEqual(result) + }) + + test.each([ + ['order-[0]', 'order-0'], + ['order-0', 'order-0'], + + // Do not migrate `order-none` if defined as a custom utility as it is then + // not safe to migrate to `order-0` + ['order-none', 'order-none'], + ])(`${testName} with custom implementations`, async (candidate, result) => { + if (strategy === 'with-variant') { + candidate = `focus:${candidate}` + result = `focus:${result}` + } else if (strategy === 'important') { + candidate = `${candidate}!` + result = `${result}!` + } else if (strategy === 'prefix') { + // Not only do we need to prefix the candidate, we also have to make + // sure that we prefix all CSS variables. + candidate = `tw:${candidate.replaceAll('var(--', 'var(--tw-')}` + result = `tw:${result.replaceAll('var(--', 'var(--tw-')}` + } + + let localInput = css` + ${input} + + @utility order-none { + order: none; /* imagine this exists */ + } + ` + + let designSystem = await designSystems.get(__dirname).get(localInput) + let migrated = await migrate(designSystem, {}, candidate) + expect(migrated).toEqual(result) + }) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts index c18e2f25e..976c63a83 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts @@ -1,53 +1,78 @@ import fs from 'node:fs/promises' import path, { extname } from 'node:path' +import { + createSignatureOptions, + prepareDesignSystemStorage, + UTILITY_SIGNATURE_KEY, +} from '../../../../tailwindcss/src/canonicalize-candidates' 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 { migrateArbitraryValueToBareValue } from './migrate-arbitrary-value-to-bare-value' +import { isSafeMigration } from './is-safe-migration' import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection' -import { migrateBgGradient } from './migrate-bg-gradient' +import { migrateCamelcaseInNamedValue } from './migrate-camelcase-in-named-value' +import { migrateCanonicalizeCandidate } from './migrate-canonicalize-candidate' import { migrateEmptyArbitraryValues } from './migrate-handle-empty-arbitrary-values' -import { migrateImportant } from './migrate-important' import { migrateLegacyArbitraryValues } from './migrate-legacy-arbitrary-values' import { migrateLegacyClasses } from './migrate-legacy-classes' import { migrateMaxWidthScreen } from './migrate-max-width-screen' import { migrateModernizeArbitraryValues } from './migrate-modernize-arbitrary-values' import { migratePrefix } from './migrate-prefix' import { migrateSimpleLegacyClasses } from './migrate-simple-legacy-classes' -import { migrateThemeToVar } from './migrate-theme-to-var' import { migrateVariantOrder } from './migrate-variant-order' export type Migration = ( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, rawCandidate: string, - location?: { - contents: string - start: number - end: number - }, ) => string | Promise export const DEFAULT_MIGRATIONS: Migration[] = [ migrateEmptyArbitraryValues, migratePrefix, - migrateImportant, - migrateBgGradient, + migrateCanonicalizeCandidate, migrateSimpleLegacyClasses, + migrateCamelcaseInNamedValue, migrateLegacyClasses, migrateMaxWidthScreen, - migrateThemeToVar, migrateVariantOrder, // Has to happen before migrations that modify variants migrateAutomaticVarInjection, migrateLegacyArbitraryValues, - migrateArbitraryValueToBareValue, migrateModernizeArbitraryValues, ] +let migrateCached = new DefaultMap((baseDesignSystem: DesignSystem) => { + let designSystem = prepareDesignSystemStorage(baseDesignSystem) + let options = createSignatureOptions(designSystem) + + return new DefaultMap((userConfig: Config | null) => { + return new DefaultMap(async (rawCandidate) => { + let original = rawCandidate + + for (let migration of DEFAULT_MIGRATIONS) { + rawCandidate = await migration(designSystem, userConfig, rawCandidate) + } + + // Canonicalize the final migrated candidate to its final form + rawCandidate = designSystem.canonicalizeCandidates([rawCandidate]).pop()! + + // Verify that the candidate actually makes sense at all. E.g.: `duration` + // is not a valid candidate, but it will parse because `duration-` + // exists. + let signature = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options).get(rawCandidate) + if (typeof signature !== 'string') return original + + return rawCandidate + }) + }) +}) + export async function migrateCandidate( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, rawCandidate: string, // Location is only set when migrating a candidate from a source file location?: { @@ -56,15 +81,17 @@ export async function migrateCandidate( end: number }, ): Promise { - for (let migration of DEFAULT_MIGRATIONS) { - rawCandidate = await migration(designSystem, userConfig, rawCandidate, location) + // Skip this migration if we think that the migration is unsafe + if (location && !isSafeMigration(rawCandidate, location, designSystem)) { + return rawCandidate } - return rawCandidate + + return migrateCached.get(designSystem).get(userConfig).get(rawCandidate) } export default async function migrateContents( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, contents: string, extension: string, ): Promise { @@ -93,12 +120,18 @@ export default async function migrateContents( return spliceChangesIntoString(contents, changes) } -export async function migrate(designSystem: DesignSystem, userConfig: Config, file: string) { +export async function migrate( + designSystem: DesignSystem, + userConfig: Config | null, + file: string, +): Promise { let fullPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file) let contents = await fs.readFile(fullPath, 'utf-8') - await fs.writeFile( - fullPath, - await migrateContents(designSystem, userConfig, contents, extname(file)), - ) + 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 } diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts index b86647c82..2eae32f30 100644 --- a/packages/@tailwindcss-upgrade/src/index.test.ts +++ b/packages/@tailwindcss-upgrade/src/index.test.ts @@ -2,10 +2,12 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' import path from 'node:path' import postcss from 'postcss' -import { expect, it } from 'vitest' +import { expect, it, vi } from 'vitest' import { formatNodes } from './codemods/css/format-nodes' import { migrateContents } from './codemods/css/migrate' import { sortBuckets } from './codemods/css/sort-buckets' +import * as versions from './utils/version' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) const css = dedent diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 75bd47b52..d304bbe1f 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -1,12 +1,13 @@ #!/usr/bin/env node -import { globby } from 'globby' +import { Scanner } from '@tailwindcss/oxide' +import { globby, isGitIgnored } from 'globby' import fs from 'node:fs/promises' import path from 'node:path' +import pc from 'picocolors' import postcss from 'postcss' import { migrateJsConfig } from './codemods/config/migrate-js-config' import { migratePostCSSConfig } from './codemods/config/migrate-postcss' -import { migratePrettierPlugin } from './codemods/config/migrate-prettier' import { analyze as analyzeStylesheets } from './codemods/css/analyze' import { formatNodes } from './codemods/css/format-nodes' import { linkConfigs as linkConfigsToStylesheets } from './codemods/css/link' @@ -18,11 +19,11 @@ import { prepareConfig } from './codemods/template/prepare-config' import { help } from './commands/help' import { Stylesheet } from './stylesheet' import { args, type Arg } from './utils/args' -import { isRepoDirty } from './utils/git' -import { hoistStaticGlobParts } from './utils/hoist-static-glob-parts' -import { getPackageVersion } from './utils/package-version' +import { gitRoot, 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' }, @@ -42,6 +43,9 @@ 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() @@ -59,11 +63,27 @@ async function run() { } } - // Require an installed `tailwindcss` version < 4 - let tailwindVersion = await getPackageVersion('tailwindcss', base) - if (tailwindVersion && Number(tailwindVersion.split('.')[0]) !== 3) { + info(`Upgrading from Tailwind CSS ${highlight(`v${version.installedTailwindVersion(base)}`)}`, { + prefix: '↳ ', + }) + + if (version.installedTailwindVersion(base) !== version.expectedTailwindVersion(base)) { + let pkgManager = await pkg(base).manager() + error( - `Tailwind CSS v${tailwindVersion} found. The migration tool can only be run on v3 projects.`, + [ + 'Version mismatch', + '', + pc.dim('```diff'), + `${pc.red('-')} ${`${pc.dim('"tailwindcss":')} ${`${pc.dim('"')}${pc.blue(version.expectedTailwindVersion(base))}${pc.dim('"')}`}`} (expected version in ${highlight('package.json')})`, + `${pc.green('+')} ${`${pc.dim('"tailwindcss":')} ${`${pc.dim('"')}${pc.blue(version.installedTailwindVersion(base))}${pc.dim('"')}`}`} (installed version in ${highlight('node_modules')})`, + pc.dim('```'), + '', + `Make sure to run ${highlight(`${pkgManager} install`)} and try again.`, + ].join('\n'), + { + prefix: '↳ ', + }, ) process.exit(1) } @@ -79,8 +99,11 @@ 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 + ignore: ['**/node_modules/**'], }) } @@ -100,26 +123,30 @@ async function run() { let stylesheets = loadResults .filter((result) => result.status === 'fulfilled') .map((result) => result.value) + let originals = new Map(stylesheets.map((sheet) => [sheet, sheet.root.toString()])) // Analyze the stylesheets try { - await analyzeStylesheets(stylesheets) + await analyzeStylesheets(stylesheets, { base }) } catch (e: any) { error(`${e?.message ?? e}`, { prefix: '↳ ' }) } - // Ensure stylesheets are linked to configs - try { - await linkConfigsToStylesheets(stylesheets, { - configPath: flags['--config'], - base, - }) - } catch (e: any) { - error(`${e?.message ?? e}`, { prefix: '↳ ' }) + // Ensure stylesheets are linked to configs. But this is only necessary when + // migrating from v3 to v4. + if (version.isMajor(3)) { + try { + await linkConfigsToStylesheets(stylesheets, { + configPath: flags['--config'], + base, + }) + } catch (e: any) { + error(`${e?.message ?? e}`, { prefix: '↳ ' }) + } } // Migrate js config files, linked to stylesheets - if (stylesheets.some((sheet) => sheet.isTailwindRoot)) { + if (stylesheets.some((sheet) => sheet.isTailwindRoot && sheet.linkedConfigPath)) { info('Migrating JavaScript configuration files…') } let configBySheet = new Map>>() @@ -129,6 +156,7 @@ async function run() { >() for (let sheet of stylesheets) { if (!sheet.isTailwindRoot) continue + if (!version.isMajor(3) && !sheet.linkedConfigPath) continue let config = await prepareConfig(sheet.linkedConfigPath, { base }) configBySheet.set(sheet, config) @@ -153,41 +181,6 @@ async function run() { } } - // Migrate source files, linked to config files - if (configBySheet.size > 0) { - info('Migrating templates…') - } - { - // Template migrations - for (let config of configBySheet.values()) { - let set = new Set() - for (let globEntry of config.sources.flatMap((entry) => hoistStaticGlobParts(entry))) { - let files = await globby([globEntry.pattern], { - absolute: true, - gitignore: true, - cwd: globEntry.base, - }) - - for (let file of files) { - set.add(file) - } - } - - let files = Array.from(set) - files.sort() - - // Migrate each file - await Promise.allSettled( - files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)), - ) - - success( - `Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))}`, - { prefix: '↳ ' }, - ) - } - } - // Migrate each CSS file if (stylesheets.length > 0) { info('Migrating stylesheets…') @@ -195,20 +188,33 @@ async function run() { await Promise.all( stylesheets.map(async (sheet) => { try { - let config = configBySheet.get(sheet)! - let jsConfigMigration = jsConfigMigrationBySheet.get(sheet)! + let config = configBySheet.get(sheet) + let jsConfigMigration = jsConfigMigrationBySheet.get(sheet) ?? null if (!config) { for (let parent of sheet.ancestors()) { if (parent.isTailwindRoot) { config ??= configBySheet.get(parent)! - jsConfigMigration ??= jsConfigMigrationBySheet.get(parent)! + jsConfigMigration ??= jsConfigMigrationBySheet.get(parent) ?? null break } } } - await migrateStylesheet(sheet, { ...config, jsConfigMigration }) + // 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()), + userConfig: config?.userConfig ?? null, + configFilePath: config?.configFilePath ?? null, + jsConfigMigration, + }) } catch (e: any) { error(`${e?.message ?? e} in ${highlight(relative(sheet.file!, base))}`, { prefix: '↳ ' }) } @@ -216,36 +222,39 @@ async function run() { ) // Split up stylesheets (as needed) - try { - await splitStylesheets(stylesheets) - } catch (e: any) { - error(`${e?.message ?? e}`, { prefix: '↳ ' }) - } + if (version.isMajor(3)) { + try { + await splitStylesheets(stylesheets) + } catch (e: any) { + error(`${e?.message ?? e}`, { prefix: '↳ ' }) + } - // Cleanup `@import "…" layer(utilities)` - for (let sheet of stylesheets) { - for (let importRule of sheet.importRules) { - if (!importRule.raws.tailwind_injected_layer) continue - let importedSheet = stylesheets.find( - (sheet) => sheet.id === importRule.raws.tailwind_destination_sheet_id, - ) - if (!importedSheet) continue + // Cleanup `@import "…" layer(utilities)` + for (let sheet of stylesheets) { + for (let importRule of sheet.importRules) { + if (!importRule.raws.tailwind_injected_layer) continue + let importedSheet = stylesheets.find( + (sheet) => sheet.id === importRule.raws.tailwind_destination_sheet_id, + ) + if (!importedSheet) continue - // Only remove the `layer(…)` next to the import if any of the children - // contain `@utility`. Otherwise `@utility` will not be top-level. - if ( - !importedSheet.containsRule((node) => node.type === 'atrule' && node.name === 'utility') - ) { - continue + // Only remove the `layer(…)` next to the import if any of the children + // contain `@utility`. Otherwise `@utility` will not be top-level. + if ( + !importedSheet.containsRule((node) => node.type === 'atrule' && node.name === 'utility') + ) { + continue + } + + // Make sure to remove the `layer(…)` from the `@import` at-rule + importRule.params = importRule.params.replace(/ layer\([^)]+\)/, '').trim() } - - // Make sure to remove the `layer(…)` from the `@import` at-rule - importRule.params = importRule.params.replace(/ layer\([^)]+\)/, '').trim() } } // Format nodes for (let sheet of stylesheets) { + if (originals.get(sheet) === sheet.root.toString()) continue await postcss([sortBuckets(), formatNodes()]).process(sheet.root!, { from: sheet.file! }) } @@ -253,31 +262,186 @@ async function run() { for (let sheet of stylesheets) { if (!sheet.file) continue - await fs.writeFile(sheet.file, sheet.root.toString()) + await writeFileSafely(sheet.file, sheet.root.toString()) if (sheet.isTailwindRoot) { success(`Migrated stylesheet: ${highlight(relative(sheet.file, base))}`, { prefix: '↳ ' }) } } + + info('Updating dependencies…') + { + let pkgManager = pkg(base) + let dependencies = [ + 'tailwindcss', + '@tailwindcss/cli', + '@tailwindcss/postcss', + '@tailwindcss/vite', + '@tailwindcss/node', + '@tailwindcss/oxide', + 'prettier-plugin-tailwindcss', + ].filter((dependency) => dependency === 'tailwindcss' || pkgManager.has(dependency)) + try { + await pkgManager.add(dependencies.map((dependency) => `${dependency}@latest`)) + for (let dependency of dependencies) { + success(`Updated package: ${highlight(dependency)}`, { prefix: '↳ ' }) + } + } catch {} + } + + let tailwindRootStylesheets = stylesheets.filter((sheet) => sheet.isTailwindRoot && sheet.file) + + // Migrate source files + if (tailwindRootStylesheets.length > 0) { + info('Migrating templates…') + } + { + let seenFiles = new Set() + + // Template migrations + for (let sheet of tailwindRootStylesheets) { + let compiler = await sheet.compiler() + if (!compiler) continue + 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 + if (compiler.root === 'none') { + return [] + } + + // No root specified + 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 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: '↳ ' }) + } + }), + ) + + if (config?.configFilePath) { + success( + `Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))} (${changes} file${changes === 1 ? '' : 's'} changed)`, + { prefix: '↳ ' }, + ) + } else { + success( + `Migrated templates for: ${highlight(relative(sheet.file ?? '', base))} (${changes} file${changes === 1 ? '' : 's'} changed)`, + { prefix: '↳ ' }, + ) + } + } + } } - { + if (version.isMajor(3)) { // PostCSS config migration await migratePostCSSConfig(base) } - info('Updating dependencies…') - { - // Migrate the prettier plugin to the latest version - await migratePrettierPlugin(base) - } - - try { - // Upgrade Tailwind CSS - await pkg(base).add(['tailwindcss@latest']) - success(`Updated package: ${highlight('tailwindcss')}`, { prefix: '↳ ' }) - } catch {} - // Run all cleanup functions because we completed the migration await Promise.allSettled(cleanup.map((fn) => fn())) diff --git a/packages/@tailwindcss-upgrade/src/stylesheet.ts b/packages/@tailwindcss-upgrade/src/stylesheet.ts index 092d9414b..573413262 100644 --- a/packages/@tailwindcss-upgrade/src/stylesheet.ts +++ b/packages/@tailwindcss-upgrade/src/stylesheet.ts @@ -1,8 +1,10 @@ +import { __unstable__loadDesignSystem, compileAst } from '@tailwindcss/node' import * as fsSync from 'node:fs' import * as fs from 'node:fs/promises' import * as path from 'node:path' import * as util from 'node:util' import * as postcss from 'postcss' +import { postCssAstToCssAst } from '../../@tailwindcss-postcss/src/ast' export type StylesheetId = string @@ -263,6 +265,28 @@ 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), { + 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(), { + base: path.dirname(this.file), + })) + } + [util.inspect.custom]() { return { ...this, diff --git a/packages/@tailwindcss-upgrade/src/utils/git.ts b/packages/@tailwindcss-upgrade/src/utils/git.ts index d716fbb8a..38edf21cb 100644 --- a/packages/@tailwindcss-upgrade/src/utils/git.ts +++ b/packages/@tailwindcss-upgrade/src/utils/git.ts @@ -1,8 +1,20 @@ import { execSync } from 'node:child_process' -export function isRepoDirty() { +export function gitRoot(cwd?: string): string | null { try { - let stdout = execSync('git status --porcelain', { encoding: 'utf-8' }) + 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 }) 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/hoist-static-glob-parts.test.ts b/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.test.ts deleted file mode 100644 index 1e6d8b1e8..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { expect, it } from 'vitest' -import { hoistStaticGlobParts } from './hoist-static-glob-parts' - -it.each([ - // A basic glob - [ - { base: '/projects/project-a', pattern: './src/**/*.html' }, - [{ base: '/projects/project-a/src', pattern: '**/*.html' }], - ], - - // A glob pointing to a folder should result in `**/*` - [ - { base: '/projects/project-a', pattern: './src' }, - [{ base: '/projects/project-a/src', pattern: '**/*' }], - ], - - // A glob pointing to a file, should result in the file as the pattern - [ - { base: '/projects/project-a', pattern: './src/index.html' }, - [{ base: '/projects/project-a/src', pattern: 'index.html' }], - ], - - // A glob going up a directory, should result in the new directory as the base - [ - { base: '/projects/project-a', pattern: '../project-b/src/**/*.html' }, - [{ base: '/projects/project-b/src', pattern: '**/*.html' }], - ], - - // A glob with curlies, should be expanded to multiple globs - [ - { base: '/projects/project-a', pattern: '../project-{b,c}/src/**/*.html' }, - [ - { base: '/projects/project-b/src', pattern: '**/*.html' }, - { base: '/projects/project-c/src', pattern: '**/*.html' }, - ], - ], - [ - { base: '/projects/project-a', pattern: '../project-{b,c}/src/**/*.{js,html}' }, - [ - { base: '/projects/project-b/src', pattern: '**/*.js' }, - { base: '/projects/project-b/src', pattern: '**/*.html' }, - { base: '/projects/project-c/src', pattern: '**/*.js' }, - { base: '/projects/project-c/src', pattern: '**/*.html' }, - ], - ], -])('should hoist the static parts of the glob: %s', (input, output) => { - expect(hoistStaticGlobParts(input)).toEqual(output) -}) diff --git a/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.ts b/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.ts deleted file mode 100644 index 18d587998..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { normalizePath } from '@tailwindcss/node' -import braces from 'braces' -import path from 'node:path' - -interface GlobEntry { - base: string - pattern: string -} - -export function hoistStaticGlobParts(entry: GlobEntry): GlobEntry[] { - return braces(entry.pattern, { expand: true }).map((pattern) => { - let clone = { ...entry } - let [staticPart, dynamicPart] = splitPattern(pattern) - - // Move static part into the `base`. - let absolutePosixPath = normalizePath(entry.base) - - if (staticPart !== null) { - clone.base = path.posix.join(absolutePosixPath, staticPart) - } else { - clone.base = absolutePosixPath - } - - // Move dynamic part into the `pattern`. - if (dynamicPart === null) { - clone.pattern = '**/*' - } else { - clone.pattern = dynamicPart - } - - // If the pattern looks like a file, move the file name from the `base` to - // the `pattern`. - let file = path.basename(clone.base) - if (file.includes('.')) { - clone.pattern = file - clone.base = path.dirname(clone.base) - } - - return clone - }) -} - -// Split a glob pattern into a `static` and `dynamic` part. -// -// Assumption: we assume that all globs are expanded, which means that the only -// dynamic parts are using `*`. -// -// E.g.: -// Original input: `../project-b/**/*.{html,js}` -// Expanded input: `../project-b/**/*.html` & `../project-b/**/*.js` -// Split on first input: ("../project-b", "**/*.html") -// Split on second input: ("../project-b", "**/*.js") -function splitPattern(pattern: string): [staticPart: string | null, dynamicPart: string | null] { - // No dynamic parts, so we can just return the input as-is. - if (!pattern.includes('*')) { - return [pattern, null] - } - - let lastSlashPosition: number | null = null - - for (let i = 0; i < pattern.length; i++) { - let c = pattern[i] - if (c === '/') { - lastSlashPosition = i - } - - if (c === '*' || c === '!') { - break - } - } - - // Very first character is a `*`, therefore there is no static part, only a - // dynamic part. - if (lastSlashPosition === null) { - return [null, pattern] - } - - let staticPart = pattern.slice(0, lastSlashPosition).trim() - let dynamicPart = pattern.slice(lastSlashPosition + 1).trim() - - return [staticPart || null, dynamicPart || null] -} diff --git a/packages/@tailwindcss-upgrade/src/utils/package-version.ts b/packages/@tailwindcss-upgrade/src/utils/package-version.ts index 833ccadf2..64a1e7ed1 100644 --- a/packages/@tailwindcss-upgrade/src/utils/package-version.ts +++ b/packages/@tailwindcss-upgrade/src/utils/package-version.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs' import fs from 'node:fs/promises' import { resolveJsId } from './resolve' @@ -15,3 +16,14 @@ export async function getPackageVersion(pkg: string, base: string): Promise = { bun: '-d', } +const manifests = new DefaultMap((base) => { + try { + let packageJsonPath = resolve(base, 'package.json') + return readFileSync(packageJsonPath, 'utf-8') + } catch { + return '' + } +}) + export function pkg(base: string) { return { + async manager() { + return await packageManagerForBase.get(base) + }, async add(packages: string[], location: 'dependencies' | 'devDependencies' = 'dependencies') { let packageManager = await packageManagerForBase.get(base) let args = packages.slice() @@ -21,6 +34,13 @@ export function pkg(base: string) { args.push(SAVE_DEV[packageManager] || SAVE_DEV.default) } + // Allow running the `pnpm` command in the workspace root without + // erroring. Can't just use `--workspace-root` because that will force + // install dependencies in the workspace root. + if (packageManager === 'pnpm') { + args.push('--ignore-workspace-root-check') + } + let command = `${packageManager} add ${args.join(' ')}` try { return await exec(command, { cwd: base }) @@ -29,8 +49,13 @@ export function pkg(base: string) { prefix: '↳ ', }) throw e + } finally { + manifests.delete(base) } }, + has(name: string) { + return manifests.get(base).includes(`"${name}":`) + }, async remove(packages: string[]) { let packageManager = await packageManagerForBase.get(base) let command = `${packageManager} remove ${packages.join(' ')}` @@ -41,6 +66,8 @@ export function pkg(base: string) { prefix: '↳ ', }) throw e + } finally { + manifests.delete(base) } }, } diff --git a/packages/@tailwindcss-upgrade/src/utils/renderer.ts b/packages/@tailwindcss-upgrade/src/utils/renderer.ts index 38bd9826a..c9a967801 100644 --- a/packages/@tailwindcss-upgrade/src/utils/renderer.ts +++ b/packages/@tailwindcss-upgrade/src/utils/renderer.ts @@ -44,7 +44,7 @@ export function wordWrap(text: string, width: number): string[] { // Handle text with newlines by maintaining the newlines, then splitting // each line separately. if (text.includes('\n')) { - return text.split('\n').flatMap((line) => wordWrap(line, width)) + return text.split('\n').flatMap((line) => (line ? wordWrap(line, width) : [''])) } let words = text.split(' ') diff --git a/packages/@tailwindcss-upgrade/src/utils/version.ts b/packages/@tailwindcss-upgrade/src/utils/version.ts new file mode 100644 index 000000000..27bc44f2b --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/version.ts @@ -0,0 +1,63 @@ +import { execSync } from 'node:child_process' +import semver from 'semver' +import { DefaultMap } from '../../../tailwindcss/src/utils/default-map' +import { getPackageVersionSync } from './package-version' + +/** + * Must be of major version. + * + * E.g.: `isMajor(3)` + */ +export function isMajor(version: number) { + return semver.satisfies(installedTailwindVersion(), `>=${version}.0.0 <${version + 1}.0.0`) +} + +/** + * Must be of greater than the current major version including minor and patch. + * + * E.g.: `isGreaterThan(3)` + */ +export function isGreaterThan(version: number) { + return semver.gte(installedTailwindVersion(), `${version + 1}.0.0`) +} + +let cache = new DefaultMap((base) => { + let tailwindVersion = getPackageVersionSync('tailwindcss', base) + if (!tailwindVersion) throw new Error('Tailwind CSS is not installed') + return tailwindVersion +}) + +export function installedTailwindVersion(base = process.cwd()): string { + return cache.get(base) +} + +let expectedCache = new DefaultMap((base) => { + try { + // This will report a problem if the package.json/package-lock.json + // mismatches with the installed version in node_modules. + // + // Also tested this with Bun and PNPM, both seem to work fine. + execSync('npm ls tailwindcss --json', { cwd: base, stdio: 'pipe' }) + return installedTailwindVersion(base) + } catch (_e) { + try { + let e = _e as { stdout: Buffer } + let data = JSON.parse(e.stdout.toString()) + + return ( + // Could be a sub-dependency issue, but we are only interested in + // the top-level version mismatch. + /"(.*?)" from the root project/.exec(data.dependencies.tailwindcss.invalid)?.[1] ?? + // Fallback to the installed version + installedTailwindVersion(base) + ) + } catch { + // We don't know how to verify, so let's just return the installed + // version to not block the user. + return installedTailwindVersion(base) + } + } +}) +export function expectedTailwindVersion(base = process.cwd()): string { + return expectedCache.get(base) +} diff --git a/packages/@tailwindcss-upgrade/src/utils/walk-variants.ts b/packages/@tailwindcss-upgrade/src/utils/walk-variants.ts new file mode 100644 index 000000000..e0dd16899 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/walk-variants.ts @@ -0,0 +1,18 @@ +import type { Candidate, Variant } from '../../../tailwindcss/src/candidate' + +export function* walkVariants(candidate: Candidate) { + function* inner( + variant: Variant, + parent: Extract | null = null, + ): Iterable<[Variant, Extract | null]> { + yield [variant, parent] + + if (variant.kind === 'compound') { + yield* inner(variant.variant, variant) + } + } + + for (let variant of candidate.variants) { + yield* inner(variant, null) + } +} diff --git a/packages/@tailwindcss-upgrade/src/utils/write-file-safely.ts b/packages/@tailwindcss-upgrade/src/utils/write-file-safely.ts new file mode 100644 index 000000000..2ff99f9e7 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/write-file-safely.ts @@ -0,0 +1,49 @@ +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 95ec9d87d..1a5d25c73 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

--- @@ -27,14 +27,50 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ## Community -For help, discussion about best practices, or any other conversation that would benefit from being searchable: +For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) - -For chatting with others using the framework: - -[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. + +--- + +## `@tailwindcss/vite` plugin API + +### Enabling or disabling Lightning CSS + +By default, this plugin detects whether or not the CSS is being built for production by checking the `NODE_ENV` environment variable. When building for production Lightning CSS will be enabled otherwise it is disabled. + +If you want to always enable or disable Lightning CSS the `optimize` option may be used: + +```js +import tailwindcss from '@tailwindcss/vite' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + tailwindcss({ + // Disable Lightning CSS optimization + optimize: false, + }), + ], +}) +``` + +It's also possible to keep Lightning CSS enabled but disable minification: + +```js +import tailwindcss from '@tailwindcss/vite' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + tailwindcss({ + // Enable Lightning CSS but disable minification + optimize: { minify: false }, + }), + ], +}) +``` diff --git a/packages/@tailwindcss-vite/package.json b/packages/@tailwindcss-vite/package.json index fe7aedc82..821704a1d 100644 --- a/packages/@tailwindcss-vite/package.json +++ b/packages/@tailwindcss-vite/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/vite", - "version": "4.1.3", + "version": "4.3.3", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -24,7 +24,7 @@ "exports": { ".": { "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" + "default": "./dist/index.mjs" } }, "dependencies": { @@ -37,6 +37,6 @@ "vite": "catalog:" }, "peerDependencies": { - "vite": "^5.2.0 || ^6" + "vite": "^5.2.0 || ^6 || ^7 || ^8" } } diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index a484583a3..8fc660eaa 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -1,41 +1,178 @@ -import { compile, env, Features, Instrumentation, normalizePath, optimize } from '@tailwindcss/node' +import { + compile, + env, + Features, + Instrumentation, + normalizePath, + optimize, + toSourceMap, +} from '@tailwindcss/node' 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 { Plugin, ResolvedConfig, ViteDevServer } from 'vite' +import type { Environment, InternalResolveOptions, Plugin, ResolvedConfig } from 'vite' +import * as vite 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 default function tailwindcss(): Plugin[] { - let servers: ViteDevServer[] = [] +export type PluginOptions = { + /** + * Optimize and minify the output CSS. + */ + 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 config: ResolvedConfig | null = null + let rootsByEnv = new DefaultMap>((env: string) => new Map()) let isSSR = false - let minify = false + let shouldOptimize = true + let minify = true - 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) + 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 jsResolver = config!.createResolver(config!.resolve) - function customJsResolver(id: string, base: string) { - return jsResolver(id, base, true, isSSR) - } - return new Root(id, config!.root, customCssResolver, customJsResolver) - }) + return new Root( + id, + config!.root, + // Currently, Vite only supports CSS source maps in development and they + // are off by default. Check to see if we need them or not. + config?.css.devSourcemap ?? false, + customCssResolver, + customJsResolver, + ) + } return [ { @@ -43,14 +180,24 @@ export default function tailwindcss(): Plugin[] { name: '@tailwindcss/vite:scan', enforce: 'pre', - configureServer(server) { - servers.push(server) - }, - async configResolved(_config) { config = _config - minify = config.build.cssMinify !== false isSSR = config.build.ssr !== false && config.build.ssr !== undefined + + // By default we optimize CSS during the build phase but if the user + // provides explicit options we'll use those instead + if (opts.optimize !== undefined) { + shouldOptimize = opts.optimize !== false + } + + // Minification is also performed when optimizing as long as it's also + // enabled in Vite + minify = shouldOptimize && config.build.cssMinify !== false + + // But again, the user can override that choice explicitly + if (typeof opts.optimize === 'object') { + minify = opts.optimize.minify !== false + } }, }, @@ -59,23 +206,35 @@ export default function tailwindcss(): 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 - async transform(src, id, options) { - if (!isPotentialCssRootFile(id)) return + using I = new Instrumentation() + DEBUG && I.start('[@tailwindcss/vite] Generate CSS (serve)') - using I = new Instrumentation() - DEBUG && I.start('[@tailwindcss/vite] Generate CSS (serve)') + 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) + } - let root = roots.get(id) + let result = await root.generate(src, (file) => this.addWatchFile(file), I) + if (!result) { + roots.delete(id) + return + } - let generated = await root.generate(src, (file) => this.addWatchFile(file), I) - if (!generated) { - roots.delete(id) - return src - } - - DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)') - return { code: generated } + DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)') + return result + }, }, }, @@ -84,27 +243,44 @@ export default function tailwindcss(): 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 - async transform(src, id) { - if (!isPotentialCssRootFile(id)) return + using I = new Instrumentation() + DEBUG && I.start('[@tailwindcss/vite] Generate CSS (build)') - using I = new Instrumentation() - DEBUG && I.start('[@tailwindcss/vite] Generate CSS (build)') + 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) + } - let root = roots.get(id) + 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 generated = await root.generate(src, (file) => this.addWatchFile(file), I) - if (!generated) { - roots.delete(id) - return src - } - DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)') + if (shouldOptimize) { + DEBUG && I.start('[@tailwindcss/vite] Optimize CSS') + result = optimize(result.code, { + minify, + map: result.map, + }) + DEBUG && I.end('[@tailwindcss/vite] Optimize CSS') + } - DEBUG && I.start('[@tailwindcss/vite] Optimize CSS') - generated = optimize(generated, { minify }) - DEBUG && I.end('[@tailwindcss/vite] Optimize CSS') - - return { code: generated } + return result + }, }, }, ] satisfies Plugin[] @@ -116,13 +292,15 @@ function getExtension(id: string) { } function isPotentialCssRootFile(id: string) { - if (id.includes('/.vite/')) return + 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 + let extension = getExtension(id) - 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) + let isCssFile = extension === 'css' || id.includes('&lang.css') || id.match(INLINE_STYLE_ID_RE) + return isCssFile } @@ -173,6 +351,7 @@ class Root { private id: string, private base: string, + private enableSourceMaps: boolean, private customCssResolver: (id: string, base: string) => Promise, private customJsResolver: (id: string, base: string) => Promise, ) {} @@ -183,11 +362,17 @@ class Root { content: string, _addWatchFile: (file: string) => void, I: Instrumentation, - ): Promise { + ): Promise< + | { + code: string + map: string | undefined + } + | false + > { let inputPath = idToPath(this.id) function addWatchFile(file: string) { - // Don't watch the input file since it's already a dependency anc causes + // Don't watch the input file since it's already a dependency and causes // issues with some setups (e.g. Qwik). if (file === inputPath) { return @@ -197,7 +382,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) @@ -215,6 +400,7 @@ class Root { DEBUG && I.start('Setup compiler') let addBuildDependenciesPromises: Promise[] = [] this.compiler = await compile(content, { + from: this.enableSourceMaps ? this.id : undefined, base: inputBase, shouldRewriteUrls: true, onDependency: (path) => { @@ -256,7 +442,11 @@ class Root { if ( !( this.compiler.features & - (Features.AtApply | Features.JsPluginCompat | Features.ThemeFunction | Features.Utilities) + (Features.AtApply | + Features.JsPluginCompat | + Features.ThemeFunction | + Features.Utilities | + Features.Variants) ) ) { return false @@ -274,6 +464,7 @@ 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) @@ -310,13 +501,21 @@ class Root { } } } + DEBUG && I.end('Register dependency messages') } DEBUG && I.start('Build CSS') - let result = this.compiler.build([...this.candidates]) + let code = this.compiler.build([...this.candidates]) DEBUG && I.end('Build CSS') - return result + DEBUG && I.start('Build Source Map') + let map = this.enableSourceMaps ? toSourceMap(this.compiler.buildSourceMap()).raw : undefined + DEBUG && I.end('Build Source Map') + + return { + code, + map, + } } private async addBuildDependency(path: string) { diff --git a/packages/@tailwindcss-webpack/README.md b/packages/@tailwindcss-webpack/README.md new file mode 100644 index 000000000..56484ed7f --- /dev/null +++ b/packages/@tailwindcss-webpack/README.md @@ -0,0 +1,102 @@ +

+ + + + + 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 new file mode 100644 index 000000000..dce09429d --- /dev/null +++ b/packages/@tailwindcss-webpack/package.json @@ -0,0 +1,55 @@ +{ + "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 new file mode 100644 index 000000000..dbdded748 --- /dev/null +++ b/packages/@tailwindcss-webpack/src/index.cts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000..6d02f1f0b --- /dev/null +++ b/packages/@tailwindcss-webpack/src/index.ts @@ -0,0 +1,287 @@ +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 new file mode 100644 index 000000000..d2131cbeb --- /dev/null +++ b/packages/@tailwindcss-webpack/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "verbatimModuleSyntax": false, + }, +} diff --git a/packages/@tailwindcss-webpack/tsup.config.ts b/packages/@tailwindcss-webpack/tsup.config.ts new file mode 100644 index 000000000..998eebe4f --- /dev/null +++ b/packages/@tailwindcss-webpack/tsup.config.ts @@ -0,0 +1,19 @@ +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 95ec9d87d..5f532607d 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

--- @@ -27,14 +27,10 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ## Community -For help, discussion about best practices, or any other conversation that would benefit from being searchable: +For help, discussion about best practices, or feature ideas: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) - -For chatting with others using the framework: - -[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe) +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index 7d89e7839..d1288d6fa 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -1,6 +1,6 @@ { "name": "tailwindcss", - "version": "4.1.3", + "version": "4.3.3", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -127,9 +127,12 @@ "utilities.css" ], "devDependencies": { + "@jridgewell/remapping": "^2.3.5", "@tailwindcss/oxide": "workspace:^", "@types/node": "catalog:", + "dedent": "catalog:", "lightningcss": "catalog:", - "dedent": "1.5.3" + "magic-string": "^1.1.0", + "source-map-js": "^1.2.1" } } diff --git a/packages/tailwindcss/playwright.config.ts b/packages/tailwindcss/playwright.config.ts index c0a123abf..ba8f95d70 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', + reporter: [['html', { open: 'never' }]], /* 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,10 +36,11 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, + // 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: 'firefox', use: { diff --git a/packages/tailwindcss/preflight.css b/packages/tailwindcss/preflight.css index 0976a40d4..04e87905c 100644 --- a/packages/tailwindcss/preflight.css +++ b/packages/tailwindcss/preflight.css @@ -32,8 +32,13 @@ html, tab-size: 4; /* 3 */ font-family: --theme( --default-font-family, - ui-sans-serif, - system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + 'Noto Sans', + Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', @@ -170,7 +175,7 @@ table { Use the modern Firefox focus style for all focusable elements. */ -:-moz-focusring { +:-moz-focusring:where(:not(iframe)) { outline: auto; } @@ -349,6 +354,14 @@ textarea { padding-block: 0; } +/* + Center dropdown marker shown on inputs with paired ``s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499) +*/ + +::-webkit-calendar-picker-indicator { + line-height: 1; +} + /* Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) */ diff --git a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap index e8159b0f6..9921709d7 100644 --- a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap @@ -1,7 +1,8 @@ // 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; @@ -123,14 +124,18 @@ 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: 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-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-default-font-family: var(--tw-font-sans); --tw-default-mono-font-family: var(--tw-font-mono); } @@ -155,7 +160,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, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + 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-feature-settings: var(--tw-default-font-feature-settings, normal); font-variation-settings: var(--tw-default-font-variation-settings, normal); -webkit-tap-highlight-color: transparent; @@ -221,7 +226,7 @@ exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = ` border-collapse: collapse; } - :-moz-focusring { + :-moz-focusring:where(:not(iframe)) { outline: auto; } @@ -354,6 +359,10 @@ exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = ` padding-block: 0; } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + :-moz-ui-invalid { box-shadow: none; } @@ -379,5 +388,6 @@ 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 511b880e7..d595e8c01 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -306,66 +306,6 @@ 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", @@ -463,8 +403,256 @@ 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", @@ -739,6 +927,76 @@ 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", @@ -1239,6 +1497,76 @@ 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", @@ -1572,66 +1900,6 @@ 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", @@ -1954,6 +2222,7 @@ exports[`getClassList 1`] = ` "accent-current/90", "accent-current/95", "accent-current/100", + "accent-current/background", "accent-inherit", "accent-inherit/0", "accent-inherit/5", @@ -1976,6 +2245,7 @@ exports[`getClassList 1`] = ` "accent-inherit/90", "accent-inherit/95", "accent-inherit/100", + "accent-inherit/background", "accent-transparent", "accent-transparent/0", "accent-transparent/5", @@ -1998,6 +2268,7 @@ exports[`getClassList 1`] = ` "accent-transparent/90", "accent-transparent/95", "accent-transparent/100", + "accent-transparent/background", "align-baseline", "align-bottom", "align-middle", @@ -2322,6 +2593,7 @@ exports[`getClassList 1`] = ` "bg-current/90", "bg-current/95", "bg-current/100", + "bg-current/background", "bg-fixed", "bg-inherit", "bg-inherit/0", @@ -2345,6 +2617,7 @@ exports[`getClassList 1`] = ` "bg-inherit/90", "bg-inherit/95", "bg-inherit/100", + "bg-inherit/background", "bg-left", "bg-linear-0", "bg-linear-0/oklab", @@ -2573,7 +2846,79 @@ 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", @@ -2597,6 +2942,7 @@ exports[`getClassList 1`] = ` "border/90", "border/95", "border/100", + "border/background", "border-0", "border-2", "border-4", @@ -2623,6 +2969,7 @@ exports[`getClassList 1`] = ` "border-b/90", "border-b/95", "border-b/100", + "border-b/background", "border-b-0", "border-b-2", "border-b-4", @@ -2649,6 +2996,7 @@ 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", @@ -2671,6 +3019,7 @@ 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", @@ -2693,6 +3042,199 @@ 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", @@ -2716,6 +3258,7 @@ exports[`getClassList 1`] = ` "border-current/90", "border-current/95", "border-current/100", + "border-current/background", "border-dashed", "border-dotted", "border-double", @@ -2741,6 +3284,7 @@ exports[`getClassList 1`] = ` "border-e/90", "border-e/95", "border-e/100", + "border-e/background", "border-e-0", "border-e-2", "border-e-4", @@ -2767,6 +3311,7 @@ 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", @@ -2789,6 +3334,7 @@ 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", @@ -2811,6 +3357,7 @@ 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", @@ -2834,6 +3381,7 @@ exports[`getClassList 1`] = ` "border-inherit/90", "border-inherit/95", "border-inherit/100", + "border-inherit/background", "border-l", "border-l/0", "border-l/5", @@ -2856,6 +3404,7 @@ exports[`getClassList 1`] = ` "border-l/90", "border-l/95", "border-l/100", + "border-l/background", "border-l-0", "border-l-2", "border-l-4", @@ -2882,6 +3431,7 @@ 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", @@ -2904,6 +3454,7 @@ 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", @@ -2926,6 +3477,7 @@ 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", @@ -2949,6 +3501,7 @@ exports[`getClassList 1`] = ` "border-r/90", "border-r/95", "border-r/100", + "border-r/background", "border-r-0", "border-r-2", "border-r-4", @@ -2975,6 +3528,7 @@ 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", @@ -2997,6 +3551,7 @@ 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", @@ -3019,6 +3574,7 @@ 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", @@ -3041,6 +3597,7 @@ exports[`getClassList 1`] = ` "border-s/90", "border-s/95", "border-s/100", + "border-s/background", "border-s-0", "border-s-2", "border-s-4", @@ -3067,6 +3624,7 @@ 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", @@ -3089,6 +3647,7 @@ 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", @@ -3111,6 +3670,7 @@ 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", @@ -3240,6 +3800,7 @@ exports[`getClassList 1`] = ` "border-t/90", "border-t/95", "border-t/100", + "border-t/background", "border-t-0", "border-t-2", "border-t-4", @@ -3266,6 +3827,7 @@ 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", @@ -3288,6 +3850,7 @@ 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", @@ -3310,6 +3873,7 @@ 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", @@ -3332,6 +3896,7 @@ exports[`getClassList 1`] = ` "border-transparent/90", "border-transparent/95", "border-transparent/100", + "border-transparent/background", "border-x", "border-x/0", "border-x/5", @@ -3354,6 +3919,7 @@ exports[`getClassList 1`] = ` "border-x/90", "border-x/95", "border-x/100", + "border-x/background", "border-x-0", "border-x-2", "border-x-4", @@ -3380,6 +3946,7 @@ 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", @@ -3402,6 +3969,7 @@ 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", @@ -3424,6 +3992,7 @@ 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", @@ -3446,6 +4015,7 @@ exports[`getClassList 1`] = ` "border-y/90", "border-y/95", "border-y/100", + "border-y/background", "border-y-0", "border-y-2", "border-y-4", @@ -3472,6 +4042,7 @@ 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", @@ -3494,6 +4065,7 @@ 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", @@ -3516,6 +4088,7 @@ 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", @@ -3606,7 +4179,6 @@ exports[`getClassList 1`] = ` "break-inside-avoid-page", "break-keep", "break-normal", - "break-words", "brightness-0", "brightness-50", "brightness-75", @@ -3643,6 +4215,7 @@ exports[`getClassList 1`] = ` "caret-current/90", "caret-current/95", "caret-current/100", + "caret-current/background", "caret-inherit", "caret-inherit/0", "caret-inherit/5", @@ -3665,6 +4238,7 @@ exports[`getClassList 1`] = ` "caret-inherit/90", "caret-inherit/95", "caret-inherit/100", + "caret-inherit/background", "caret-transparent", "caret-transparent/0", "caret-transparent/5", @@ -3687,6 +4261,7 @@ exports[`getClassList 1`] = ` "caret-transparent/90", "caret-transparent/95", "caret-transparent/100", + "caret-transparent/background", "clear-both", "clear-end", "clear-left", @@ -3842,6 +4417,7 @@ exports[`getClassList 1`] = ` "decoration-current/90", "decoration-current/95", "decoration-current/100", + "decoration-current/background", "decoration-dashed", "decoration-dotted", "decoration-double", @@ -3868,6 +4444,7 @@ exports[`getClassList 1`] = ` "decoration-inherit/90", "decoration-inherit/95", "decoration-inherit/100", + "decoration-inherit/background", "decoration-solid", "decoration-transparent", "decoration-transparent/0", @@ -3891,6 +4468,7 @@ exports[`getClassList 1`] = ` "decoration-transparent/90", "decoration-transparent/95", "decoration-transparent/100", + "decoration-transparent/background", "decoration-wavy", "delay-75", "delay-100", @@ -3923,6 +4501,7 @@ exports[`getClassList 1`] = ` "divide-current/90", "divide-current/95", "divide-current/100", + "divide-current/background", "divide-dashed", "divide-dotted", "divide-double", @@ -3948,6 +4527,7 @@ exports[`getClassList 1`] = ` "divide-inherit/90", "divide-inherit/95", "divide-inherit/100", + "divide-inherit/background", "divide-none", "divide-solid", "divide-transparent", @@ -3972,6 +4552,7 @@ exports[`getClassList 1`] = ` "divide-transparent/90", "divide-transparent/95", "divide-transparent/100", + "divide-transparent/background", "divide-x", "divide-x-0", "divide-x-2", @@ -4006,6 +4587,7 @@ 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", @@ -4028,6 +4610,7 @@ 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", @@ -4052,6 +4635,7 @@ 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", @@ -4063,66 +4647,6 @@ 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", @@ -4150,6 +4674,7 @@ exports[`getClassList 1`] = ` "fill-current/90", "fill-current/95", "fill-current/100", + "fill-current/background", "fill-inherit", "fill-inherit/0", "fill-inherit/5", @@ -4172,6 +4697,7 @@ exports[`getClassList 1`] = ` "fill-inherit/90", "fill-inherit/95", "fill-inherit/100", + "fill-inherit/background", "fill-none", "fill-transparent", "fill-transparent/0", @@ -4195,8 +4721,21 @@ exports[`getClassList 1`] = ` "fill-transparent/90", "fill-transparent/95", "fill-transparent/100", + "fill-transparent/background", "fixed", "flex", + "flex-1", + "flex-2", + "flex-3", + "flex-4", + "flex-5", + "flex-6", + "flex-7", + "flex-8", + "flex-9", + "flex-10", + "flex-11", + "flex-12", "flex-1/2", "flex-1/3", "flex-1/4", @@ -4306,6 +4845,7 @@ exports[`getClassList 1`] = ` "from-current/90", "from-current/95", "from-current/100", + "from-current/background", "from-inherit", "from-inherit/0", "from-inherit/5", @@ -4328,6 +4868,7 @@ exports[`getClassList 1`] = ` "from-inherit/90", "from-inherit/95", "from-inherit/100", + "from-inherit/background", "from-transparent", "from-transparent/0", "from-transparent/5", @@ -4350,6 +4891,7 @@ exports[`getClassList 1`] = ` "from-transparent/90", "from-transparent/95", "from-transparent/100", + "from-transparent/background", "gap-0", "gap-0.5", "gap-1", @@ -4562,6 +5104,7 @@ exports[`getClassList 1`] = ` "h-dvw", "h-fit", "h-full", + "h-lh", "h-lvh", "h-lvw", "h-max", @@ -4616,9 +5159,81 @@ 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", @@ -4681,6 +5296,195 @@ 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", @@ -4711,6 +5515,7 @@ 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", @@ -4733,6 +5538,7 @@ 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", @@ -4755,28 +5561,70 @@ exports[`getClassList 1`] = ` "inset-ring-transparent/90", "inset-ring-transparent/95", "inset-ring-transparent/100", - "inset-shadow", - "inset-shadow/0", - "inset-shadow/5", - "inset-shadow/10", - "inset-shadow/15", - "inset-shadow/20", - "inset-shadow/25", - "inset-shadow/30", - "inset-shadow/35", - "inset-shadow/40", - "inset-shadow/45", - "inset-shadow/50", - "inset-shadow/55", - "inset-shadow/60", - "inset-shadow/65", - "inset-shadow/70", - "inset-shadow/75", - "inset-shadow/80", - "inset-shadow/85", - "inset-shadow/90", - "inset-shadow/95", - "inset-shadow/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", @@ -4799,6 +5647,7 @@ 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", @@ -4821,6 +5670,7 @@ 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", @@ -4867,6 +5717,7 @@ 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", @@ -5268,6 +6119,7 @@ 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", @@ -5290,6 +6142,7 @@ 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", @@ -5312,6 +6165,7 @@ 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", @@ -5389,6 +6243,7 @@ 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", @@ -5411,6 +6266,7 @@ 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", @@ -5433,6 +6289,7 @@ 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", @@ -5530,6 +6387,7 @@ 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", @@ -5552,6 +6410,7 @@ 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", @@ -5574,6 +6433,7 @@ 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", @@ -5651,6 +6511,7 @@ 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", @@ -5673,6 +6534,7 @@ 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", @@ -5695,6 +6557,7 @@ 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", @@ -5777,6 +6640,7 @@ 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", @@ -5799,6 +6663,7 @@ 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", @@ -5821,6 +6686,7 @@ 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", @@ -5898,6 +6764,7 @@ 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", @@ -5920,6 +6787,7 @@ 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", @@ -5942,6 +6810,7 @@ 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", @@ -6029,6 +6898,7 @@ 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", @@ -6051,6 +6921,7 @@ 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", @@ -6073,6 +6944,7 @@ 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", @@ -6150,6 +7022,7 @@ 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", @@ -6172,6 +7045,7 @@ 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", @@ -6194,6 +7068,7 @@ 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", @@ -6282,6 +7157,7 @@ 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", @@ -6304,6 +7180,7 @@ 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", @@ -6326,6 +7203,7 @@ 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", @@ -6403,6 +7281,7 @@ 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", @@ -6425,6 +7304,7 @@ 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", @@ -6447,6 +7327,7 @@ 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", @@ -6537,6 +7418,7 @@ 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", @@ -6559,6 +7441,7 @@ 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", @@ -6581,6 +7464,7 @@ 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", @@ -6658,6 +7542,7 @@ 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", @@ -6680,6 +7565,7 @@ 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", @@ -6702,6 +7588,7 @@ 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", @@ -6786,6 +7673,7 @@ 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", @@ -6808,6 +7696,7 @@ 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", @@ -6830,6 +7719,7 @@ 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", @@ -6907,6 +7797,7 @@ 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", @@ -6929,6 +7820,7 @@ 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", @@ -6951,6 +7843,7 @@ 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", @@ -7033,6 +7926,7 @@ 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", @@ -7055,6 +7949,7 @@ 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", @@ -7077,6 +7972,7 @@ 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", @@ -7154,6 +8050,7 @@ 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", @@ -7176,6 +8073,7 @@ 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", @@ -7198,6 +8096,7 @@ 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", @@ -7275,6 +8174,7 @@ 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", @@ -7297,6 +8197,7 @@ 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", @@ -7319,6 +8220,7 @@ 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", @@ -7396,6 +8298,7 @@ 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", @@ -7418,6 +8321,7 @@ 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", @@ -7440,6 +8344,78 @@ 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", @@ -7504,6 +8480,7 @@ exports[`getClassList 1`] = ` "max-h-dvw", "max-h-fit", "max-h-full", + "max-h-lh", "max-h-lvh", "max-h-lvw", "max-h-max", @@ -7513,6 +8490,78 @@ 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", @@ -7585,7 +8634,6 @@ exports[`getClassList 1`] = ` "max-w-min", "max-w-none", "max-w-px", - "max-w-screen", "max-w-svh", "max-w-svw", "mb-0", @@ -7624,6 +8672,78 @@ 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", @@ -7660,6 +8780,77 @@ 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", @@ -7725,6 +8916,7 @@ exports[`getClassList 1`] = ` "min-h-dvw", "min-h-fit", "min-h-full", + "min-h-lh", "min-h-lvh", "min-h-lvw", "min-h-max", @@ -7733,6 +8925,78 @@ 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", @@ -8098,7 +9362,6 @@ exports[`getClassList 1`] = ` "order-12", "order-first", "order-last", - "order-none", "ordinal", "origin-bottom", "origin-bottom-left", @@ -8131,6 +9394,7 @@ exports[`getClassList 1`] = ` "outline/90", "outline/95", "outline/100", + "outline/background", "outline-0", "outline-1", "outline-2", @@ -8158,6 +9422,7 @@ exports[`getClassList 1`] = ` "outline-current/90", "outline-current/95", "outline-current/100", + "outline-current/background", "outline-dashed", "outline-dotted", "outline-double", @@ -8184,6 +9449,7 @@ exports[`getClassList 1`] = ` "outline-inherit/90", "outline-inherit/95", "outline-inherit/100", + "outline-inherit/background", "outline-none", "outline-offset-0", "outline-offset-1", @@ -8213,6 +9479,7 @@ exports[`getClassList 1`] = ` "outline-transparent/90", "outline-transparent/95", "outline-transparent/100", + "outline-transparent/background", "overflow-auto", "overflow-clip", "overflow-hidden", @@ -8308,6 +9575,76 @@ 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", @@ -8436,6 +9773,7 @@ exports[`getClassList 1`] = ` "placeholder-current/90", "placeholder-current/95", "placeholder-current/100", + "placeholder-current/background", "placeholder-inherit", "placeholder-inherit/0", "placeholder-inherit/5", @@ -8458,6 +9796,7 @@ exports[`getClassList 1`] = ` "placeholder-inherit/90", "placeholder-inherit/95", "placeholder-inherit/100", + "placeholder-inherit/background", "placeholder-transparent", "placeholder-transparent/0", "placeholder-transparent/5", @@ -8480,6 +9819,7 @@ exports[`getClassList 1`] = ` "placeholder-transparent/90", "placeholder-transparent/95", "placeholder-transparent/100", + "placeholder-transparent/background", "pointer-events-auto", "pointer-events-none", "pr-0", @@ -8754,6 +10094,7 @@ exports[`getClassList 1`] = ` "ring-current/90", "ring-current/95", "ring-current/100", + "ring-current/background", "ring-inherit", "ring-inherit/0", "ring-inherit/5", @@ -8776,6 +10117,7 @@ exports[`getClassList 1`] = ` "ring-inherit/90", "ring-inherit/95", "ring-inherit/100", + "ring-inherit/background", "ring-inset", "ring-offset-0", "ring-offset-1", @@ -8804,6 +10146,7 @@ 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", @@ -8826,6 +10169,7 @@ 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", @@ -8848,6 +10192,7 @@ 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", @@ -8870,6 +10215,7 @@ exports[`getClassList 1`] = ` "ring-transparent/90", "ring-transparent/95", "ring-transparent/100", + "ring-transparent/background", "rotate-0", "rotate-1", "rotate-2", @@ -9107,6 +10453,76 @@ 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", @@ -9422,6 +10838,76 @@ 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", @@ -9668,6 +11154,150 @@ 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", @@ -9685,28 +11315,6 @@ exports[`getClassList 1`] = ` "sepia-0", "sepia-50", "sepia-100", - "shadow", - "shadow/0", - "shadow/5", - "shadow/10", - "shadow/15", - "shadow/20", - "shadow/25", - "shadow/30", - "shadow/35", - "shadow/40", - "shadow/45", - "shadow/50", - "shadow/55", - "shadow/60", - "shadow/65", - "shadow/70", - "shadow/75", - "shadow/80", - "shadow/85", - "shadow/90", - "shadow/95", - "shadow/100", "shadow-current", "shadow-current/0", "shadow-current/5", @@ -9729,6 +11337,7 @@ exports[`getClassList 1`] = ` "shadow-current/90", "shadow-current/95", "shadow-current/100", + "shadow-current/background", "shadow-inherit", "shadow-inherit/0", "shadow-inherit/5", @@ -9751,6 +11360,7 @@ exports[`getClassList 1`] = ` "shadow-inherit/90", "shadow-inherit/95", "shadow-inherit/100", + "shadow-inherit/background", "shadow-initial", "shadow-none", "shadow-transparent", @@ -9775,6 +11385,7 @@ exports[`getClassList 1`] = ` "shadow-transparent/90", "shadow-transparent/95", "shadow-transparent/100", + "shadow-transparent/background", "shrink", "shrink-0", "size-0", @@ -9954,66 +11565,6 @@ 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", @@ -10045,6 +11596,7 @@ exports[`getClassList 1`] = ` "stroke-current/90", "stroke-current/95", "stroke-current/100", + "stroke-current/background", "stroke-inherit", "stroke-inherit/0", "stroke-inherit/5", @@ -10067,6 +11619,7 @@ exports[`getClassList 1`] = ` "stroke-inherit/90", "stroke-inherit/95", "stroke-inherit/100", + "stroke-inherit/background", "stroke-none", "stroke-transparent", "stroke-transparent/0", @@ -10090,7 +11643,11 @@ 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", @@ -10128,6 +11685,7 @@ exports[`getClassList 1`] = ` "text-current/90", "text-current/95", "text-current/100", + "text-current/background", "text-ellipsis", "text-end", "text-inherit", @@ -10152,33 +11710,12 @@ exports[`getClassList 1`] = ` "text-inherit/90", "text-inherit/95", "text-inherit/100", + "text-inherit/background", "text-justify", "text-left", "text-nowrap", "text-pretty", "text-right", - "text-shadow", - "text-shadow/0", - "text-shadow/5", - "text-shadow/10", - "text-shadow/15", - "text-shadow/20", - "text-shadow/25", - "text-shadow/30", - "text-shadow/35", - "text-shadow/40", - "text-shadow/45", - "text-shadow/50", - "text-shadow/55", - "text-shadow/60", - "text-shadow/65", - "text-shadow/70", - "text-shadow/75", - "text-shadow/80", - "text-shadow/85", - "text-shadow/90", - "text-shadow/95", - "text-shadow/100", "text-shadow-current", "text-shadow-current/0", "text-shadow-current/5", @@ -10201,6 +11738,7 @@ 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", @@ -10223,6 +11761,7 @@ 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", @@ -10247,6 +11786,7 @@ 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", @@ -10270,6 +11810,7 @@ exports[`getClassList 1`] = ` "text-transparent/90", "text-transparent/95", "text-transparent/100", + "text-transparent/background", "text-wrap", "text-xs", "to-0%", @@ -10315,6 +11856,7 @@ exports[`getClassList 1`] = ` "to-current/90", "to-current/95", "to-current/100", + "to-current/background", "to-inherit", "to-inherit/0", "to-inherit/5", @@ -10337,6 +11879,7 @@ exports[`getClassList 1`] = ` "to-inherit/90", "to-inherit/95", "to-inherit/100", + "to-inherit/background", "to-transparent", "to-transparent/0", "to-transparent/5", @@ -10359,6 +11902,7 @@ exports[`getClassList 1`] = ` "to-transparent/90", "to-transparent/95", "to-transparent/100", + "to-transparent/background", "top-0", "top-0.5", "top-1", @@ -10727,6 +12271,7 @@ exports[`getClassList 1`] = ` "via-current/90", "via-current/95", "via-current/100", + "via-current/background", "via-inherit", "via-inherit/0", "via-inherit/5", @@ -10749,6 +12294,7 @@ exports[`getClassList 1`] = ` "via-inherit/90", "via-inherit/95", "via-inherit/100", + "via-inherit/background", "via-none", "via-transparent", "via-transparent/0", @@ -10772,6 +12318,7 @@ exports[`getClassList 1`] = ` "via-transparent/90", "via-transparent/95", "via-transparent/100", + "via-transparent/background", "visible", "w-0", "w-0.5", @@ -10868,6 +12415,16 @@ 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 3f5fd56b7..43e8fdf6f 100644 --- a/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap @@ -1,7 +1,8 @@ // 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; @@ -11,6 +12,7 @@ exports[`border-* 1`] = ` :root, :host { --color-red-500: #ef4444; + --border-color-blue-500: #3b82f6; } .border { @@ -111,6 +113,10 @@ exports[`border-* 1`] = ` } } +.border-blue-500 { + border-color: var(--border-color-blue-500); +} + .border-current, .border-current\\/50 { border-color: currentColor; } @@ -177,11 +183,13 @@ 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; @@ -191,6 +199,7 @@ exports[`border-b-* 1`] = ` :root, :host { --color-red-500: #ef4444; + --border-color-blue-500: #3b82f6; } .border-b { @@ -291,6 +300,10 @@ exports[`border-b-* 1`] = ` } } +.border-b-blue-500 { + border-bottom-color: var(--border-color-blue-500); +} + .border-b-current, .border-b-current\\/50 { border-bottom-color: currentColor; } @@ -357,11 +370,13 @@ exports[`border-b-* 1`] = ` syntax: "*"; inherits: false; initial-value: solid; -}" +} +" `; -exports[`border-e-* 1`] = ` -"@layer properties { +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; @@ -371,6 +386,381 @@ exports[`border-e-* 1`] = ` :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 { + @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-e { @@ -471,6 +861,10 @@ exports[`border-e-* 1`] = ` } } +.border-e-blue-500 { + border-inline-end-color: var(--border-color-blue-500); +} + .border-e-current, .border-e-current\\/50 { border-inline-end-color: currentColor; } @@ -537,11 +931,13 @@ 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; @@ -551,6 +947,7 @@ exports[`border-l-* 1`] = ` :root, :host { --color-red-500: #ef4444; + --border-color-blue-500: #3b82f6; } .border-l { @@ -651,6 +1048,10 @@ exports[`border-l-* 1`] = ` } } +.border-l-blue-500 { + border-left-color: var(--border-color-blue-500); +} + .border-l-current, .border-l-current\\/50 { border-left-color: currentColor; } @@ -717,11 +1118,13 @@ 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; @@ -731,6 +1134,7 @@ exports[`border-r-* 1`] = ` :root, :host { --color-red-500: #ef4444; + --border-color-blue-500: #3b82f6; } .border-r { @@ -831,6 +1235,10 @@ exports[`border-r-* 1`] = ` } } +.border-r-blue-500 { + border-right-color: var(--border-color-blue-500); +} + .border-r-current, .border-r-current\\/50 { border-right-color: currentColor; } @@ -897,11 +1305,13 @@ 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; @@ -911,6 +1321,7 @@ exports[`border-s-* 1`] = ` :root, :host { --color-red-500: #ef4444; + --border-color-blue-500: #3b82f6; } .border-s { @@ -1011,6 +1422,10 @@ exports[`border-s-* 1`] = ` } } +.border-s-blue-500 { + border-inline-start-color: var(--border-color-blue-500); +} + .border-s-current, .border-s-current\\/50 { border-inline-start-color: currentColor; } @@ -1077,11 +1492,13 @@ 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; @@ -1091,6 +1508,7 @@ exports[`border-t-* 1`] = ` :root, :host { --color-red-500: #ef4444; + --border-color-blue-500: #3b82f6; } .border-t { @@ -1191,6 +1609,10 @@ exports[`border-t-* 1`] = ` } } +.border-t-blue-500 { + border-top-color: var(--border-color-blue-500); +} + .border-t-current, .border-t-current\\/50 { border-top-color: currentColor; } @@ -1257,11 +1679,13 @@ 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; @@ -1271,6 +1695,7 @@ exports[`border-x-* 1`] = ` :root, :host { --color-red-500: #ef4444; + --border-color-blue-500: #3b82f6; } .border-x { @@ -1371,6 +1796,10 @@ exports[`border-x-* 1`] = ` } } +.border-x-blue-500 { + border-inline-color: var(--border-color-blue-500); +} + .border-x-current, .border-x-current\\/50 { border-inline-color: currentColor; } @@ -1437,11 +1866,13 @@ 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; @@ -1451,6 +1882,7 @@ exports[`border-y-* 1`] = ` :root, :host { --color-red-500: #ef4444; + --border-color-blue-500: #3b82f6; } .border-y { @@ -1551,6 +1983,10 @@ exports[`border-y-* 1`] = ` } } +.border-y-blue-500 { + border-block-color: var(--border-color-blue-500); +} + .border-y-current, .border-y-current\\/50 { border-block-color: currentColor; } @@ -1617,5 +2053,6 @@ 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 676b4f2ee..2d5e5b847 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -1,8 +1,11 @@ import { Features } from '.' -import { rule, toCss, walk, WalkAction, type AstNode } from './ast' +import { cloneAstNode, rule, toCss, type AstNode } from './ast' import { compileCandidates } from './compile' import type { DesignSystem } from './design-system' +import type { SourceLocation } from './source-maps/source' import { DefaultMap } from './utils/default-map' +import { segment } from './utils/segment' +import { walk, WalkAction } from './walk' export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { let features = Features.None @@ -23,7 +26,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { let definitions = new DefaultMap(() => new Set()) // Collect all new `@utility` definitions and all `@apply` rules first - walk([root], (node, { parent }) => { + walk([root], (node, ctx) => { if (node.kind !== 'at-rule') return // Do not allow `@apply` rules inside `@keyframes` rules. @@ -59,14 +62,18 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { if (node.name === '@apply') { // `@apply` cannot be top-level, so we need to have a parent such that we // can replace the `@apply` node with the actual utility classes later. - if (parent === null) return + if (ctx.parent === null) return features |= Features.AtApply - parents.add(parent) + parents.add(ctx.parent) for (let dependency of resolveApplyDependencies(node, designSystem)) { - dependencies.get(parent).add(dependency) + // Mark every parent in the path as having a dependency to that utility. + for (let parent of ctx.path()) { + if (!parents.has(parent)) continue + dependencies.get(parent).add(dependency) + } } } }) @@ -151,21 +158,163 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { for (let parent of sorted) { if (!('nodes' in parent)) continue - for (let i = 0; i < parent.nodes.length; i++) { - let node = parent.nodes[i] - if (node.kind !== 'at-rule' || node.name !== '@apply') continue + walk(parent.nodes, (child) => { + if (child.kind !== 'at-rule' || child.name !== '@apply') return - let candidates = node.params.split(/\s+/g) + 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 + } + + 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 // with. - let candidateAst = compileCandidates(candidates, designSystem, { + let candidates = Object.keys(candidateOffsets) + let compiled = compileCandidates(candidates, designSystem, { + respectImportant: false, onInvalidCandidate: (candidate) => { - throw new Error(`Cannot apply unknown utility class: ${candidate}`) + // When using prefix, make sure prefix is used in candidate + if (designSystem.theme.prefix && !candidate.startsWith(designSystem.theme.prefix)) { + throw new Error( + `Cannot apply unprefixed utility class \`${candidate}\`. Did you mean \`${designSystem.theme.prefix}:${candidate}\`?`, + ) + } + + // When the utility is blocklisted, let the user know + // + // Note: `@apply` is processed before handling incoming classes from + // template files. This means that the `invalidCandidates` set will + // only contain explicit classes via: + // + // - `blocklist` from a JS config + // - `@source not inline(…)` + if (designSystem.invalidCandidates.has(candidate)) { + throw new Error( + `Cannot apply utility class \`${candidate}\` because it has been explicitly disabled: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes`, + ) + } + + // Verify if variants exist + let parts = segment(candidate, ':') + if (parts.length > 1) { + let utility = parts.pop()! + + // Ensure utility on its own compiles, if not, we will fallback to + // the next error + if (designSystem.candidatesToCss([utility])[0]) { + let compiledVariants = designSystem.candidatesToCss( + parts.map((variant) => `${variant}:[--tw-variant-check:1]`), + ) + let unknownVariants = parts.filter((_, idx) => compiledVariants[idx] === null) + if (unknownVariants.length > 0) { + if (unknownVariants.length === 1) { + throw new Error( + `Cannot apply utility class \`${candidate}\` because the ${unknownVariants.map((variant) => `\`${variant}\``)} variant does not exist.`, + ) + } else { + let formatter = new Intl.ListFormat('en', { + style: 'long', + type: 'conjunction', + }) + throw new Error( + `Cannot apply utility class \`${candidate}\` because the ${formatter.format(unknownVariants.map((variant) => `\`${variant}\``))} variants do not exist.`, + ) + } + } + } + } + + // When the theme is empty, it means that no theme was loaded and + // `@import "tailwindcss"`, `@reference "app.css"` or similar is + // very likely missing. + if (designSystem.theme.size === 0) { + throw new Error( + `Cannot apply unknown utility class \`${candidate}\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive`, + ) + } + + // Fallback to most generic error message + throw new Error(`Cannot apply unknown utility class \`${candidate}\``) }, - }).astNodes + }) + + let src = child.src + + let candidateAst = compiled.astNodes.map((node) => { + let candidate = compiled.nodeSorting.get(node)?.candidate + let candidateOffset = candidate ? candidateOffsets[candidate] : undefined + + node = cloneAstNode(node) + + if (!src || !candidate || candidateOffset === undefined) { + // While the original nodes may have come from an `@utility` we still + // want to replace the source because the `@apply` is ultimately the + // reason the node was emitted into the AST. + walk([node], (node) => { + node.src = src + }) + + return node + } + + let candidateSrc: SourceLocation = [src[0], src[1], src[2]] + + candidateSrc[1] += 7 /* '@apply '.length */ + candidateOffset + candidateSrc[2] = candidateSrc[1] + candidate.length + + // While the original nodes may have come from an `@utility` we still + // want to replace the source because the `@apply` is ultimately the + // reason the node was emitted into the AST. + walk([node], (node) => { + node.src = candidateSrc + }) + + return node + }) // Collect the nodes to insert in place of the `@apply` rule. When a rule // was used, we want to insert its children instead of the rule because we @@ -181,10 +330,11 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { } } - parent.nodes.splice(i, 1, ...newNodes) + return WalkAction.Replace(newNodes) } - } + }) } + return features } diff --git a/packages/tailwindcss/src/ast.bench.ts b/packages/tailwindcss/src/ast.bench.ts new file mode 100644 index 000000000..034faee27 --- /dev/null +++ b/packages/tailwindcss/src/ast.bench.ts @@ -0,0 +1,64 @@ +import { bench, describe } from 'vitest' +import { cloneAstNode, toCss, type AstNode } from './ast' +import * as CSS from './css-parser' + +const css = String.raw +const input = css` + @theme { + --color-primary: #333; + } + @tailwind utilities; + .foo { + color: red; + /* comment */ + &:hover { + color: blue; + @apply font-bold; + } + } +` +const ast = CSS.parse(input) + +describe('AST to CSS', () => { + bench('toCss', () => { + toCss(ast) + }) + + bench('toCss with source maps', () => { + toCss(ast, true) + }) +}) + +describe('Cloning AST nodes', () => { + bench('cloneAstNode()', () => { + ast.map(cloneAstNode) + }) + + bench('cloneAstNode (with spread)', () => { + ast.map(cloneAstNodeSpread) + }) + + bench('structuredClone()', () => { + structuredClone(ast) + }) +}) + +function cloneAstNodeSpread(node: T): T { + switch (node.kind) { + case 'rule': + case 'at-rule': + case 'at-root': + return { ...node, nodes: node.nodes.map(cloneAstNodeSpread) } + + case 'context': + return { ...node, context: { ...node.context }, nodes: node.nodes.map(cloneAstNodeSpread) } + + case 'declaration': + case 'comment': + return { ...node } + + default: + node satisfies never + throw new Error(`Unknown node kind: ${(node as any).kind}`) + } +} diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index 9f879d64b..14092c9f5 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -1,26 +1,28 @@ -import { expect, it } from 'vitest' +import { describe, expect, it, test } from 'vitest' import { - atRule, context, + cssContext, decl, + handleNesting, optimizeAst, + rule, styleRule, toCss, - walk, - WalkAction, 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' const css = String.raw const defaultDesignSystem = buildDesignSystem(new Theme()) it('should pretty print an AST', () => { - expect(toCss(optimizeAst(CSS.parse('.foo{color:red;&:hover{color:blue;}}'), defaultDesignSystem))) - .toMatchInlineSnapshot(` - ".foo { + expect(pretty(toCss(CSS.parse('.foo{color:red;&:hover{color:blue;}}')))).toMatchInlineSnapshot(` + " + .foo { color: red; &:hover { color: blue; @@ -31,7 +33,7 @@ it('should pretty print an AST', () => { }) it('allows the placement of context nodes', () => { - const ast = [ + let ast: AstNode[] = [ styleRule('.foo', [decl('color', 'red')]), context({ context: 'a' }, [ styleRule('.bar', [ @@ -48,17 +50,18 @@ it('allows the placement of context nodes', () => { let blueContext let greenContext - walk(ast, (node, { context }) => { + walk(ast, (node, _ctx) => { if (node.kind !== 'declaration') return + let ctx = cssContext(_ctx) switch (node.value) { case 'red': - redContext = context + redContext = ctx.context break case 'blue': - blueContext = context + blueContext = ctx.context break case 'green': - greenContext = context + greenContext = ctx.context break } }) @@ -67,8 +70,9 @@ it('allows the placement of context nodes', () => { expect(blueContext).toEqual({ context: 'a' }) expect(greenContext).toEqual({ context: 'b' }) - expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(` - ".foo { + expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` + " + .foo { color: red; } .bar { @@ -81,36 +85,6 @@ 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 */ @@ -153,13 +127,15 @@ 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(toCss(ast)).toMatchInlineSnapshot(` - ".foo { + expect(pretty(toCss(ast))).toMatchInlineSnapshot(` + " + .foo { } .foo { .bar { @@ -186,14 +162,16 @@ 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(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(` - "@charset "UTF-8"; + expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` + " + @charset "UTF-8"; @layer foo, bar, baz; @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml'; @@ -202,309 +180,1485 @@ it('should not emit empty rules once optimized', () => { `) }) -it('should only visit children once when calling `replaceWith` with single element array', () => { - let visited = new Set() - - let ast = [ - atRule('@media', '', [styleRule('.foo', [decl('color', 'blue')])]), - styleRule('.bar', [decl('color', 'blue')]), - ] - - walk(ast, (node, { replaceWith }) => { - if (visited.has(node)) { - throw new Error('Visited node twice') +it('should not emit exact duplicate declarations in the same rule', () => { + let ast = CSS.parse(css` + .foo { + color: red; + .bar { + color: green; + color: blue; + color: green; + } + color: red; } - visited.add(node) - - if (node.kind === 'at-rule') replaceWith(node.nodes) - }) -}) - -it('should only visit children once when calling `replaceWith` with multi-element array', () => { - let visited = new Set() - - let ast = [ - atRule('@media', '', [ - context({}, [ - styleRule('.foo', [decl('color', 'red')]), - styleRule('.baz', [decl('color', 'blue')]), - ]), - ]), - styleRule('.bar', [decl('color', 'green')]), - ] - - walk(ast, (node, { replaceWith }) => { - let key = id(node) - if (visited.has(key)) { - throw new Error('Visited node twice') + .foo { + color: red; + & { + color: green; + & { + color: red; + color: green; + color: blue; + } + color: red; + } + background: blue; + .bar { + color: green; + color: blue; + color: green; + } + caret-color: orange; } - visited.add(key) + `) - if (node.kind === 'at-rule') replaceWith(node.nodes) - }) - - expect(visited).toMatchInlineSnapshot(` - Set { - "@media ", - ".foo", - "color: red", - ".baz", - "color: blue", - ".bar", - "color: green", + expect(pretty(toCss(ast))).toMatchInlineSnapshot(` + " + .foo { + color: red; + .bar { + color: green; + color: blue; + color: green; + } + color: red; } + .foo { + color: red; + & { + color: green; + & { + color: red; + color: green; + color: blue; + } + color: red; + } + background: blue; + .bar { + color: green; + color: blue; + color: green; + } + caret-color: orange; + } + " + `) + + expect(pretty(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; + } + background: blue; + .bar { + color: green; + color: blue; + color: green; + } + caret-color: orange; + } + " `) }) -it('should never visit children when calling `replaceWith` with `WalkAction.Skip`', () => { - let visited = new Set() - - let inner = styleRule('.foo', [decl('color', 'blue')]) - - let ast = [atRule('@media', '', [inner]), styleRule('.bar', [decl('color', 'blue')])] - - walk(ast, (node, { replaceWith }) => { - visited.add(node) - - if (node.kind === 'at-rule') { - replaceWith(node.nodes) - return WalkAction.Skip +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); + } } - }) + `) - 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", - }, + 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); + } } + " `) }) -it('should skip the correct number of children based on the 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, { replaceWith }) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - replaceWith([]) - return WalkAction.Skip - } +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)) + } + }, }) - - expect(visited).toMatchInlineSnapshot(` - [ - "--index: 0", - "--index: 1", - "--index: 2", - "--index: 3", - "--index: 4", - ] - `) + return pretty(toCss(handleNesting(ast))) } - { - let ast = [ - decl('--index', '0'), - decl('--index', '1'), - decl('--index', '2'), - decl('--index', '3'), - decl('--index', '4'), - ] - let visited: string[] = [] - walk(ast, (node, { replaceWith }) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - replaceWith([]) - return WalkAction.Continue - } + // 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; + } + " + `) }) - 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, { replaceWith }) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - replaceWith([decl('--index', '2.1')]) - return WalkAction.Skip - } + 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; + } + " + `) }) - expect(visited).toMatchInlineSnapshot(` - [ - "--index: 0", - "--index: 1", - "--index: 2", - "--index: 3", - "--index: 4", - ] - `) - } + it('should replace the first rule, that contains `&` with `:scope`', async () => { + expect( + optimize(css` + /* Standalone */ + & { + --x: 1; + } - { - let ast = [ - decl('--index', '0'), - decl('--index', '1'), - decl('--index', '2'), - decl('--index', '3'), - decl('--index', '4'), - ] - let visited: string[] = [] - walk(ast, (node, { replaceWith }) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - replaceWith([decl('--index', '2.1')]) - return WalkAction.Continue - } + /* 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; + } + } + " + `) }) - 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, { replaceWith }) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - replaceWith([decl('--index', '2.1'), decl('--index', '2.2')]) - return WalkAction.Skip - } + 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; + } + " + `) }) - 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, { replaceWith }) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - replaceWith([decl('--index', '2.1'), decl('--index', '2.2')]) - return WalkAction.Continue - } + 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; + } + " + `) }) - expect(visited).toMatchInlineSnapshot(` - [ - "--index: 0", - "--index: 1", - "--index: 2", - "--index: 2.1", - "--index: 2.2", - "--index: 3", - "--index: 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); + } + } + " + `) + }) + }) }) - -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 af76b0772..09c3ea051 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -1,17 +1,25 @@ 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' selector: string nodes: AstNode[] + + src?: SourceLocation + dst?: SourceLocation } export type AtRule = { @@ -19,6 +27,9 @@ export type AtRule = { name: string params: string nodes: AstNode[] + + src?: SourceLocation + dst?: SourceLocation } export type Declaration = { @@ -26,22 +37,34 @@ export type Declaration = { property: string value: string | undefined important: boolean + + src?: SourceLocation + dst?: SourceLocation } export type Comment = { kind: 'comment' value: string + + src?: SourceLocation + dst?: SourceLocation } export type Context = { kind: 'context' context: Record nodes: AstNode[] + + src?: undefined + dst?: undefined } export type AtRoot = { kind: 'at-root' nodes: AstNode[] + + src?: undefined + dst?: undefined } export type Rule = StyleRule | AtRule @@ -103,157 +126,97 @@ export function atRoot(nodes: AstNode[]): AtRoot { } } -export const enum WalkAction { - /** Continue walking, which is the default */ - Continue, +export function cloneAstNode(node: T): T { + switch (node.kind) { + case 'rule': + return { + kind: node.kind, + selector: node.selector, + nodes: node.nodes.map(cloneAstNode), + src: node.src, + dst: node.dst, + } satisfies StyleRule as T - /** Skip visiting the children of this node */ - Skip, + case 'at-rule': + return { + kind: node.kind, + name: node.name, + params: node.params, + nodes: node.nodes.map(cloneAstNode), + src: node.src, + dst: node.dst, + } satisfies AtRule as T - /** Stop the walk entirely */ - Stop, -} + case 'at-root': + return { + kind: node.kind, + nodes: node.nodes.map(cloneAstNode), + src: node.src, + dst: node.dst, + } satisfies AtRoot as T -export function walk( - ast: AstNode[], - visit: ( - node: AstNode, - utils: { - parent: AstNode | null - replaceWith(newNode: AstNode | AstNode[]): void - context: Record - path: AstNode[] - }, - ) => void | WalkAction, - path: AstNode[] = [], - context: Record = {}, -) { - for (let i = 0; i < ast.length; i++) { - let node = ast[i] - let parent = path[path.length - 1] ?? null + case 'context': + return { + kind: node.kind, + context: { ...node.context }, + nodes: node.nodes.map(cloneAstNode), + src: node.src, + dst: node.dst, + } satisfies Context as T - // We want context nodes to be transparent in walks. This means that - // whenever we encounter one, we immediately walk through its children and - // furthermore we also don't update the parent. - if (node.kind === 'context') { - if (walk(node.nodes, visit, path, { ...context, ...node.context }) === WalkAction.Stop) { - return WalkAction.Stop - } - continue - } + case 'declaration': + return { + kind: node.kind, + property: node.property, + value: node.value, + important: node.important, + src: node.src, + dst: node.dst, + } satisfies Declaration as T - path.push(node) - let replacedNode = false - let replacedNodeOffset = 0 - let status = - visit(node, { - parent, - context, - path, - replaceWith(newNode) { - if (replacedNode) return - replacedNode = true + case 'comment': + return { + kind: node.kind, + value: node.value, + src: node.src, + dst: node.dst, + } satisfies Comment as T - if (Array.isArray(newNode)) { - if (newNode.length === 0) { - ast.splice(i, 1) - replacedNodeOffset = 0 - } else if (newNode.length === 1) { - ast[i] = newNode[0] - replacedNodeOffset = 1 - } else { - ast.splice(i, 1, ...newNode) - replacedNodeOffset = newNode.length - } - } else { - ast[i] = newNode - replacedNodeOffset = 1 - } - }, - }) ?? WalkAction.Continue - path.pop() - - // We want to visit or skip the newly replaced node(s), which start at the - // current index (i). By decrementing the index here, the next loop will - // process this position (containing the replaced node) again. - if (replacedNode) { - if (status === WalkAction.Continue) { - i-- - } else { - i += replacedNodeOffset - 1 - } - continue - } - - // Stop the walk entirely - if (status === WalkAction.Stop) return WalkAction.Stop - - // Skip visiting the children of this node - if (status === WalkAction.Skip) continue - - if ('nodes' in node) { - path.push(node) - let result = walk(node.nodes, visit, path, context) - path.pop() - - if (result === WalkAction.Stop) { - return WalkAction.Stop - } - } + default: + node satisfies never + throw new Error(`Unknown node kind: ${(node as any).kind}`) } } -// This is a depth-first traversal of the AST -export function walkDepth( - ast: AstNode[], - visit: ( - node: AstNode, - utils: { - parent: AstNode | null - path: AstNode[] - context: Record - replaceWith(newNode: AstNode[]): void - }, - ) => void, - path: AstNode[] = [], - context: Record = {}, -) { - for (let i = 0; i < ast.length; i++) { - let node = ast[i] - let parent = path[path.length - 1] ?? null - - if (node.kind === 'rule' || node.kind === 'at-rule') { - path.push(node) - walkDepth(node.nodes, visit, path, context) - path.pop() - } else if (node.kind === 'context') { - walkDepth(node.nodes, visit, path, { ...context, ...node.context }) - continue - } - - path.push(node) - visit(node, { - parent, - context, - path, - replaceWith(newNode) { - if (Array.isArray(newNode)) { - if (newNode.length === 0) { - ast.splice(i, 1) - } else if (newNode.length === 1) { - ast[i] = newNode[0] - } else { - ast.splice(i, 1, ...newNode) - } - } else { - ast[i] = newNode +export function cssContext( + ctx: VisitContext, +): VisitContext & { context: Record } { + return { + depth: ctx.depth, + index: ctx.index, + siblings: ctx.siblings, + get context() { + let context: Record = {} + for (let child of ctx.path()) { + if (child.kind === 'context') { + Object.assign(context, child.context) } + } - // Skip over the newly inserted nodes (being depth-first it doesn't make sense to visit them) - i += newNode.length - 1 - }, - }) - path.pop() + // Once computed, we never need to compute this again + Object.defineProperty(this, 'context', { value: context }) + return context + }, + get parent() { + let parent = (this.path().pop() as Extract) ?? null + + // Once computed, we never need to compute this again + Object.defineProperty(this, 'parent', { value: parent }) + return parent + }, + path() { + return ctx.path().filter((n) => n.kind !== 'context') + }, } } @@ -266,10 +229,8 @@ export function optimizeAst( ) { let atRoots: AstNode[] = [] let seenAtProperties = new Set() - let cssThemeVariables = new DefaultMap< - Extract['nodes'], - Set - >(() => new Set()) + let cssThemeVariables = new DefaultMap>(() => new Set()) + let colorMixDeclarations = new DefaultMap>(() => new Set()) let keyframes = new Set() let usedKeyframeNames = new Set() @@ -280,7 +241,7 @@ export function optimizeAst( function transform( node: AstNode, - parent: Extract['nodes'], + parent: AstNode[], context: Record = {}, depth = 0, ) { @@ -319,78 +280,20 @@ 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 && node.value.includes('color-mix(')) { - let ast = ValueParser.parse(node.value) - - let requiresPolyfill = false - ValueParser.walk(ast, (node, { replaceWith }) => { - if (node.kind !== 'function' || node.value !== 'color-mix') return - - let containsUnresolvableVars = false - let containsCurrentcolor = false - ValueParser.walk(node.nodes, (node, { replaceWith }) => { - if (node.kind == 'word' && node.value.toLowerCase() === 'currentcolor') { - containsCurrentcolor = true - requiresPolyfill = true - return - } - if (node.kind !== 'function' || node.value !== 'var') return - let firstChild = node.nodes[0] - if (!firstChild || firstChild.kind !== 'word') return - - requiresPolyfill = true - - let inlinedColor = designSystem.theme.resolveValue(null, [firstChild.value as any]) - if (!inlinedColor) { - containsUnresolvableVars = true - return - } - - replaceWith({ kind: 'word', value: inlinedColor }) - }) - - if (containsUnresolvableVars || containsCurrentcolor) { - let separatorIndex = node.nodes.findIndex( - (node) => node.kind === 'separator' && node.value.trim().includes(','), - ) - if (separatorIndex === -1) return - let firstColorValue = - node.nodes.length > separatorIndex ? node.nodes[separatorIndex + 1] : null - if (!firstColorValue) return - replaceWith(firstColorValue) - } else if (requiresPolyfill) { - // Change the colorspace to `srgb` since the fallback values should not be represented as - // `oklab(…)` functions again as their support in Safari <16 is very limited. - let colorspace = node.nodes[2] - if ( - colorspace.kind === 'word' && - (colorspace.value === 'oklab' || - colorspace.value === 'oklch' || - colorspace.value === 'lab' || - colorspace.value === 'lch') - ) { - colorspace.value = 'srgb' - } - } - }) - - if (requiresPolyfill) { - let fallback = { - ...node, - value: ValueParser.toCss(ast), - } - let colorMixQuery = rule('@supports (color: color-mix(in lab, red, red))', [node]) - - parent.push(fallback, colorMixQuery) - - return - } + // found in the theme config. + if ( + polyfills & Polyfills.ColorMix && + !context.supportsColorMix && + !context.keyframes && + node.value.includes('color-mix(') + ) { + colorMixDeclarations.get(parent).add(node) } parent.push(node) @@ -398,26 +301,14 @@ export function optimizeAst( // Rule else if (node.kind === 'rule') { - // Rules with `&` as the selector should be flattened - if (node.selector === '&') { - for (let child of node.nodes) { - let nodes: AstNode[] = [] - transform(child, nodes, context, depth + 1) - if (nodes.length > 0) { - parent.push(...nodes) - } - } + let nodes: AstNode[] = [] + + for (let child of node.nodes) { + transform(child, nodes, context, depth + 1) } - // - else { - let copy = { ...node, nodes: [] } - for (let child of node.nodes) { - transform(child, copy.nodes, context, depth + 1) - } - if (copy.nodes.length > 0) { - parent.push(copy) - } + if (nodes.length > 0) { + parent.push({ ...node, nodes }) } } @@ -444,10 +335,13 @@ export function optimizeAst( } } + let fallback = decl(property, initialValue ?? 'initial') + fallback.src = node.src + if (inherits) { - propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial')) + propertyFallbacksRoot.push(fallback) } else { - propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial')) + propertyFallbacksUniversal.push(fallback) } } @@ -464,6 +358,8 @@ 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: [] } @@ -482,7 +378,8 @@ export function optimizeAst( copy.name === '@charset' || copy.name === '@custom-media' || copy.name === '@namespace' || - copy.name === '@import' + copy.name === '@import' || + copy.name === '@apply' ) { parent.push(copy) } @@ -504,6 +401,21 @@ 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) @@ -595,15 +507,119 @@ export function optimizeAst( newAst = newAst.concat(atRoots) // Fallbacks + // Create fallback values for usages of the `color-mix(…)` function that reference variables + // found in the theme config. + if (polyfills & Polyfills.ColorMix) { + for (let [parent, declarations] of colorMixDeclarations) { + for (let declaration of declarations) { + let idx = parent.indexOf(declaration) + // If the declaration is no longer present, we don't need to create a polyfill anymore + if (idx === -1 || declaration.value == null) continue + + let ast = ValueParser.parse(declaration.value) + let requiresPolyfill = false + walk(ast, (node) => { + if (node.kind !== 'function' || node.value !== 'color-mix') return + + let containsUnresolvableVars = false + let containsCurrentcolor = false + walk(node.nodes, (node) => { + if (node.kind == 'word' && node.value.toLowerCase() === 'currentcolor') { + containsCurrentcolor = true + requiresPolyfill = true + return + } + + let varNode: ValueParser.ValueAstNode | null = node + let inlinedColor: string | null = null + let seenVariables = new Set() + do { + if (varNode.kind !== 'function' || varNode.value !== 'var') return + let firstChild = varNode.nodes[0] + if (!firstChild || firstChild.kind !== 'word') return + + let variableName = firstChild.value + + if (seenVariables.has(variableName)) { + containsUnresolvableVars = true + return + } + + seenVariables.add(variableName) + + requiresPolyfill = true + + inlinedColor = designSystem.theme.resolveValue(null, [firstChild.value as any]) + if (!inlinedColor) { + containsUnresolvableVars = true + return + } + if (inlinedColor.toLowerCase() === 'currentcolor') { + containsCurrentcolor = true + return + } + + if (inlinedColor.startsWith('var(')) { + let subAst = ValueParser.parse(inlinedColor) + varNode = subAst[0] + } else { + varNode = null + } + } while (varNode) + + return WalkAction.Replace({ kind: 'word', value: inlinedColor } as const) + }) + + if (containsUnresolvableVars || containsCurrentcolor) { + let separatorIndex = node.nodes.findIndex( + (node) => node.kind === 'separator' && node.value.trim().includes(','), + ) + if (separatorIndex === -1) return + let firstColorValue = + node.nodes.length > separatorIndex ? node.nodes[separatorIndex + 1] : null + if (!firstColorValue) return + return WalkAction.Replace(firstColorValue) + } else if (requiresPolyfill) { + // Change the colorspace to `srgb` since the fallback values should not be represented as + // `oklab(…)` functions again as their support in Safari <16 is very limited. + let colorspace = node.nodes[2] + if ( + colorspace.kind === 'word' && + (colorspace.value === 'oklab' || + colorspace.value === 'oklch' || + colorspace.value === 'lab' || + colorspace.value === 'lch') + ) { + colorspace.value = 'srgb' + } + } + }) + if (!requiresPolyfill) continue + + let fallback = { + ...declaration, + value: ValueParser.toCss(ast), + } + let colorMixQuery = rule('@supports (color: color-mix(in lab, red, red))', [declaration]) + colorMixQuery.src = declaration.src + parent.splice(idx, 1, fallback, colorMixQuery) + } + } + } + if (polyfills & Polyfills.AtProperty) { let fallbackAst = [] if (propertyFallbacksRoot.length > 0) { - fallbackAst.push(rule(':root, :host', propertyFallbacksRoot)) + let wrapper = rule(':root, :host', propertyFallbacksRoot) + wrapper.src = propertyFallbacksRoot[0].src + fallbackAst.push(wrapper) } if (propertyFallbacksUniversal.length > 0) { - fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)) + let wrapper = rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal) + wrapper.src = propertyFallbacksUniversal[0].src + fallbackAst.push(wrapper) } if (fallbackAst.length > 0) { @@ -625,30 +641,1009 @@ export function optimizeAst( return true }) + let layerPropertiesStatement = atRule('@layer', 'properties', []) + layerPropertiesStatement.src = fallbackAst[0].src + newAst.splice( firstValidNodeIndex < 0 ? newAst.length : firstValidNodeIndex, 0, - atRule('@layer', 'properties', []), + layerPropertiesStatement, ) - newAst.push( - rule('@layer properties', [ - atRule( - '@supports', - // We can't write a supports query for `@property` directly so we have to test for - // features that are added around the same time in Mozilla and Safari. - '((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))', - fallbackAst, - ), - ]), - ) + let block = rule('@layer properties', [ + atRule( + '@supports', + // We can't write a supports query for `@property` directly so we have to test for + // features that are added around the same time in Mozilla and Safari. + '((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))', + fallbackAst, + ), + ]) + + block.src = fallbackAst[0].src + block.nodes[0].src = fallbackAst[0].src + + newAst.push(block) } } - return newAst + return handleNesting(newAst) } -export function toCss(ast: AstNode[]) { +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 + + let source: Source = { + file: null, + code: '', + } + function stringify(node: AstNode, depth = 0): string { let css = '' let indent = ' '.repeat(depth) @@ -656,15 +1651,70 @@ export function toCss(ast: AstNode[]) { // Declaration if (node.kind === 'declaration') { css += `${indent}${node.property}: ${node.value}${node.important ? ' !important' : ''};\n` + + if (track) { + // indent + pos += indent.length + + // node.property + let start = pos + pos += node.property.length + + // `: ` + pos += 2 + + // node.value + pos += node.value?.length ?? 0 + + // !important + if (node.important) { + pos += 11 + } + + let end = pos + + // `;\n` + pos += 2 + + node.dst = [source, start, end] + } } // Rule else if (node.kind === 'rule') { css += `${indent}${node.selector} {\n` + + if (track) { + // indent + pos += indent.length + + // node.selector + let start = pos + pos += node.selector.length + + // ` ` + pos += 1 + + let end = pos + node.dst = [source, start, end] + + // `{\n` + pos += 2 + } + for (let child of node.nodes) { css += stringify(child, depth + 1) } + css += `${indent}}\n` + + if (track) { + // indent + pos += indent.length + + // `}\n` + pos += 2 + } } // AtRule @@ -677,22 +1727,97 @@ export function toCss(ast: AstNode[]) { // @layer base, components, utilities; // ``` if (node.nodes.length === 0) { - return `${indent}${node.name} ${node.params};\n` + let css = `${indent}${node.name} ${node.params};\n` + + if (track) { + // indent + pos += indent.length + + // node.name + let start = pos + pos += node.name.length + + // ` ` + pos += 1 + + // node.params + pos += node.params.length + let end = pos + + // `;\n` + pos += 2 + + node.dst = [source, start, end] + } + + return css } css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\n` + + if (track) { + // indent + pos += indent.length + + // node.name + let start = pos + pos += node.name.length + + if (node.params) { + // ` ` + pos += 1 + + // node.params + pos += node.params.length + } + + // ` ` + pos += 1 + + let end = pos + node.dst = [source, start, end] + + // `{\n` + pos += 2 + } + for (let child of node.nodes) { css += stringify(child, depth + 1) } + css += `${indent}}\n` + + if (track) { + // indent + pos += indent.length + + // `}\n` + pos += 2 + } } // Comment else if (node.kind === 'comment') { css += `${indent}/*${node.value}*/\n` + + if (track) { + // indent + pos += indent.length + + // The comment itself. We do this instead of just the inside because + // it seems more useful to have the entire comment span tracked. + let start = pos + pos += 2 + node.value.length + 2 + let end = pos + + node.dst = [source, start, end] + + // `\n` + pos += 1 + } } - // These should've been handled already by `prepareAstForPrinting` which + // These should've been handled already by `optimizeAst` which // means we can safely ignore them here. We return an empty string // immediately to signal that something went wrong. else if (node.kind === 'context' || node.kind === 'at-root') { @@ -710,20 +1835,20 @@ export function toCss(ast: AstNode[]) { let css = '' for (let node of ast) { - let result = stringify(node) - if (result !== '') { - css += result - } + css += stringify(node, 0) } + source.code = css + return css } function findNode(ast: AstNode[], fn: (node: AstNode) => boolean): AstNode[] | null { let foundPath: AstNode[] = [] - walk(ast, (node, { path }) => { + walk(ast, (node, ctx) => { if (fn(node)) { - foundPath = [...path] + foundPath = ctx.path() + foundPath.push(node) return WalkAction.Stop } }) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 090279fd4..a19123105 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -1,37 +1,13 @@ 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 { optimizeCss } from './test-utils/run' +import { compileCss, run } 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 }> - loadModule?: ( - id: string, - base: string, - resourceHint: 'plugin' | 'config', - ) => Promise<{ module: Config | Plugin; base: 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 () => { - let loadStylesheet = async (id: string, base: string) => { + async function loadStylesheet(id: string, base: string) { expect(base).toBe('/root') expect(id).toBe('./foo/bar.css') return { @@ -41,18 +17,20 @@ test('can resolve relative @imports', async () => { } `, base: '/root/foo', + path: '', } } - await expect( - run( + expect( + await compileCss( css` @import './foo/bar.css'; `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - ".foo { + ).toMatchInlineSnapshot(` + " + .foo { color: red; } " @@ -60,13 +38,14 @@ test('can resolve relative @imports', async () => { }) test('can recursively resolve relative @imports', async () => { - let loadStylesheet = async (id: string, base: string) => { + async function loadStylesheet(id: string, base: string) { if (base === '/root' && id === './foo/bar.css') { return { content: css` @import './bar/baz.css'; `, base: '/root/foo', + path: '', } } else if (base === '/root/foo' && id === './bar/baz.css') { return { @@ -76,21 +55,23 @@ test('can recursively resolve relative @imports', async () => { } `, base: '/root/foo/bar', + path: '', } } throw new Error(`Unexpected import: ${id}`) } - await expect( - run( + expect( + await compileCss( css` @import './foo/bar.css'; `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - ".baz { + ).toMatchInlineSnapshot(` + " + .baz { color: #00f; } " @@ -102,52 +83,56 @@ let exampleCSS = css` color: red; } ` -let loadStylesheet = async (id: string) => { +async function loadStylesheet(id: string) { if (!id.endsWith('example.css')) throw new Error('Unexpected import: ' + id) return { content: exampleCSS, base: '/root', + path: '', } } test('extracts path from @import nodes', async () => { - await expect( - run( + expect( + await compileCss( css` @import 'example.css'; `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "a { + ).toMatchInlineSnapshot(` + " + a { color: red; } " `) - await expect( - run( + expect( + await compileCss( css` @import './example.css'; `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "a { + ).toMatchInlineSnapshot(` + " + a { color: red; } " `) - await expect( - run( + expect( + await compileCss( css` @import '/example.css'; `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "a { + ).toMatchInlineSnapshot(` + " + a { color: red; } " @@ -155,71 +140,96 @@ test('extracts path from @import nodes', async () => { }) test('url() imports are passed-through', async () => { - await expect( - run( + expect( + await compileCss( css` @import url('example.css'); `, - { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, ), - ).resolves.toMatchInlineSnapshot(`"@import url('example.css');"`) + ).toMatchInlineSnapshot(` + " + @import "example.css"; + " + `) - await expect( - run( + expect( + await compileCss( css` @import url('./example.css'); `, - { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, ), - ).resolves.toMatchInlineSnapshot(`"@import url('./example.css');"`) + ).toMatchInlineSnapshot(` + " + @import "./example.css"; + " + `) - await expect( - run( + expect( + await compileCss( css` @import url('/example.css'); `, - { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, ), - ).resolves.toMatchInlineSnapshot(`"@import url('/example.css');"`) + ).toMatchInlineSnapshot(` + " + @import "/example.css"; + " + `) - await expect( - run( + expect( + await compileCss( css` @import url(example.css); `, - { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, ), - ).resolves.toMatchInlineSnapshot(`"@import url(example.css);"`) + ).toMatchInlineSnapshot(` + " + @import "example.css"; + " + `) - await expect( - run( + expect( + await compileCss( css` @import url(./example.css); `, - { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, ), - ).resolves.toMatchInlineSnapshot(`"@import url(./example.css);"`) + ).toMatchInlineSnapshot(` + " + @import "./example.css"; + " + `) - await expect( - run( + expect( + await compileCss( css` @import url(/example.css); `, - { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, ), - ).resolves.toMatchInlineSnapshot(`"@import url(/example.css);"`) + ).toMatchInlineSnapshot(` + " + @import "/example.css"; + " + `) }) test('handles case-insensitive @import directive', async () => { - await expect( - run( + expect( + await compileCss( css` @import 'example.css'; `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "a { + ).toMatchInlineSnapshot(` + " + a { color: red; } " @@ -227,15 +237,16 @@ test('handles case-insensitive @import directive', async () => { }) test('@media', async () => { - await expect( - run( + expect( + await compileCss( css` @import 'example.css' print; `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@media print { + ).toMatchInlineSnapshot(` + " + @media print { a { color: red; } @@ -243,15 +254,16 @@ test('@media', async () => { " `) - await expect( - run( + expect( + await compileCss( css` @import 'example.css' print, screen; `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@media print, screen { + ).toMatchInlineSnapshot(` + " + @media print, screen { a { color: red; } @@ -259,15 +271,16 @@ test('@media', async () => { " `) - await expect( - run( + expect( + await compileCss( css` @import 'example.css' screen and (orientation: landscape); `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@media screen and (orientation: landscape) { + ).toMatchInlineSnapshot(` + " + @media screen and (orientation: landscape) { a { color: red; } @@ -275,15 +288,16 @@ test('@media', async () => { " `) - await expect( - run( + expect( + await compileCss( css` @import 'example.css' foo(bar); `, - { loadStylesheet, optimize: false }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@media foo(bar) { + ).toMatchInlineSnapshot(` + " + @media foo(bar) { a { color: red; } @@ -293,15 +307,16 @@ test('@media', async () => { }) test('@supports', async () => { - await expect( - run( + expect( + await compileCss( css` @import 'example.css' supports(display: grid); `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@supports (display: grid) { + ).toMatchInlineSnapshot(` + " + @supports (display: grid) { a { color: red; } @@ -309,15 +324,16 @@ test('@supports', async () => { " `) - await expect( - run( + expect( + await compileCss( css` @import 'example.css' supports(display: grid) screen and (max-width: 400px); `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@supports (display: grid) { + ).toMatchInlineSnapshot(` + " + @supports (display: grid) { @media screen and (max-width: 400px) { a { color: red; @@ -327,16 +343,17 @@ test('@supports', async () => { " `) - await expect( - run( + expect( + await compileCss( css` @import 'example.css' supports((not (display: grid)) and (display: flex)) screen and (max-width: 400px); `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@supports (not (display: grid)) and (display: flex) { + ).toMatchInlineSnapshot(` + " + @supports (not (display: grid)) and (display: flex) { @media screen and (max-width: 400px) { a { color: red; @@ -346,17 +363,18 @@ test('@supports', async () => { " `) - await expect( - run( + expect( + await compileCss( // prettier-ignore css` @import 'example.css' supports((selector(h2 > p)) and (font-tech(color-COLRv1))); `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@supports selector(h2 > p) and font-tech(color-COLRv1) { + ).toMatchInlineSnapshot(` + " + @supports selector(h2 > p) and font-tech(color-COLRv1) { a { color: red; } @@ -366,15 +384,16 @@ test('@supports', async () => { }) test('@layer', async () => { - await expect( - run( + expect( + await compileCss( css` @import 'example.css' layer(utilities); `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@layer utilities { + ).toMatchInlineSnapshot(` + " + @layer utilities { a { color: red; } @@ -382,15 +401,16 @@ test('@layer', async () => { " `) - await expect( - run( + expect( + await compileCss( css` @import 'example.css' layer(); `, - { loadStylesheet }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - "@layer { + ).toMatchInlineSnapshot(` + " + @layer { a { color: red; } @@ -401,12 +421,14 @@ test('@layer', async () => { test('supports theme(reference) imports', async () => { expect( - run( + await run( + ['text-red-500'], css` @tailwind utilities; @import 'example.css' theme(reference); `, { + base: '/root', loadStylesheet: () => Promise.resolve({ content: css` @@ -415,12 +437,13 @@ test('supports theme(reference) imports', async () => { } `, base: '', + path: '', }), - candidates: ['text-red-500'], }, ), - ).resolves.toMatchInlineSnapshot(` - ".text-red-500 { + ).toMatchInlineSnapshot(` + " + .text-red-500 { color: var(--color-red-500, red); } " @@ -428,27 +451,27 @@ test('supports theme(reference) imports', async () => { }) test('updates the base when loading modules inside nested files', async () => { - let loadStylesheet = () => - Promise.resolve({ + async function loadStylesheet() { + return { content: css` @config './nested-config.js'; @plugin './nested-plugin.js'; `, base: '/root/foo', - }) - let loadModule = vi.fn().mockResolvedValue({ base: '', module: () => {} }) + path: '', + } + } + using loadModule = vi.fn().mockResolvedValue({ base: '', path: '', module: () => {} }) expect( - ( - await run( - css` - @import './foo/bar.css'; - @config './root-config.js'; - @plugin './root-plugin.js'; - `, - { loadStylesheet, loadModule }, - ) - ).trim(), + await compileCss( + css` + @import './foo/bar.css'; + @config './root-config.js'; + @plugin './root-plugin.js'; + `, + { base: '/root', loadStylesheet, loadModule }, + ), ).toBe('') expect(loadModule).toHaveBeenNthCalledWith(1, './nested-config.js', '/root/foo', 'config') @@ -458,13 +481,15 @@ test('updates the base when loading modules inside nested files', async () => { }) test('emits the right base for @source directives inside nested files', async () => { - let loadStylesheet = () => - Promise.resolve({ + async function loadStylesheet() { + return { content: css` @source './nested/**/*.css'; `, base: '/root/foo', - }) + path: '', + } + } let compiler = await compile( css` @@ -481,15 +506,18 @@ 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 () => { - let loadStylesheet = () => - Promise.resolve({ + async function loadStylesheet() { + return { content: css` @config './nested-config.js'; @plugin './nested-plugin.js'; `, base: '/root/foo', - }) - let loadModule = vi.fn().mockImplementation((id: string) => { + path: '', + } + } + + using 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' @@ -502,12 +530,14 @@ test('emits the right base for @source found inside JS configs and plugins from plugins: [plugin(() => {}, { content: [pluginGlob] })], } satisfies Config, base: base + '-config', + path: '', } } else { let glob = id.includes('nested') ? './nested-plugin/*.html' : './root-plugin/*.html' return { module: plugin(() => {}, { content: [glob] }), base: base + '-plugin', + path: '', } } }) @@ -534,28 +564,25 @@ test('emits the right base for @source found inside JS configs and plugins from }) test('it crashes when inside a cycle', async () => { - let loadStylesheet = () => - Promise.resolve({ - content: css` - @import 'foo.css'; - `, - base: '/root', - }) + let input = css` + @import 'foo.css'; + ` - expect( - run( - css` - @import 'foo.css'; - `, - { loadStylesheet }, - ), - ).rejects.toMatchInlineSnapshot( + async function loadStylesheet() { + return { + content: input, + base: '/root', + path: '', + } + } + + await expect(compileCss(input, { base: '/root', loadStylesheet })).rejects.toMatchInlineSnapshot( `[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`, ) }) test('resolves @reference as `@import "…" reference`', async () => { - let loadStylesheet = async (id: string, base: string) => { + async function loadStylesheet(id: string, base: string) { expect(base).toBe('/root') expect(id).toBe('./foo/bar.css') return { @@ -568,19 +595,22 @@ test('resolves @reference as `@import "…" reference`', async () => { } `, base: '/root/foo', + path: '', } } - await expect( - run( + expect( + await run( + ['text-red-500'], css` @reference './foo/bar.css'; @tailwind utilities; `, - { loadStylesheet, candidates: ['text-red-500'] }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - ".text-red-500 { + ).toMatchInlineSnapshot(` + " + .text-red-500 { color: var(--color-red-500, red); } " @@ -588,7 +618,7 @@ test('resolves @reference as `@import "…" reference`', async () => { }) test('resolves `@variant` used as `@custom-variant` inside `@reference`', async () => { - let loadStylesheet = async (id: string, base: string) => { + async function loadStylesheet(id: string, base: string) { expect(base).toBe('/root') expect(id).toBe('./foo/bar.css') return { @@ -600,19 +630,22 @@ test('resolves `@variant` used as `@custom-variant` inside `@reference`', async } `, base: '/root/foo', + path: '', } } - await expect( - run( + expect( + await run( + ['dark:flex'], css` @reference './foo/bar.css'; @tailwind utilities; `, - { loadStylesheet, candidates: ['dark:flex'] }, + { base: '/root', loadStylesheet }, ), - ).resolves.toMatchInlineSnapshot(` - ".dark\\:flex:where([data-theme="dark"] *) { + ).toMatchInlineSnapshot(` + " + .dark\\:flex:where([data-theme="dark"] *) { display: flex; } " diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index effd33e5b..ba7172f8e 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -1,20 +1,29 @@ import { Features } from '.' -import { atRule, context, walk, WalkAction, type AstNode } from './ast' +import { atRule, context, type AstNode } from './ast' import * as CSS from './css-parser' import * as ValueParser from './value-parser' +import { walk, WalkAction } from './walk' -type LoadStylesheet = (id: string, basedir: string) => Promise<{ base: string; content: string }> +type LoadStylesheet = ( + id: string, + basedir: string, +) => Promise<{ + path: string + base: string + content: string +}> export async function substituteAtImports( ast: AstNode[], base: string, loadStylesheet: LoadStylesheet, recurseCount = 0, + track = false, ) { let features = Features.None let promises: Promise[] = [] - walk(ast, (node, { replaceWith }) => { + walk(ast, (node) => { if (node.kind === 'at-rule' && (node.name === '@import' || node.name === '@reference')) { let parsed = parseImportParams(ValueParser.parse(node.params)) if (parsed === null) return @@ -45,10 +54,11 @@ export async function substituteAtImports( } let loaded = await loadStylesheet(uri, base) - let ast = CSS.parse(loaded.content) - await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1) + let ast = CSS.parse(loaded.content, { from: track ? loaded.path : undefined }) + await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1, track) contextNode.nodes = buildImportNodes( + node, [context({ base: loaded.base }, ast)], layer, media, @@ -57,11 +67,9 @@ export async function substituteAtImports( })(), ) - replaceWith(contextNode) - // The resolved Stylesheets already have their transitive @imports // resolved, so we can skip walking them. - return WalkAction.Skip + return WalkAction.ReplaceSkip(contextNode) } }) @@ -140,6 +148,7 @@ export function parseImportParams(params: ValueParser.ValueAstNode[]) { } function buildImportNodes( + importNode: AstNode, importedAst: AstNode[], layer: string | null, media: string | null, @@ -148,15 +157,21 @@ function buildImportNodes( let root = importedAst if (layer !== null) { - root = [atRule('@layer', layer, root)] + let node = atRule('@layer', layer, root) + node.src = importNode.src + root = [node] } if (media !== null) { - root = [atRule('@media', media, root)] + let node = atRule('@media', media, root) + node.src = importNode.src + root = [node] } if (supports !== null) { - root = [atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)] + let node = atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root) + node.src = importNode.src + root = [node] } return root diff --git a/packages/tailwindcss/src/attribute-selector-parser.bench.ts b/packages/tailwindcss/src/attribute-selector-parser.bench.ts new file mode 100644 index 000000000..2d6e89590 --- /dev/null +++ b/packages/tailwindcss/src/attribute-selector-parser.bench.ts @@ -0,0 +1,35 @@ +import { bench, describe } from 'vitest' +import * as AttributeSelectorParser from './attribute-selector-parser' + +let examples = [ + '[open]', + '[data-foo]', + '[data-state=expanded]', + '[data-state = expanded ]', + '[data-state*="expanded"]', + '[data-state*="expanded"i]', + '[data-state*=expanded i]', +] + +const ATTRIBUTE_REGEX = + /\[\s*(?[a-zA-Z_-][a-zA-Z0-9_-]*)\s*((?[*|~^$]?=)\s*(?['"])?\s*(?.*?)\4\s*(?[is])?\s*)?\]/ + +describe('parsing', () => { + bench('AttributeSelectorParser.parse', () => { + for (let example of examples) { + AttributeSelectorParser.parse(example) + } + }) + + bench('REGEX.test(…)', () => { + for (let example of examples) { + ATTRIBUTE_REGEX.exec(example) + } + }) + + bench('….match(REGEX)', () => { + for (let example of examples) { + example.match(ATTRIBUTE_REGEX) + } + }) +}) diff --git a/packages/tailwindcss/src/attribute-selector-parser.test.ts b/packages/tailwindcss/src/attribute-selector-parser.test.ts new file mode 100644 index 000000000..87f5f3013 --- /dev/null +++ b/packages/tailwindcss/src/attribute-selector-parser.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' +import { parse } from './attribute-selector-parser' + +describe('parse', () => { + it.each([ + [''], + [']'], + ['[]'], + ['['], + ['="value"'], + ['data-foo]'], + ['[data-foo'], + ['[data-foo="foo]'], + ['[data-foo * = foo]'], + ['[data-foo*=]'], + ['[data-foo=value x]'], + ['[data-foo=value ix]'], + ])('should parse an invalid attribute selector (%s) as `null`', (input) => { + expect(parse(input)).toBeNull() + }) + + it.each([ + [ + '[data-foo]', + { attribute: 'data-foo', operator: null, quote: null, value: null, sensitivity: null }, + ], + [ + '[ 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 }, + ], + [ + '[data-state = expanded ]', + { attribute: 'data-state', operator: '=', quote: null, value: 'expanded', sensitivity: null }, + ], + [ + '[data-state*="expanded"]', + { attribute: 'data-state', operator: '*=', quote: '"', value: 'expanded', sensitivity: null }, + ], + [ + '[data-state*="expanded"i]', + { attribute: 'data-state', operator: '*=', quote: '"', value: 'expanded', sensitivity: 'i' }, + ], + [ + '[data-state*=expanded i]', + { attribute: 'data-state', operator: '*=', quote: null, value: 'expanded', sensitivity: 'i' }, + ], + ])('should parse correctly: %s', (selector, expected) => { + expect(parse(selector)).toEqual(expected) + }) + + it('should work with a real-world example', () => { + expect(parse('[data-url$=".com"i]')).toEqual({ + attribute: 'data-url', + operator: '$=', + quote: '"', + value: '.com', + sensitivity: 'i', + }) + }) +}) diff --git a/packages/tailwindcss/src/attribute-selector-parser.ts b/packages/tailwindcss/src/attribute-selector-parser.ts new file mode 100644 index 000000000..624043d59 --- /dev/null +++ b/packages/tailwindcss/src/attribute-selector-parser.ts @@ -0,0 +1,232 @@ +const TAB = 9 +const LINE_BREAK = 10 +const CARRIAGE_RETURN = 13 +const SPACE = 32 +const DOUBLE_QUOTE = 34 +const DOLLAR = 36 +const SINGLE_QUOTE = 39 +const ASTERISK = 42 +const EQUALS = 61 +const UPPER_I = 73 +const UPPER_S = 83 +const BACKSLASH = 92 +const CARET = 94 +const LOWER_I = 105 +const LOWER_S = 115 +const PIPE = 124 +const TILDE = 126 +const LOWER_A = 97 +const LOWER_Z = 122 +const UPPER_A = 65 +const UPPER_Z = 90 +const ZERO = 48 +const NINE = 57 +const DASH = 45 +const UNDERSCORE = 95 +const NON_ASCII = 128 + +interface AttributeSelector { + attribute: string + operator: '=' | '~=' | '|=' | '^=' | '$=' | '*=' | null + quote: '"' | "'" | null + value: string | null + sensitivity: 'i' | 's' | null +} + +export function parse(input: string): AttributeSelector | null { + // Must start with `[` and end with `]` + if (input[0] !== '[' || input[input.length - 1] !== ']') { + return null + } + + let i = 1 + let start = i + let end = input.length - 1 + + // Skip whitespace, e.g.: [ data-foo] + // ^^^ + while (isAsciiWhitespace(input.charCodeAt(i))) i++ + + // Attribute name, e.g.: [data-foo] + // ^^^^^^^^ + { + start = i + for (; i < end; i++) { + let currentChar = input.charCodeAt(i) + // Skip escaped character + if (currentChar === BACKSLASH) { + i++ + continue + } + if (currentChar >= UPPER_A && currentChar <= UPPER_Z) continue + 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 + } + + // Must have at least one character in the attribute name + if (start === i) { + return null + } + } + let attribute = input.slice(start, i) + + // Skip whitespace, e.g.: [data-foo =value] + // ^^^ + while (isAsciiWhitespace(input.charCodeAt(i))) i++ + + // At the end, e.g.: `[data-foo]` + if (i === end) { + return { + attribute, + operator: null, + quote: null, + value: null, + sensitivity: null, + } + } + + // Operator, e.g.: [data-foo*=value] + // ^^ + let operator = null + let currentChar = input.charCodeAt(i) + if (currentChar === EQUALS) { + operator = '=' + i++ + } else if ( + (currentChar === TILDE || + currentChar === PIPE || + currentChar === CARET || + currentChar === DOLLAR || + currentChar === ASTERISK) && + input.charCodeAt(i + 1) === EQUALS + ) { + operator = input[i] + '=' + i += 2 + } else { + return null // Invalid operator + } + + // Skip whitespace, e.g.: [data-foo*= value] + // ^^^ + while (isAsciiWhitespace(input.charCodeAt(i))) i++ + + // At the end, that means that we have an operator but no valid, which is + // invalid, e.g.: `[data-foo*=]` + if (i === end) { + return null + } + + // Value, e.g.: [data-foo*=value] + // ^^^^^ + let value = '' + + // Quoted value, e.g.: [data-foo*="value"] + // ^^^^^^^ + let quote = null + currentChar = input.charCodeAt(i) + if (currentChar === SINGLE_QUOTE || currentChar === DOUBLE_QUOTE) { + quote = input[i] as '"' | "'" + i++ + + start = i + for (let j = i; j < end; j++) { + let current = input.charCodeAt(j) + // Found ending quote + if (current === currentChar) { + i = j + 1 + } + + // Skip escaped character + else if (current === BACKSLASH) { + j++ + } + } + + value = input.slice(start, i - 1) + } + + // Unquoted value, e.g.: [data-foo*=value] + // ^^^^^ + else { + start = i + // Keep going until we find whitespace or the end + while (i < end && !isAsciiWhitespace(input.charCodeAt(i))) i++ + value = input.slice(start, i) + } + + // Skip whitespace, e.g.: [data-foo*=value ] + // ^^^ + while (isAsciiWhitespace(input.charCodeAt(i))) i++ + + // At the end, e.g.: `[data-foo=value]` + if (i === end) { + return { + attribute, + operator: operator as '=' | '~=' | '|=' | '^=' | '$=' | '*=', + quote: quote as '"' | "'" | null, + value, + sensitivity: null, + } + } + + // Sensitivity, e.g.: [data-foo=value i] + // ^ + let sensitivity = null + { + switch (input.charCodeAt(i)) { + case LOWER_I: + case UPPER_I: { + sensitivity = 'i' + i++ + break + } + + case LOWER_S: + case UPPER_S: { + sensitivity = 's' + i++ + break + } + + default: + return null // Invalid sensitivity + } + } + + // Skip whitespace, e.g.: [data-foo=value i ] + // ^^^ + while (isAsciiWhitespace(input.charCodeAt(i))) i++ + + // We must be at the end now, if not, then there is an additional character + // after the sensitivity which is invalid, e.g.: [data-foo=value iX] + // ^ + if (i !== end) { + return null + } + + // Fully done + return { + attribute, + operator: operator as '=' | '~=' | '|=' | '^=' | '$=' | '*=', + quote: quote as '"' | "'" | null, + value, + sensitivity: sensitivity as 'i' | 's' | null, + } +} + +function isAsciiWhitespace(code: number): boolean { + switch (code) { + case SPACE: + case TAB: + case LINE_BREAK: + case CARRIAGE_RETURN: + return true + + default: + return false + } +} diff --git a/packages/tailwindcss/src/candidate.bench.ts b/packages/tailwindcss/src/candidate.bench.ts index 5b7171432..7ac0dbb0a 100644 --- a/packages/tailwindcss/src/candidate.bench.ts +++ b/packages/tailwindcss/src/candidate.bench.ts @@ -1,6 +1,6 @@ import { Scanner } from '@tailwindcss/oxide' -import { bench } from 'vitest' -import { parseCandidate } from './candidate' +import { bench, describe } from 'vitest' +import { cloneCandidate, parseCandidate, type Candidate, type Variant } from './candidate' import { buildDesignSystem } from './design-system' import { Theme } from './theme' @@ -8,13 +8,87 @@ import { Theme } from './theme' const root = process.env.FOLDER || process.cwd() // Auto content detection -const scanner = new Scanner({ sources: [{ base: root, pattern: '**/*' }] }) +const scanner = new Scanner({ sources: [{ base: root, pattern: '**/*', negated: false }] }) const candidates = scanner.scan() const designSystem = buildDesignSystem(new Theme()) -bench('parseCandidate', () => { - for (let candidate of candidates) { - Array.from(parseCandidate(candidate, designSystem)) - } +describe('parsing', () => { + bench('parseCandidate', () => { + for (let candidate of candidates) { + Array.from(parseCandidate(candidate, designSystem)) + } + }) }) + +describe('Candidate cloning', async () => { + let parsedCanddiates = candidates.flatMap((candidate) => + Array.from(parseCandidate(candidate, designSystem)), + ) + + bench('structuredClone', () => { + for (let candidate of parsedCanddiates) { + structuredClone(candidate) + } + }) + + bench('cloneCandidate', () => { + for (let candidate of parsedCanddiates) { + cloneCandidate(candidate) + } + }) + + bench('cloneCandidate (spread)', () => { + for (let candidate of parsedCanddiates) { + cloneCandidateSpread(candidate) + } + }) +}) + +function cloneCandidateSpread(candidate: Candidate): Candidate { + switch (candidate.kind) { + case 'arbitrary': + return { + ...candidate, + modifier: candidate.modifier ? { ...candidate.modifier } : null, + variants: candidate.variants.map(cloneVariantSpread), + } + + case 'static': + return { ...candidate, variants: candidate.variants.map(cloneVariantSpread) } + + case 'functional': + return { + ...candidate, + value: candidate.value ? { ...candidate.value } : null, + modifier: candidate.modifier ? { ...candidate.modifier } : null, + variants: candidate.variants.map(cloneVariantSpread), + } + + default: + candidate satisfies never + throw new Error('Unknown candidate kind') + } +} + +function cloneVariantSpread(variant: Variant): Variant { + switch (variant.kind) { + case 'arbitrary': + case 'static': + return { ...variant } + + case 'functional': + return { ...variant, modifier: variant.modifier ? { ...variant.modifier } : null } + + case 'compound': + return { + ...variant, + variant: cloneVariantSpread(variant.variant), + modifier: variant.modifier ? { ...variant.modifier } : null, + } + + default: + variant satisfies never + throw new Error('Unknown variant kind') + } +} diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 61be4378a..b8f914e1b 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1,4 +1,5 @@ -import { expect, it } from 'vitest' +import { describe, expect, it, test } from 'vitest' +import { __unstable__loadDesignSystem } from '.' import { buildDesignSystem } from './design-system' import { Theme } from './theme' import { Utilities } from './utilities' @@ -515,6 +516,8 @@ 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', () => { @@ -1087,6 +1090,7 @@ it('should parse a utility with an implicit variable as the modifier using the s let utilities = new Utilities() utilities.functional('bg', () => []) + // Standard case (no underscores) expect(run('bg-red-500/(--value)', { utilities })).toMatchInlineSnapshot(` [ { @@ -1107,6 +1111,156 @@ it('should parse a utility with an implicit variable as the modifier using the s }, ] `) + + // Should preserve underscores + expect(run('bg-red-500/(--with_underscore)', { utilities })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": { + "kind": "arbitrary", + "value": "var(--with_underscore)", + }, + "raw": "bg-red-500/(--with_underscore)", + "root": "bg", + "value": { + "fraction": null, + "kind": "named", + "value": "red-500", + }, + "variants": [], + }, + ] + `) + + // Should remove underscores in fallback values + expect(run('bg-red-500/(--with_underscore,fallback_value)', { utilities })) + .toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": { + "kind": "arbitrary", + "value": "var(--with_underscore,fallback value)", + }, + "raw": "bg-red-500/(--with_underscore,fallback_value)", + "root": "bg", + "value": { + "fraction": null, + "kind": "named", + "value": "red-500", + }, + "variants": [], + }, + ] + `) + + // Should keep underscores in the CSS variable itself, but remove underscores + // in fallback values + expect(run('bg-(--a_b,c_d_var(--e_f,g_h))/(--i_j,k_l_var(--m_n,o_p))', { utilities })) + .toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": { + "kind": "arbitrary", + "value": "var(--i_j,k l var(--m_n,o p))", + }, + "raw": "bg-(--a_b,c_d_var(--e_f,g_h))/(--i_j,k_l_var(--m_n,o_p))", + "root": "bg", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "var(--a_b,c d var(--e_f,g h))", + }, + "variants": [], + }, + ] + `) +}) + +it('should not parse an invalid arbitrary shorthand modifier', () => { + let utilities = new Utilities() + utilities.functional('bg', () => []) + + // Completely empty + expect(run('bg-red-500/()', { utilities })).toMatchInlineSnapshot(`[]`) + + // Invalid due to leading spaces + expect(run('bg-red-500/(_--)', { utilities })).toMatchInlineSnapshot(`[]`) + expect(run('bg-red-500/(_--x)', { utilities })).toMatchInlineSnapshot(`[]`) + + // Invalid due to leading spaces + expect(run('bg-red-500/(_--)', { utilities })).toMatchInlineSnapshot(`[]`) + expect(run('bg-red-500/(_--x)', { utilities })).toMatchInlineSnapshot(`[]`) + + // Invalid due to top-level `;` or `}` characters + expect(run('bg-red-500/(--x;--y)', { utilities })).toMatchInlineSnapshot(`[]`) + expect(run('bg-red-500/(--x:{foo:bar})', { utilities })).toMatchInlineSnapshot(`[]`) + + // Valid, but ensuring that we didn't make an off-by-one error + expect(run('bg-red-500/(--x)', { utilities })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": { + "kind": "arbitrary", + "value": "var(--x)", + }, + "raw": "bg-red-500/(--x)", + "root": "bg", + "value": { + "fraction": null, + "kind": "named", + "value": "red-500", + }, + "variants": [], + }, + ] + `) +}) + +it('should not parse an invalid arbitrary shorthand value', () => { + let utilities = new Utilities() + utilities.functional('bg', () => []) + + // Completely empty + expect(run('bg-()', { utilities })).toMatchInlineSnapshot(`[]`) + + // Invalid due to leading spaces + expect(run('bg-(_--)', { utilities })).toMatchInlineSnapshot(`[]`) + expect(run('bg-(_--x)', { utilities })).toMatchInlineSnapshot(`[]`) + + // Invalid due to leading spaces + expect(run('bg-(_--)', { utilities })).toMatchInlineSnapshot(`[]`) + expect(run('bg-(_--x)', { utilities })).toMatchInlineSnapshot(`[]`) + + // Invalid due to top-level `;` or `}` characters + expect(run('bg-(--x;--y)', { utilities })).toMatchInlineSnapshot(`[]`) + expect(run('bg-(--x:{foo:bar})', { utilities })).toMatchInlineSnapshot(`[]`) + + // Valid, but ensuring that we didn't make an off-by-one error + expect(run('bg-(--x)', { utilities })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "bg-(--x)", + "root": "bg", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "var(--x)", + }, + "variants": [], + }, + ] + `) }) it('should not parse a utility with an implicit invalid variable as the modifier using the shorthand', () => { @@ -1264,6 +1418,19 @@ 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', () => []) @@ -1294,6 +1461,36 @@ it('should parse a functional variant starting with @', () => { `) }) +it('should parse a functional variant starting with @ that has a hyphen', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + + let variants = new Variants() + variants.functional('@', () => {}) + + expect(run('@foo-bar:flex', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "static", + "raw": "@foo-bar:flex", + "root": "flex", + "variants": [ + { + "kind": "functional", + "modifier": null, + "root": "@", + "value": { + "kind": "named", + "value": "foo-bar", + }, + }, + ], + }, + ] + `) +}) + it('should parse a functional variant starting with @ and a modifier', () => { let utilities = new Utilities() utilities.static('flex', () => []) @@ -1594,6 +1791,14 @@ it('should not parse compound group with a non-compoundable variant', () => { expect(run('group-*:flex', { utilities, variants })).toMatchInlineSnapshot(`[]`) }) +it('empty data types are invalid', () => { + let utilities = new Utilities() + utilities.functional('bg', () => []) + let variants = new Variants() + + expect(run('bg-[:foo]', { utilities, variants })).toMatchInlineSnapshot(`[]`) +}) + it('should parse a variant containing an arbitrary string with unbalanced parens, brackets, curlies and other quotes', () => { let utilities = new Utilities() utilities.static('flex', () => []) @@ -1807,7 +2012,12 @@ it.each([ // Arbitrary variant values that end the block 'data-[a]{color:red}foo[a]:flex', -])('should not parse invalid arbitrary values: %s', (rawCandidate) => { + + // Named values contain invalid characters + 'data-foo=bar:flex', + 'bg-foo=bar', + 'bg-red-500/foo=bar', +])('should not parse invalid values: %s', (rawCandidate) => { let utilities = new Utilities() utilities.static('flex', () => []) utilities.functional('bg', () => []) @@ -1818,3 +2028,129 @@ it.each([ expect(run(rawCandidate, { utilities, variants })).toEqual([]) }) + +const candidates = [ + // Arbitrary candidates + ['[color:red]', '[color:red]'], + ['[color:red]/50', '[color:red]/50'], + ['[color:red]/[0.5]', '[color:red]/[0.5]'], + ['[color:red]/50!', '[color:red]/50!'], + ['![color:red]/50', '[color:red]/50!'], + ['[color:red]/[0.5]!', '[color:red]/[0.5]!'], + + // Static candidates + ['box-border', 'box-border'], + ['underline!', 'underline!'], + ['!underline', 'underline!'], + ['-inset-full', '-inset-full'], + + // Functional candidates + ['bg-red-500', 'bg-red-500'], + ['bg-red-500/50', 'bg-red-500/50'], + ['bg-red-500/[0.5]', 'bg-red-500/[0.5]'], + ['bg-red-500!', 'bg-red-500!'], + ['!bg-red-500', 'bg-red-500!'], + ['bg-[#0088cc]/50', 'bg-[#0088cc]/50'], + ['bg-[#0088cc]/[0.5]', 'bg-[#0088cc]/[0.5]'], + ['bg-[#0088cc]!', 'bg-[#0088cc]!'], + ['!bg-[#0088cc]', 'bg-[#0088cc]!'], + ['bg-[var(--spacing)-1px]', 'bg-[var(--spacing)-1px]'], + ['bg-[var(--spacing)_-_1px]', 'bg-[var(--spacing)-1px]'], + ['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'], + ['p-[calc((100vw-theme(maxWidth.2xl))_/_2)]', 'p-[calc((100vw-theme(maxWidth.2xl))/2)]'], + + // Keep spaces in strings + ['content-["hello_world"]', 'content-["hello_world"]'], + ['content-[____"hello_world"___]', 'content-["hello_world"]'], + + // Do not escape underscores for url() and CSS variable in var() + ['bg-[no-repeat_url(/image_13.png)]', 'bg-[no-repeat_url(/image_13.png)]'], + [ + 'bg-[var(--spacing-0_5,_var(--spacing-1_5,_3rem))]', + 'bg-(--spacing-0_5,var(--spacing-1_5,3rem))', + ], + + // Normalize whitespace in arbitrary properties + ['[display:flex]', '[display:flex]'], + ['[display:_flex]', '[display:flex]'], + ['[display:flex_]', '[display:flex]'], + ['[display:_flex_]', '[display:flex]'], + + // Normalize whitespace in `calc` expressions + ['w-[calc(100%-2rem)]', 'w-[calc(100%-2rem)]'], + ['w-[calc(100%_-_2rem)]', 'w-[calc(100%-2rem)]'], + + // Normalize the important modifier + ['!flex', 'flex!'], + ['flex!', 'flex!'], +] + +const variants = [ + ['', ''], // no variant + ['*:', '*:'], + ['focus:', 'focus:'], + ['group-focus:', 'group-focus:'], + + ['hover:focus:', 'hover:focus:'], + ['hover:group-focus:', 'hover:group-focus:'], + ['group-hover:focus:', 'group-hover:focus:'], + ['group-hover:group-focus:', 'group-hover:group-focus:'], + + ['min-[10px]:', 'min-[10px]:'], + + // Normalize spaces + ['min-[calc(1000px_+_12em)]:', 'min-[calc(1000px+12em)]:'], + ['min-[calc(1000px_+12em)]:', 'min-[calc(1000px+12em)]:'], + ['min-[calc(1000px+_12em)]:', 'min-[calc(1000px+12em)]:'], + ['min-[calc(1000px___+___12em)]:', 'min-[calc(1000px+12em)]:'], + + ['peer-[&_p]:', 'peer-[&_p]:'], + ['peer-[&_p]:hover:', 'peer-[&_p]:hover:'], + ['hover:peer-[&_p]:', 'hover:peer-[&_p]:'], + ['hover:peer-[&_p]:focus:', 'hover:peer-[&_p]:focus:'], + ['peer-[&:hover]:peer-[&_p]:', 'peer-[&:hover]:peer-[&_p]:'], + + ['[p]:', '[p]:'], + ['[_p_]:', '[p]:'], + ['has-[p]:', 'has-[p]:'], + ['has-[_p_]:', 'has-[p]:'], + + // Simplify `&:is(p)` to `p` + ['[&:is(p)]:', '[p]:'], + ['[&:is(_p_)]:', '[p]:'], + ['has-[&:is(p)]:', 'has-[p]:'], + ['has-[&:is(_p_)]:', 'has-[p]:'], + + // Handle special `@` variants. These shouldn't be printed as `@-` + ['@xl:', '@xl:'], + ['@[123px]:', '@[123px]:'], + + // Compound variants that forward modifiers + ['not-group-hover/name:', 'not-group-hover/name:'], + ['has-group-peer-hover/name:', 'has-group-peer-hover/name:'], + ['in-group-peer-hover/name:', 'in-group-peer-hover/name:'], +] + +let combinations: [string, string][] = [] + +for (let [inputVariant, outputVariant] of variants) { + for (let [inputCandidate, outputCandidate] of candidates) { + combinations.push([`${inputVariant}${inputCandidate}`, `${outputVariant}${outputCandidate}`]) + } +} + +describe('normalize candidates', () => { + test.each(combinations)('`%s` -> `%s`', async (candidate: string, result: string) => { + let designSystem = await __unstable__loadDesignSystem('@tailwind utilities', { + base: __dirname, + }) + + let [parsed] = designSystem.parseCandidate(candidate) + + expect(designSystem.printCandidate(parsed)).toEqual(result) + }) +}) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 406a16aad..a389af365 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -1,14 +1,19 @@ import type { DesignSystem } from './design-system' 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' const COLON = 0x3a const DASH = 0x2d const LOWER_A = 0x61 const LOWER_Z = 0x7a +const IS_VALID_NAMED_VALUE = /^[a-zA-Z0-9_.%-]+$/ -type ArbitraryUtilityValue = { +export type ArbitraryUtilityValue = { kind: 'arbitrary' /** @@ -60,7 +65,7 @@ export type NamedUtilityValue = { fraction: string | null } -type ArbitraryModifier = { +export type ArbitraryModifier = { kind: 'arbitrary' /** @@ -72,7 +77,7 @@ type ArbitraryModifier = { value: string } -type NamedModifier = { +export type NamedModifier = { kind: 'named' /** @@ -213,6 +218,102 @@ export type Candidate = raw: string } +export function cloneCandidate(candidate: T): T { + switch (candidate.kind) { + case 'arbitrary': + return { + kind: candidate.kind, + property: candidate.property, + value: candidate.value, + modifier: candidate.modifier + ? { kind: candidate.modifier.kind, value: candidate.modifier.value } + : null, + variants: candidate.variants.map(cloneVariant), + important: candidate.important, + raw: candidate.raw, + } satisfies Extract as T + + case 'static': + return { + kind: candidate.kind, + root: candidate.root, + variants: candidate.variants.map(cloneVariant), + important: candidate.important, + raw: candidate.raw, + } satisfies Extract as T + + case 'functional': + return { + kind: candidate.kind, + root: candidate.root, + value: candidate.value + ? candidate.value.kind === 'arbitrary' + ? { + kind: candidate.value.kind, + dataType: candidate.value.dataType, + value: candidate.value.value, + } + : { + kind: candidate.value.kind, + value: candidate.value.value, + fraction: candidate.value.fraction, + } + : null, + modifier: candidate.modifier + ? { kind: candidate.modifier.kind, value: candidate.modifier.value } + : null, + variants: candidate.variants.map(cloneVariant), + important: candidate.important, + raw: candidate.raw, + } satisfies Extract as T + + default: + candidate satisfies never + throw new Error('Unknown candidate kind') + } +} + +export function cloneVariant(variant: T): T { + switch (variant.kind) { + case 'arbitrary': + return { + kind: variant.kind, + selector: variant.selector, + relative: variant.relative, + } satisfies Extract as T + + case 'static': + return { kind: variant.kind, root: variant.root } satisfies Extract< + Variant, + { kind: 'static' } + > as T + + case 'functional': + return { + kind: variant.kind, + root: variant.root, + value: variant.value ? { kind: variant.value.kind, value: variant.value.value } : null, + modifier: variant.modifier + ? { kind: variant.modifier.kind, value: variant.modifier.value } + : null, + } satisfies Extract as T + + case 'compound': + return { + kind: variant.kind, + root: variant.root, + variant: cloneVariant(variant.variant), + modifier: variant.modifier + ? { kind: variant.modifier.kind, value: variant.modifier.value } + : null, + } satisfies Extract as T + + default: + variant satisfies never + throw new Error('Unknown variant kind') + } +} + export function* parseCandidate(input: string, designSystem: DesignSystem): Iterable { // hover:focus:underline // ^^^^^ ^^^^^^ -> Variants @@ -271,6 +372,14 @@ 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.: @@ -280,14 +389,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // ^^^^^^^^^^ -> Base without modifier // ^^ -> Modifier segment // ``` - 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 [baseWithoutModifier, modifierSegment = null] = parts let parsedModifier = modifierSegment === null ? null : parseModifier(modifierSegment) @@ -383,6 +485,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // // E.g.: // + // ``` // bg-(--my-var) // ^^ -> Root // ^^^^^^^^^^ -> Arbitrary value @@ -409,7 +512,10 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // An arbitrary value with `(…)` should always start with `--` since it // represents a CSS variable. - if (value[0] !== '-' && value[1] !== '-') return + if (value[0] !== '-' || value[1] !== '-') return + + // Values can't contain `;` or `}` characters at the top-level. + if (!isValidArbitrary(value)) return roots = [[root, dataType === null ? `[var(${value})]` : `[${dataType}:var(${value})]`]] } @@ -451,7 +557,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter if (!isValidArbitrary(arbitraryValue)) continue // Extract an explicit typehint if present, e.g. `bg-[color:var(--my-var)])` - let typehint = '' + let typehint: string | null = null for (let i = 0; i < arbitraryValue.length; i++) { let code = arbitraryValue.charCodeAt(i) @@ -477,6 +583,8 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter continue } + if (typehint === '') continue + candidate.value = { kind: 'arbitrary', dataType: typehint || null, @@ -491,6 +599,8 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter ? null : `${value}/${modifierSegment}` + if (!IS_VALID_NAMED_VALUE.test(value)) continue + candidate.value = { kind: 'named', value, @@ -521,24 +631,29 @@ function parseModifier(modifier: string): CandidateModifier | null { } if (modifier[0] === '(' && modifier[modifier.length - 1] === ')') { - let arbitraryValue = decodeArbitraryValue(modifier.slice(1, -1)) + // Drop the `(` and `)` characters + modifier = modifier.slice(1, -1) + + // A modifier with `(…)` should always start with `--` since it + // represents a CSS variable. + if (modifier[0] !== '-' || modifier[1] !== '-') return null // Values can't contain `;` or `}` characters at the top-level. - if (!isValidArbitrary(arbitraryValue)) return null + if (!isValidArbitrary(modifier)) return null - // Empty arbitrary values are invalid. E.g.: `data-():` - // ^^ - if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) return null + // Wrap the value in `var(…)` to ensure that it is a valid CSS variable. + modifier = `var(${modifier})` - // Arbitrary values must start with `--` since it represents a CSS variable. - if (arbitraryValue[0] !== '-' && arbitraryValue[1] !== '-') return null + let arbitraryValue = decodeArbitraryValue(modifier) return { kind: 'arbitrary', - value: `var(${arbitraryValue})`, + value: arbitraryValue, } } + if (!IS_VALID_NAMED_VALUE.test(modifier)) return null + return { kind: 'named', value: modifier, @@ -593,17 +708,18 @@ 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` - if (additionalModifier) return null + let parts = segment(variant, '/') + if (parts.length > 2) return null + + // group-hover/group-name + // ^^^^^^^^^^^ -> Variant without modifier + // ^^^^^^^^^^ -> Modifier + let [variantWithoutModifier, modifier = null] = parts let roots = findRoots(variantWithoutModifier, (root) => { return designSystem.variants.has(root) @@ -677,7 +793,7 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) return null // Arbitrary values must start with `--` since it represents a CSS variable. - if (arbitraryValue[0] !== '-' && arbitraryValue[1] !== '-') return null + if (arbitraryValue[0] !== '-' || arbitraryValue[1] !== '-') return null return { kind: 'functional', @@ -690,6 +806,8 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia } } + if (!IS_VALID_NAMED_VALUE.test(value)) continue + return { kind: 'functional', root, @@ -701,6 +819,13 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia case 'compound': { if (value === null) return null + // Forward the modifier of the compound variants to its subVariant. + // This allows for `not-group-hover/name:flex` to work. + if (modifier && (root === 'not' || root === 'has' || root === 'in')) { + value = `${value}/${modifier}` + modifier = null + } + let subVariant = designSystem.parseVariant(value) if (subVariant === null) return null @@ -745,14 +870,6 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable // Otherwise test every permutation of the input by iteratively removing // everything after the last dash. let idx = input.lastIndexOf('-') - if (idx === -1) { - // Variants starting with `@` are special because they don't need a `-` - // after the `@` (E.g.: `@-lg` should be written as `@lg`). - if (input[0] === '@' && exists('@')) { - yield ['@', input.slice(1)] - } - return - } // Determine the root and value by testing permutations of the incoming input. // @@ -761,7 +878,7 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable // `bg-red-500` -> No match // `bg-red` -> No match // `bg` -> Match - do { + while (idx > 0) { let maybeRoot = input.slice(0, idx) if (exists(maybeRoot)) { @@ -772,9 +889,351 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable // can skip any further parsing. if (root[1] === '') break + // Edge case: `@-…` is not valid as a variant or a utility so we want to + // skip if an `@` is followed by a `-`. Otherwise `@-2xl:flex` and + // `@-2xl:flex` would be considered the same. + if (root[0] === '@' && exists('@') && input[idx] === '-') break + yield root } idx = input.lastIndexOf('-', idx - 1) - } while (idx > 0) + } + + // Try '@' variant after permutations. This allows things like `@max` of `@max-foo-bar` + // to match before looking for `@`. + if (input[0] === '@' && exists('@')) { + yield ['@', input.slice(1)] + } +} + +export function printCandidate(designSystem: DesignSystem, candidate: Candidate) { + let parts: string[] = [] + + for (let variant of candidate.variants) { + parts.unshift(printVariant(variant)) + } + + // Handle prefix + if (designSystem.theme.prefix) { + parts.unshift(designSystem.theme.prefix) + } + + let base: string = '' + + // Handle static + if (candidate.kind === 'static') { + base += candidate.root + } + + // Handle functional + if (candidate.kind === 'functional') { + base += candidate.root + + if (candidate.value) { + if (candidate.value.kind === 'arbitrary') { + if (candidate.value !== null) { + let isVarValue = isVar(candidate.value.value) + let value = isVarValue ? candidate.value.value.slice(4, -1) : candidate.value.value + let [open, close] = isVarValue ? ['(', ')'] : ['[', ']'] + + if (candidate.value.dataType) { + base += `-${open}${candidate.value.dataType}:${printArbitraryValue(value)}${close}` + } else { + base += `-${open}${printArbitraryValue(value)}${close}` + } + } + } else if (candidate.value.kind === 'named') { + base += `-${candidate.value.value}` + } + } + } + + // Handle arbitrary + if (candidate.kind === 'arbitrary') { + base += `[${candidate.property}:${printArbitraryValue(candidate.value)}]` + } + + // Handle modifier + if (candidate.kind === 'arbitrary' || candidate.kind === 'functional') { + base += printModifier(candidate.modifier) + } + + // Handle important + if (candidate.important) { + base += '!' + } + + parts.push(base) + + return parts.join(':') +} + +export function printModifier(modifier: ArbitraryModifier | NamedModifier | null) { + if (modifier === null) return '' + + let isVarValue = isVar(modifier.value) + let value = isVarValue ? modifier.value.slice(4, -1) : modifier.value + let [open, close] = isVarValue ? ['(', ')'] : ['[', ']'] + + if (modifier.kind === 'arbitrary') { + return `/${open}${printArbitraryValue(value)}${close}` + } else if (modifier.kind === 'named') { + return `/${modifier.value}` + } else { + modifier satisfies never + return '' + } +} + +export function printVariant(variant: Variant) { + // Handle static variants + if (variant.kind === 'static') { + return variant.root + } + + // Handle arbitrary variants + if (variant.kind === 'arbitrary') { + return `[${printArbitraryValue(simplifyArbitraryVariant(variant.selector))}]` + } + + let base: string = '' + + // Handle functional variants + if (variant.kind === 'functional') { + base += variant.root + // `@` is a special case for functional variants. We want to print: `@lg` + // instead of `@-lg` + let hasDash = variant.root !== '@' + if (variant.value) { + if (variant.value.kind === 'arbitrary') { + let isVarValue = isVar(variant.value.value) + let value = isVarValue ? variant.value.value.slice(4, -1) : variant.value.value + let [open, close] = isVarValue ? ['(', ')'] : ['[', ']'] + + base += `${hasDash ? '-' : ''}${open}${printArbitraryValue(value)}${close}` + } else if (variant.value.kind === 'named') { + base += `${hasDash ? '-' : ''}${variant.value.value}` + } + } + } + + // Handle compound variants + if (variant.kind === 'compound') { + base += variant.root + base += '-' + base += printVariant(variant.variant) + } + + // Handle modifiers + if (variant.kind === 'functional' || variant.kind === 'compound') { + base += printModifier(variant.modifier) + } + + return base +} + +const printArbitraryValueCache = new DefaultMap((input) => { + let ast = ValueParser.parse(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) => { + // Handle operators (e.g.: inside of `calc(…)`) + if (node.kind === 'word' && symbols.has(node.value)) { + let idx = ctx.index + + // This should not be possible + if (idx === -1) return + + // a + b + // ^ node + // ^ previous (whitespace) + let previous = ctx.siblings[idx - 1] + if (previous?.kind !== 'separator' || previous.value !== ' ') return + + // a + b + // ^ node + // ^ next (whitespace) + let next = ctx.siblings[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) { + drop.add(node) + } + } + + // Whitespace around `,` separators can be removed. + // E.g.: `min(1px , 2px)` -> `min(1px,2px)` + 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) { + walk(ast, (node) => { + if (drop.has(node)) { + drop.delete(node) + return WalkAction.ReplaceSkip([]) + } + }) + } + + recursivelyEscapeUnderscores(ast) + + return ValueParser.toCss(ast) +}) +export function printArbitraryValue(input: string) { + return printArbitraryValueCache.get(input) +} + +const simplifyArbitraryVariantCache = new DefaultMap((input) => { + let ast = ValueParser.parse(input) + + // &:is(…) + if ( + ast.length === 3 && + // & + ast[0].kind === 'word' && + ast[0].value === '&' && + // : + ast[1].kind === 'separator' && + ast[1].value === ':' && + // is(…) + ast[2].kind === 'function' && + ast[2].value === 'is' + ) { + return ValueParser.toCss(ast[2].nodes) + } + + return input +}) +function simplifyArbitraryVariant(input: string) { + return simplifyArbitraryVariantCache.get(input) +} + +function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) { + for (let node of ast) { + switch (node.kind) { + case 'function': { + if (node.value === 'url' || node.value.endsWith('_url')) { + // Don't decode underscores in url() but do decode the function name + node.value = escapeUnderscore(node.value) + break + } + + if ( + node.value === 'var' || + node.value.endsWith('_var') || + node.value === 'theme' || + node.value.endsWith('_theme') + ) { + node.value = escapeUnderscore(node.value) + for (let i = 0; i < node.nodes.length; i++) { + recursivelyEscapeUnderscores([node.nodes[i]]) + } + break + } + + node.value = escapeUnderscore(node.value) + recursivelyEscapeUnderscores(node.nodes) + break + } + case 'separator': + node.value = escapeUnderscore(node.value) + break + case 'word': { + // Dashed idents and variables `var(--my-var)` and `--my-var` should not + // have underscores escaped + if (node.value[0] !== '-' || node.value[1] !== '-') { + node.value = escapeUnderscore(node.value) + } + break + } + default: + never(node) + } + } +} + +const isVarCache = new DefaultMap((value) => { + let ast = ValueParser.parse(value) + return ast.length === 1 && ast[0].kind === 'function' && ast[0].value === 'var' +}) +function isVar(value: string) { + return isVarCache.get(value) +} + +function never(value: never): never { + throw new Error(`Unexpected value: ${value}`) +} + +function escapeUnderscore(value: string): string { + return value + .replaceAll('_', String.raw`\_`) // Escape underscores to keep them as-is + .replaceAll(' ', '_') // Replace spaces with underscores } diff --git a/packages/tailwindcss/src/canonicalize-calc-expressions.test.ts b/packages/tailwindcss/src/canonicalize-calc-expressions.test.ts new file mode 100644 index 000000000..f612e5f7b --- /dev/null +++ b/packages/tailwindcss/src/canonicalize-calc-expressions.test.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 000000000..7e08f8622 --- /dev/null +++ b/packages/tailwindcss/src/canonicalize-calc-expressions.ts @@ -0,0 +1,82 @@ +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 new file mode 100644 index 000000000..567f0ad0d --- /dev/null +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -0,0 +1,1663 @@ +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) => { + return new DefaultMap((input: string) => { + return __unstable__loadDesignSystem(input, { + base, + async loadStylesheet() { + return { + path: '', + base: '', + content: css` + @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; + } + `, + } + }, + }) + }) +}) + +const DEFAULT_CANONICALIZATION_OPTIONS: CanonicalizeOptions = { + rem: 16, + collapse: true, + logicalToPhysical: true, +} + +describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', (strategy) => { + let testName = '%s → %s (%#)' + if (strategy === 'with-variant') { + testName = testName.replaceAll('%s', 'focus:%s') + } else if (strategy === 'important') { + testName = testName.replaceAll('%s', '%s!') + } else if (strategy === 'prefix') { + testName = testName.replaceAll('%s', 'tw:%s') + } + + function prepare(candidate: string) { + if (strategy === 'with-variant') { + candidate = `focus:${candidate}` + } else if (strategy === 'important') { + candidate = `${candidate}!` + } else if (strategy === 'prefix') { + candidate = `tw:${candidate}` + + // Prefix all known CSS variables with `--tw-`, except when used inside of `--theme(…)`. + if (candidate.includes('--')) { + candidate = candidate + .replace( + // Replace the variable, as long as it is preceded by a `(`, e.g.: + // `bg-(--foo)` or an `:` in case of `bg-(color:--foo)`. + // + // It also has to end in a `,` or `)` to prevent replacing functions + // that look like variables, e.g.: `--spacing(…)` + /([(:])--([\w-]+)([,)])/g, + (_, start, variable, end) => `${start}--tw-${variable}${end}`, + ) + .replaceAll('--theme(--tw-', '--theme(--') + } + } + + return candidate + } + + async function expectCanonicalization( + input: string, + candidates: string, + expected: string, + options: CanonicalizeOptions = DEFAULT_CANONICALIZATION_OPTIONS, + ) { + let preparedCandidates = candidates.split(/\s+/g).map(prepare) + let preparedExpected = expected.split(/\s+/g).map(prepare) + + if (strategy === 'prefix') { + input = input.replace("@import 'tailwindcss';", "@import 'tailwindcss' prefix(tw);") + } + + let designSystem = await designSystems.get(__dirname).get(input) + let actual = designSystem.canonicalizeCandidates(preparedCandidates, options) + + try { + expect(actual).toEqual(preparedExpected) + } catch (err) { + if (err instanceof Error) Error.captureStackTrace(err, expectCanonicalization) + 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` + @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) + }, + ) + }) + }) + + 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) + }) + + test.each([ + // Arbitrary property to static utility + ['[text-wrap:balance]', 'text-balance'], + + // Arbitrary property to static utility with slight differences in + // whitespace. This will require some canonicalization. + ['[display:_flex_]', 'flex'], + ['[display:_flex]', 'flex'], + ['[display:flex_]', 'flex'], + + // 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'], + + // Arbitrary property with arbitrary modifier to named functional utility with + // arbitrary modifier + ['[color:var(--color-red-500)]/[25%]', 'text-red-500/25'], + ['[color:var(--color-red-500)]/[100%]', 'text-red-500'], + ['[color:var(--color-red-500)]/100', 'text-red-500'], + ['[color:var(--color-red-500)]/[10%]', 'text-red-500/10'], + ['[color:var(--color-red-500)]/[10.0%]', 'text-red-500/10'], + ['[color:var(--color-red-500)]/[.1]', 'text-red-500/10'], + ['[color:var(--color-red-500)]/[.10]', 'text-red-500/10'], + // No need for `/50` because that's already encoded in the `--color-primary` + // value + ['[color:oklch(62.3%_0.214_259.815)]/50', 'text-primary'], + + // Arbitrary property to arbitrary value + ['[max-height:20%]', 'max-h-[20%]'], + + // Arbitrary property to bare value + ['[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'], + + // Arbitrary value with data type, to more specific arbitrary value + ['bg-[position:123px]', 'bg-position-[123px]'], + ['bg-[size:123px]', 'bg-size-[123px]'], + + // Arbitrary value with inferred data type, to more specific arbitrary value + ['bg-[123px]', 'bg-position-[123px]'], + + // Arbitrary value with spacing mul + ['w-[64rem]', 'w-256'], + + // 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) => { + let input = css` + @import 'tailwindcss'; + @theme { + --color-white: #fff; + } + ` + + await expectCanonicalization(input, candidate, expected) + }) + }) + + describe('arbitrary utilities', () => { + test('migrate with custom static utility `@utility custom {…}`', { timeout }, async () => { + let candidate = '[--key:value]' + let expected = 'custom' + + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + @utility custom { + --key: value; + } + ` + + await expectCanonicalization(input, candidate, expected) + }) + + test( + 'migrate with custom functional utility `@utility custom-* {…}`', + { timeout }, + async () => { + let candidate = '[--key:value]' + let expected = 'custom-value' + + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + @utility custom-* { + --key: --value('value'); + } + ` + + 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' + + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + @utility example-* { + --resolved-value: --value(integer); + } + ` + + 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'], + ])( + 'migrate custom @utility from arbitrary values to bare values and named values (based on theme)', + async (candidate, expected) => { + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + --example-a: 8; + } + + @utility example-* { + --resolved-value: --value(--example, integer, [integer]); + } + ` + + await expectCanonicalization(input, candidate, expected) + }, + ) + + describe.each([['@theme'], ['@theme inline']])('%s', (theme) => { + test.each([ + ['[color:CanvasText]', 'text-canvas'], + ['text-[CanvasText]', 'text-canvas'], + ])(`migrate arbitrary value to theme value ${testName}`, async (candidate, expected) => { + let input = css` + @import 'tailwindcss'; + ${theme} { + --*: initial; + --color-canvas: CanvasText; + } + ` + + await expectCanonicalization(input, candidate, expected) + }) + + // Some utilities read from specific namespaces, in this case we do not want + // to migrate to a value in that namespace if we reference a variable that + // results in the same value, but comes from a different namespace. + // + // E.g.: `max-w` reads from: ['--max-width', '--spacing', '--container'] + test.each([ + // `max-w` does not read from `--breakpoint-md`, but `--breakpoint-md` and + // `--container-3xl` happen to result in the same value. The difference is + // 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' + 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'], + + // Keep arbitrary value if bare value doesn't fit in steps of .25 + ['w-[0.123rem]', 'w-[0.123rem]', '0.25rem'], + + // Custom pixel based spacing scale + ['w-[123px]', 'w-123', '1px'], + ['w-[256px]', 'w-128', '2px'], + ])(`${testName} (spacing = \`%s\`)`, { timeout }, async (candidate, expected, spacing) => { + let input = css` + @import 'tailwindcss'; + + @theme { + --*: initial; + --spacing: ${spacing}; + } + ` + + await expectCanonicalization(input, candidate, expected) + }) + }) + + describe('bare values', () => { + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + --spacing: 0.25rem; + --aspect-video: 16 / 9; + --example-a: 8; + } + + @utility example-* { + --resolved-value: --value(--example, integer); + } + ` + + test.each([ + // Built-in utility with bare value fraction + ['aspect-16/9', 'aspect-video'], + + // Custom utility with bare value integer + ['example-8', 'example-a'], + ])(testName, { timeout }, async (candidate, expected) => { + await expectCanonicalization(input, candidate, expected) + }) + }) + + describe('arbitrary variants', () => { + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + --breakpoint-lg: 64rem; + } + ` + + test.each([ + // Arbitrary variant to static variant + ['[&:focus]:flex', 'focus:flex'], + + // Arbitrary variant to static variant with at-rules + ['[@media(scripting:_none)]:flex', 'noscript:flex'], + + // Arbitrary variant to static utility at-rules and with slight differences + // in whitespace. This will require some canonicalization. + ['[@media(scripting:none)]:flex', 'noscript:flex'], + ['[@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 { + await expectCanonicalization(input, candidate, expected) + }) + + test('unsafe migrations keep the candidate as-is', { timeout }, 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' + let expected = '[&:hover]:flex' + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + ` + + await expectCanonicalization(input, candidate, expected) + }) + + test('make unsafe migration safe (1)', { timeout }, async () => { + // Overriding the `hover:` variant to only use a selector will make the + // migration safe. + let candidate = '[&:hover]:flex' + let expected = 'hover:flex' + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + @variant hover (&:hover); + ` + + await expectCanonicalization(input, candidate, expected) + }) + + test('make unsafe migration safe (2)', { timeout }, 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' + let expected = 'hover:flex' + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + @variant hover { + &:hover { + @slot; + } + } + ` + + await expectCanonicalization(input, candidate, expected) + }) + + test('custom selector-based variants', { timeout }, async () => { + let candidate = '[&.macos]:flex' + let expected = 'is-macos:flex' + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + @variant is-macos (&.macos); + ` + + await expectCanonicalization(input, candidate, expected) + }) + + test('custom @media-based variants', { timeout }, async () => { + let candidate = '[@media(prefers-reduced-transparency:reduce)]:flex' + let expected = 'transparency-safe:flex' + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + } + @variant transparency-safe { + @media (prefers-reduced-transparency: reduce) { + @slot; + } + } + ` + + await expectCanonicalization(input, candidate, expected) + }) + }) + + describe('drop unnecessary data types', () => { + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + --color-red-500: red; + } + ` + + test.each([ + // A color value can be inferred from the value + ['bg-[color:#008cc]', 'bg-[#008cc]'], + + // A color is the default for `bg-*` + ['bg-(color:--my-value)', 'bg-(--my-value)'], + + // 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) => { + await expectCanonicalization(input, candidate, expected) + }) + }) + + describe('arbitrary value to bare value', () => { + test.each([ + ['aspect-[12/34]', 'aspect-12/34'], + ['aspect-[1.2/34]', 'aspect-[1.2/34]'], + ['col-start-[7]', 'col-start-7'], + ['flex-[2]', 'flex-2'], // `flex` is implemented as static and functional utilities + + ['grid-cols-[subgrid]', 'grid-cols-subgrid'], + ['grid-rows-[subgrid]', 'grid-rows-subgrid'], + + // Handle zeroes + ['m-[0]', 'm-0'], + ['m-[0px]', 'm-0'], + ['m-[0rem]', 'm-0'], + + ['-m-[0]', 'm-0'], + ['-m-[0px]', 'm-0'], + ['-m-[0rem]', 'm-0'], + + ['m-[-0]', 'm-0'], + ['m-[-0px]', 'm-0'], + ['m-[-0rem]', 'm-0'], + + ['-m-[-0]', 'm-0'], + ['-m-[-0px]', 'm-0'], + ['-m-[-0rem]', 'm-0'], + + ['[margin:0]', 'm-0'], + ['[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%]'], + + // Only 50-200% (inclusive) are valid: + // https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch#percentage + ['font-stretch-[50%]', 'font-stretch-50%'], + ['font-stretch-[50.5%]', 'font-stretch-[50.5%]'], + ['font-stretch-[201%]', 'font-stretch-[201%]'], + ['font-stretch-[49%]', 'font-stretch-[49%]'], + // Should stay as-is + ['font-stretch-[1/2]', 'font-stretch-[1/2]'], + + // Bare value with % is valid for these utilities + ['from-[28%]', 'from-28%'], + ['via-[28%]', 'via-28%'], + ['to-[28%]', 'to-28%'], + ['from-[28.5%]', 'from-[28.5%]'], + ['via-[28.5%]', 'via-[28.5%]'], + ['to-[28.5%]', 'to-[28.5%]'], + + // This test in itself is a bit flawed because `text-[1/2]` currently + // generates something. Converting it to `text-1/2` doesn't produce anything. + ['text-[1/2]', 'text-[1/2]'], + + // Leading is special, because `leading-[123]` is the direct value of 123, but + // `leading-123` maps to `calc(--spacing(123))`. + ['leading-[123]', 'leading-[123]'], + + ['data-[selected]:flex', 'data-selected:flex'], + ['data-[foo=bar]:flex', 'data-[foo=bar]:flex'], + + ['supports-[gap]:flex', 'supports-gap:flex'], + ['supports-[display:grid]:flex', 'supports-[display:grid]:flex'], + + ['group-data-[selected]:flex', 'group-data-selected:flex'], + ['group-data-[foo=bar]:flex', 'group-data-[foo=bar]:flex'], + ['group-has-data-[selected]:flex', 'group-has-data-selected:flex'], + + ['aria-[selected]:flex', 'aria-[selected]:flex'], + ['aria-[selected="true"]:flex', 'aria-selected:flex'], + ['aria-[selected*="true"]:flex', 'aria-[selected*="true"]:flex'], + + ['group-aria-[selected]:flex', 'group-aria-[selected]:flex'], + ['group-aria-[selected="true"]:flex', 'group-aria-selected:flex'], + ['group-has-aria-[selected]:flex', 'group-has-aria-[selected]:flex'], + + ['max-lg:hover:data-[selected]:flex', 'max-lg:hover:data-selected:flex'], + [ + 'data-[selected]:aria-[selected="true"]:aspect-[12/34]', + 'data-selected:aria-selected:aspect-12/34', + ], + ])(testName, { timeout }, async (candidate, expected) => { + let input = css` + @import 'tailwindcss'; + ` + await expectCanonicalization(input, candidate, expected) + }) + }) + + // 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 + ['[[data-visible]]:flex', 'data-visible:flex'], + ['[&[data-visible]]:flex', 'data-visible:flex'], + ['[[data-visible]&]:flex', 'data-visible:flex'], + ['[&>[data-visible]]:flex', '*:data-visible:flex'], + ['[&_>_[data-visible]]:flex', '*:data-visible:flex'], + ['[&>*]:flex', '*:flex'], + ['[&_>_*]:flex', '*:flex'], + ['[&_>_[foo]]:flex', '*:[[foo]]:flex'], + ['[&_[foo]]:flex', '**:[[foo]]:flex'], + ['[&_>_[foo=bar]]:flex', '*:[[foo=bar]]:flex'], + ['[&_[foo=bar]]:flex', '**:[[foo=bar]]:flex'], + + ['[&_[data-visible]]:flex', '**:data-visible:flex'], + ['[&_*]:flex', '**:flex'], + + ['[&:first-child]:flex', 'first:flex'], + ['[&:not(:first-child)]:flex', 'not-first:flex'], + + ['[&_:first-child]:flex', '**:first:flex'], + ['[&_>_:first-child]:flex', '*:first:flex'], + ['[&_:--custom]:flex', '**:[:--custom]:flex'], + ['[&_>_:--custom]:flex', '*:[:--custom]:flex'], + + // in-* variants + ['[p_&]:flex', 'in-[p]:flex'], + ['[.foo_&]:flex', 'in-[.foo]:flex'], + ['[[data-visible]_&]:flex', 'in-data-visible:flex'], + // Multiple selectors, should stay as-is + ['[[data-foo][data-bar]_&]:flex', '[[data-foo][data-bar]_&]:flex'], + // Using `>` instead of ` ` should not be transformed: + ['[figure>&]:my-0', '[figure>&]:my-0'], + + // nth-child + ['[&:nth-child(2)]:flex', 'nth-2:flex'], + ['[&:not(:nth-child(2))]:flex', 'not-nth-2:flex'], + + ['[&:nth-child(-n+3)]:flex', 'nth-[-n+3]:flex'], + ['[&:not(:nth-child(-n+3))]:flex', 'not-nth-[-n+3]:flex'], + ['[&:nth-child(-n_+_3)]:flex', 'nth-[-n+3]:flex'], + ['[&:not(:nth-child(-n_+_3))]:flex', 'not-nth-[-n+3]:flex'], + + // nth-last-child + ['[&:nth-last-child(2)]:flex', 'nth-last-2:flex'], + ['[&:not(:nth-last-child(2))]:flex', 'not-nth-last-2:flex'], + + ['[&:nth-last-child(-n+3)]:flex', 'nth-last-[-n+3]:flex'], + ['[&:not(:nth-last-child(-n+3))]:flex', 'not-nth-last-[-n+3]:flex'], + ['[&:nth-last-child(-n_+_3)]:flex', 'nth-last-[-n+3]:flex'], + ['[&:not(:nth-last-child(-n_+_3))]:flex', 'not-nth-last-[-n+3]:flex'], + + // nth-child odd/even + ['[&:nth-child(odd)]:flex', 'odd:flex'], + ['[&:not(:nth-child(odd))]:flex', 'even:flex'], + ['[&:nth-child(even)]:flex', 'even:flex'], + ['[&:not(:nth-child(even))]:flex', 'odd:flex'], + + // Keep multiple attribute selectors as-is + ['[[data-visible][data-dark]]:flex', '[[data-visible][data-dark]]:flex'], + + // Keep `:where(…)` as is + ['[:where([data-visible])]:flex', '[:where([data-visible])]:flex'], + + // Complex attribute selectors with operators, quotes and insensitivity flags + ['[[data-url*="example"]]:flex', 'data-[url*="example"]:flex'], + ['[[data-url$=".com"_i]]:flex', 'data-[url$=".com"_i]:flex'], + ['[[data-url$=.com_i]]:flex', 'data-[url$=.com_i]:flex'], + + // Attribute selector wrapped in `&:is(…)` + ['[&:is([data-visible])]:flex', 'data-visible:flex'], + + // Media queries + ['[@media(pointer:fine)]:flex', 'pointer-fine:flex'], + ['[@media_(pointer_:_fine)]:flex', 'pointer-fine:flex'], + ['[@media_not_(pointer_:_fine)]:flex', 'not-pointer-fine:flex'], + ['[@media_print]:flex', 'print:flex'], + ['[@media_not_print]:flex', 'not-print:flex'], + + // Hoist the `:not` part to a compound variant + ['[@media_not_(prefers-color-scheme:dark)]:flex', 'not-dark:flex'], + [ + '[@media_not_(prefers-color-scheme:unknown)]:flex', + 'not-[@media_(prefers-color-scheme:unknown)]:flex', + ], + + // Compound arbitrary variants + ['has-[[data-visible]]:flex', 'has-data-visible:flex'], + ['has-[&:is([data-visible])]:flex', 'has-data-visible:flex'], + ['has-[&>[data-visible]]:flex', 'has-[&>[data-visible]]:flex'], + + ['has-[[data-slot=description]]:flex', 'has-data-[slot=description]:flex'], + ['has-[&:is([data-slot=description])]:flex', 'has-data-[slot=description]:flex'], + + ['has-[[aria-visible="true"]]:flex', 'has-aria-visible:flex'], + ['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) => { + let input = css` + @import 'tailwindcss'; + ` + await expectCanonicalization(input, candidate, expected) + }) + }) + + describe('optimize modifier', () => { + let input = css` + @import 'tailwindcss'; + @theme { + --*: initial; + --color-red-500: red; + } + ` + + test.each([ + // Keep the modifier as-is, nothing to optimize + ['bg-red-500/25', 'bg-red-500/25'], + + // Use a bare value modifier + ['bg-red-500/[25%]', 'bg-red-500/25'], + + // Convert 0-1 values to bare values + ['bg-[#f00]/[0.16]', 'bg-[#f00]/16'], + + // Drop unnecessary modifiers + ['bg-red-500/[100%]', 'bg-red-500'], + ['bg-red-500/100', 'bg-red-500'], + + // Keep modifiers on classes that don't _really_ exist + ['group/name', 'group/name'], + ])(testName, { timeout }, 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'], + + // 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'], + + // Different order as above + ['mb-1 mt-1', 'my-1'], + + // 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'], + + // Collapse duplicates into themselves + ['w-8 w-8', 'w-8'], + + // `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) => { + 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) + }) +}) + +describe('theme to var', () => { + test('extended space scale converts to var or calc', { timeout }, async () => { + let designSystem = await __unstable__loadDesignSystem( + css` + @tailwind utilities; + @theme { + --spacing: 0.25rem; + --spacing-2: 2px; + --spacing-miami: 0.875rem; + } + `, + { base: __dirname }, + ) + expect( + designSystem.canonicalizeCandidates([ + '[--value:theme(spacing.1)]', + '[--value:theme(spacing.2)]', + '[--value:theme(spacing.miami)]', + '[--value:theme(spacing.nyc)]', + ]), + ).toEqual([ + '[--value:--spacing(1)]', + '[--value:var(--spacing-2)]', + '[--value:var(--spacing-miami)]', + '[--value:theme(spacing.nyc)]', + ]) + }) + + test('custom space scale converts to var', { timeout }, async () => { + let designSystem = await __unstable__loadDesignSystem( + css` + @tailwind utilities; + @theme { + --spacing-*: initial; + --spacing-1: 0.25rem; + --spacing-2: 0.5rem; + } + `, + { base: __dirname }, + ) + expect( + designSystem.canonicalizeCandidates([ + '[--value:theme(spacing.1)]', + '[--value:theme(spacing.2)]', + '[--value:theme(spacing.3)]', + ]), + ).toEqual([ + '[--value:var(--spacing-1)]', + '[--value:var(--spacing-2)]', + '[--value:theme(spacing.3)]', + ]) + }) +}) + +describe('options', () => { + test('normalize `rem` units to `px`', { timeout }, async () => { + let designSystem = await __unstable__loadDesignSystem( + css` + @tailwind utilities; + @theme { + --spacing: 0.25rem; + } + `, + { base: __dirname }, + ) + + expect(designSystem.canonicalizeCandidates(['m-[16px]'])).toEqual(['m-[16px]']) + expect(designSystem.canonicalizeCandidates(['m-[16px]'], { rem: 16 })).toEqual(['m-4']) + expect(designSystem.canonicalizeCandidates(['m-[16px]'], { rem: 64 })).toEqual(['m-1']) + 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 new file mode 100644 index 000000000..d1dfc09de --- /dev/null +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -0,0 +1,3062 @@ +import { substituteAtApply } from './apply' +import { atRule, cloneAstNode, styleRule, toCss, type AstNode } from './ast' +import * as AttributeSelectorParser from './attribute-selector-parser' +import { + cloneCandidate, + cloneVariant, + printArbitraryValue, + printModifier, + type Candidate, + type CandidateModifier, + 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 type { DesignSystem as BaseDesignSystem } from './design-system' +import { CompileAstFlags } from './design-system' +import { expandDeclaration } from './expand-declaration' +import * as SelectorParser from './selector-parser' +import { ThemeOptions } from './theme' +import type { Writable } from './types' +import { DefaultMap } from './utils/default-map' +import { dimensions } from './utils/dimensions' +import { isPositiveInteger, isValidSpacingMultiplier } from './utils/infer-data-type' +import { replaceObject } from './utils/replace-object' +import { segment } from './utils/segment' +import { toKeyPath } from './utils/to-key-path' +import * as ValueParser from './value-parser' +import { walk, WalkAction } from './walk' + +export interface CanonicalizeOptions { + /** + * The root font size in pixels. If provided, `rem` values will be normalized + * to `px` values. + * + * E.g.: `mt-[16px]` with `rem: 16` will become `mt-4` (assuming `--spacing: 0.25rem`). + */ + rem?: number + + /** + * Whether to collapse multiple utilities into a single utility if possible. + * + * E.g.: `mt-2 mr-2 mb-2 ml-2` → `m-2` + */ + collapse?: boolean + + /** + * Whether to convert between logical and physical properties when collapsing + * utilities. + * + * E.g.: `mr-2 ml-2` → `mx-2` + */ + logicalToPhysical?: boolean +} + +enum Features { + /** + * No features enabled (default) + */ + None = 0, + + /** + * Collapse multiple utilities into a single utility if possible. + */ + CollapseUtilities = 1 << 0, +} + +interface InternalCanonicalizeOptions { + features: Features + designSystem: DesignSystem + 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 + > + [INTERNAL_OPTIONS_KEY]: DefaultMap< + SignatureOptions, + DefaultMap + > + [CANONICALIZE_CANDIDATE_KEY]: DefaultMap< + InternalCanonicalizeOptions, + DefaultMap + > + [CANONICALIZE_VARIANT_KEY]: DefaultMap< + InternalCanonicalizeOptions, + DefaultMap + > + [CANONICALIZE_UTILITY_KEY]: DefaultMap> + [CONVERTER_KEY]: (input: string, options?: Convert) => [string, CandidateModifier | null] + [SPACING_KEY]: DefaultMap | null + [UTILITY_SIGNATURE_KEY]: DefaultMap> + [STATIC_UTILITIES_KEY]: DefaultMap< + SignatureOptions, + DefaultMap>> + > + [UTILITY_PROPERTIES_KEY]: DefaultMap< + SignatureOptions, + DefaultMap>> + > + [PRE_COMPUTED_UTILITIES_KEY]: DefaultMap> + [VARIANT_SIGNATURE_KEY]: DefaultMap> + [PRE_COMPUTED_VARIANTS_KEY]: DefaultMap> + } +} + +export function prepareDesignSystemStorage( + baseDesignSystem: BaseDesignSystem, + options?: CanonicalizeOptions, +): DesignSystem { + let designSystem = baseDesignSystem as DesignSystem + + designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache() + designSystem.storage[INTERNAL_OPTIONS_KEY] ??= createInternalOptionsCache(designSystem) + designSystem.storage[CANONICALIZE_CANDIDATE_KEY] ??= createCanonicalizeCandidateCache() + 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[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 +} + +const SIGNATURE_OPTIONS_KEY = Symbol() +function createSignatureOptionsCache(): DesignSystem['storage'][typeof SIGNATURE_OPTIONS_KEY] { + return new DefaultMap((rem: number | null) => { + return new DefaultMap((features: SignatureFeatures) => { + return { rem, features } satisfies SignatureOptions + }) + }) +} + +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, +): SignatureOptions { + let features = SignatureFeatures.None + if (options?.collapse) features |= SignatureFeatures.ExpandProperties + if (options?.logicalToPhysical) features |= SignatureFeatures.LogicalToPhysical + + let designSystem = prepareDesignSystemStorage(baseDesignSystem, options) + + return designSystem.storage[SIGNATURE_OPTIONS_KEY].get(options?.rem ?? null).get(features) +} + +const INTERNAL_OPTIONS_KEY = Symbol() +function createInternalOptionsCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof INTERNAL_OPTIONS_KEY] { + return new DefaultMap((signatureOptions: SignatureOptions) => { + return new DefaultMap((features: Features) => { + return { features, designSystem, signatureOptions } satisfies InternalCanonicalizeOptions + }) + }) +} + +function createCanonicalizeOptions( + baseDesignSystem: BaseDesignSystem, + signatureOptions: SignatureOptions, + options?: CanonicalizeOptions, +) { + let features = Features.None + if (options?.collapse) features |= Features.CollapseUtilities + + let designSystem = prepareDesignSystemStorage(baseDesignSystem) + + return designSystem.storage[INTERNAL_OPTIONS_KEY].get(signatureOptions).get(features) +} + +export function canonicalizeCandidates( + baseDesignSystem: BaseDesignSystem, + candidates: string[], + options?: CanonicalizeOptions, +): string[] { + let signatureOptions = createSignatureOptions(baseDesignSystem, options) + let canonicalizeOptions = createCanonicalizeOptions(baseDesignSystem, signatureOptions, options) + + let designSystem = prepareDesignSystemStorage(baseDesignSystem) + + let result = new Set() + let cache = designSystem.storage[CANONICALIZE_CANDIDATE_KEY].get(canonicalizeOptions) + for (let candidate of candidates) { + result.add(cache.get(candidate)) + } + + return result.size <= 1 || !(canonicalizeOptions.features & Features.CollapseUtilities) + ? Array.from(result) + : collapseCandidates(canonicalizeOptions, Array.from(result)) +} + +function collapseCandidates(options: InternalCanonicalizeOptions, candidates: string[]): string[] { + let designSystem = options.designSystem + + // To keep things simple, we group candidates such that we only collapse + // candidates with the same variants and important modifier together. + let groups = new DefaultMap((_before: string) => { + return new DefaultMap((_after: string) => { + return new Set() + }) + }) + + let prefix = options.designSystem.theme.prefix ? `${options.designSystem.theme.prefix}:` : '' + + for (let candidate of candidates) { + let variants = segment(candidate, ':') + let utility = variants.pop()! + + let important = utility.endsWith('!') + if (important) { + utility = utility.slice(0, -1) + } + + let before = variants.length > 0 ? `${variants.join(':')}:` : '' + let after = important ? '!' : '' + + // Group by variants and important flag + groups.get(before).get(after).add(`${prefix}${utility}`) + } + + let result = new Set() + for (let [before, group] of groups.entries()) { + for (let [after, candidates] of group.entries()) { + for (let candidate of collapseGroup(Array.from(candidates))) { + // Drop the prefix if we had one, because the prefix is already there as + // part of the variants. + if (prefix && candidate.startsWith(prefix)) { + candidate = candidate.slice(prefix.length) + } + + result.add(`${before}${candidate}${after}`) + } + } + } + + return Array.from(result) + + function collapseGroup(candidates: string[]) { + let signatureOptions = options.signatureOptions + let computeUtilitiesPropertiesLookup = + designSystem.storage[UTILITY_PROPERTIES_KEY].get(signatureOptions) + let staticUtilities = designSystem.storage[STATIC_UTILITIES_KEY].get(signatureOptions) + + // For each candidate, compute the used properties and values. E.g.: `mt-1` → `margin-top` → `0.25rem` + // + // NOTE: Currently assuming we are dealing with static utilities only. This + // will change the moment we have `@utility` for most built-ins. + let candidatePropertiesValues = candidates.map((candidate) => + 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 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 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() + }) + + // Link each candidate that could be linked via another utility + // (intersection). This way we can reduce the amount of required combinations. + // + // E.g.: `mt-1` and `mb-1` can be linked via `my-1`. + // + // Candidates that cannot be linked won't be able to be collapsed. + // 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] + + for (let property of current) { + if (other.has(property)) { + linked.get(i).add(j) + linked.get(j).add(i) + + // The moment we find a link, we can stop comparing and move on to the + // next candidate. This will safe us some time + break + } + } + } + } + + // Not a single candidate can be linked to another one, nothing left to do + if (linked.size === 0) return candidates + + // Each candidate index will now have a set of other candidate indexes as + // its value. Let's make the lists unique combinations so that we can + // iterate over them. + let uniqueCombinations = new DefaultMap((key: string) => key.split(',').map(Number)) + for (let group of linked.values()) { + let sorted = Array.from(group).sort((a, b) => a - b) + uniqueCombinations.get(sorted.join(',')) + } + + // Let's try to actually collapse them now. + let result = new Set(candidates) + let drop = new Set() + + for (let idxs of uniqueCombinations.values()) { + for (let combo of combinations(idxs)) { + if (combo.some((idx) => drop.has(candidates[idx]))) continue // Skip already dropped items + + let potentialReplacements = combo.flatMap((idx) => otherUtilities[idx]).reduce(intersection) + + let collapsedSignature = designSystem.storage[UTILITY_SIGNATURE_KEY].get( + signatureOptions, + ).get( + combo + .map((idx) => candidates[idx]) + .sort((a, z) => a.localeCompare(z)) // Sort to increase cache hits + .join(' '), + ) + + for (let replacement of potentialReplacements) { + let signature = + 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. + for (let item of combo) { + if (candidates[item] !== replacement) { + drop.add(candidates[item]) + } + } + + break + } + } + } + + for (let item of drop) { + result.delete(item) + } + + return Array.from(result) + } +} + +const CANONICALIZE_CANDIDATE_KEY = Symbol() +function createCanonicalizeCandidateCache(): DesignSystem['storage'][typeof CANONICALIZE_CANDIDATE_KEY] { + return new DefaultMap((options: InternalCanonicalizeOptions) => { + let ds = options.designSystem + let prefix = ds.theme.prefix ? `${ds.theme.prefix}:` : '' + let variantCache = ds.storage[CANONICALIZE_VARIANT_KEY].get(options) + let utilityCache = ds.storage[CANONICALIZE_UTILITY_KEY].get(options) + + return new DefaultMap((rawCandidate: string, self) => { + for (let candidate of ds.parseCandidate(rawCandidate)) { + let variants = candidate.variants + .slice() + .reverse() + .flatMap((variant) => variantCache.get(variant)) + let important = candidate.important + + // Canonicalize the base candidate (utility), and re-attach the variants + // and important flag afterwards. This way we can maximize cache hits for + // the base candidate and each individual variant. + if (important || variants.length > 0) { + let canonicalizedUtility = self.get( + ds.printCandidate({ ...candidate, variants: [], important: false }), + ) + + // Rebuild the final candidate + let result = canonicalizedUtility + + // Remove the prefix if there are variants, because the variants exist + // between the prefix and the base candidate. + if (ds.theme.prefix !== null && variants.length > 0) { + result = result.slice(prefix.length) + } + + // Re-attach the variants + if (variants.length > 0) { + result = `${variants.map((v) => ds.printVariant(v)).join(':')}:${result}` + } + + // Re-attach the important flag + if (important) { + result += '!' + } + + // Re-attach the prefix if there were variants + if (ds.theme.prefix !== null && variants.length > 0) { + result = `${prefix}${result}` + } + + return result + } + + // We are guaranteed to have no variants and no important flag, just the + // base candidate left to canonicalize. + let result = utilityCache.get(rawCandidate) + if (result !== rawCandidate) { + return result + } + } + + return rawCandidate + }) + }) +} + +type VariantCanonicalizationFunction = ( + variant: Variant, + options: InternalCanonicalizeOptions, +) => Variant | Variant[] + +const VARIANT_CANONICALIZATIONS: VariantCanonicalizationFunction[] = [ + themeToVarVariant, + arbitraryValueToBareValueVariant, + modernizeArbitraryValuesVariant, + arbitraryVariants, +] + +const CANONICALIZE_VARIANT_KEY = Symbol() +function createCanonicalizeVariantCache(): DesignSystem['storage'][typeof CANONICALIZE_VARIANT_KEY] { + return new DefaultMap((options: InternalCanonicalizeOptions) => { + return new DefaultMap((variant: Variant): Variant[] => { + let replacement = [variant] + for (let fn of VARIANT_CANONICALIZATIONS) { + for (let current of replacement.splice(0)) { + // A single variant can result in multiple variants, e.g.: + // `[&>[data-selected]]:flex` → `*:data-selected:flex` + let result = fn(cloneVariant(current), options) + if (Array.isArray(result)) { + replacement.push(...result) + continue + } else { + replacement.push(result) + } + } + } + return replacement + }) + }) +} + +type UtilityCanonicalizationFunction = ( + candidate: Candidate, + options: InternalCanonicalizeOptions, +) => Candidate + +const UTILITY_CANONICALIZATIONS: UtilityCanonicalizationFunction[] = [ + bgGradientToLinear, + themeToVarUtility, + calcToSpacingFunction, + optimizeArbitraryValueExpressions, + arbitraryUtilities, + bareValueUtilities, + deprecatedUtilities, + dropUnnecessaryDataTypes, + arbitraryValueToBareValueUtility, + optimizeModifier, +] + +const CANONICALIZE_UTILITY_KEY = Symbol() +function createCanonicalizeUtilityCache(): DesignSystem['storage'][typeof CANONICALIZE_UTILITY_KEY] { + return new DefaultMap((options: InternalCanonicalizeOptions) => { + let designSystem = options.designSystem + return new DefaultMap((rawCandidate: string): string => { + for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { + let replacement = cloneCandidate(readonlyCandidate) as Writable + + for (let fn of UTILITY_CANONICALIZATIONS) { + replacement = fn(replacement, options) + } + + let canonicalizedCandidate = designSystem.printCandidate(replacement) + if (rawCandidate !== canonicalizedCandidate) { + return canonicalizedCandidate + } + } + + return rawCandidate + }) + }) +} + +// ---- + +const DIRECTIONS = ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] +function bgGradientToLinear(candidate: Candidate) { + if (candidate.kind === 'static' && candidate.root.startsWith('bg-gradient-to-')) { + let direction = candidate.root.slice(15) + + if (!DIRECTIONS.includes(direction)) { + return candidate + } + + candidate.root = `bg-linear-to-${direction}` + return candidate + } + + return candidate +} + +// ---- + +const enum Convert { + All = 0, + MigrateModifier = 1 << 0, + MigrateThemeOnly = 1 << 1, +} + +function themeToVarUtility(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate { + let convert = options.designSystem.storage[CONVERTER_KEY] + + if (candidate.kind === 'arbitrary') { + let [newValue, modifier] = convert( + candidate.value, + candidate.modifier === null ? Convert.MigrateModifier : Convert.All, + ) + if (newValue !== candidate.value) { + candidate.value = newValue + + if (modifier !== null) { + candidate.modifier = modifier + } + } + } else if (candidate.kind === 'functional' && candidate.value?.kind === 'arbitrary') { + let [newValue, modifier] = convert( + candidate.value.value, + candidate.modifier === null ? Convert.MigrateModifier : Convert.All, + ) + if (newValue !== candidate.value.value) { + candidate.value.value = newValue + + if (modifier !== null) { + candidate.modifier = modifier + } + } + } + + return candidate +} + +function themeToVarVariant( + variant: Variant, + options: InternalCanonicalizeOptions, +): Variant | Variant[] { + let convert = options.designSystem.storage[CONVERTER_KEY] + + let iterator = walkVariants(variant) + for (let [variant] of iterator) { + if (variant.kind === 'arbitrary') { + let [newValue] = convert(variant.selector, Convert.MigrateThemeOnly) + if (newValue !== variant.selector) { + variant.selector = newValue + } + } else if (variant.kind === 'functional' && variant.value?.kind === 'arbitrary') { + let [newValue] = convert(variant.value.value, Convert.MigrateThemeOnly) + if (newValue !== variant.value.value) { + variant.value.value = newValue + } + } + } + + 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, +): DesignSystem['storage'][typeof CONVERTER_KEY] { + return createConverter(designSystem) + + function createConverter(designSystem: DesignSystem) { + function convert(input: string, options = Convert.All): [string, CandidateModifier | null] { + let ast = ValueParser.parse(input) + + // In some scenarios (e.g.: variants), we can't migrate to `var(…)` if it + // ends up in the `@media (…)` part. In this case we only have to migrate to + // the new `theme(…)` notation. + if (options & Convert.MigrateThemeOnly) { + return [substituteFunctionsInValue(ast, toTheme), null] + } + + let themeUsageCount = 0 + let themeModifierCount = 0 + + // Analyze AST + walk(ast, (node) => { + if (node.kind !== 'function') return + if (node.value !== 'theme') return + + // We are only interested in the `theme` function + themeUsageCount += 1 + + // Figure out if a modifier is used + walk(node.nodes, (child) => { + // If we see a `,`, it means that we have a fallback value + if (child.kind === 'separator' && child.value.includes(',')) { + return WalkAction.Stop + } + + // If we see a `/`, we have a modifier + else if (child.kind === 'word' && child.value === '/') { + themeModifierCount += 1 + return WalkAction.Stop + } + + return WalkAction.Skip + }) + }) + + // No `theme(…)` calls, nothing to do + if (themeUsageCount === 0) { + return [input, null] + } + + // No `theme(…)` with modifiers, we can migrate to `var(…)` + if (themeModifierCount === 0) { + return [substituteFunctionsInValue(ast, toVar), null] + } + + // Multiple modifiers which means that there are multiple `theme(…/…)` + // values. In this case, we can't convert the modifier to a candidate + // modifier. + // + // We also can't migrate to `var(…)` because that would lose the modifier. + // + // Try to convert each `theme(…)` call to the modern syntax. + if (themeModifierCount > 1) { + return [substituteFunctionsInValue(ast, toTheme), null] + } + + // Only a single `theme(…)` with a modifier left, that modifier will be + // migrated to a candidate modifier. + let modifier: CandidateModifier | null = null + let result = substituteFunctionsInValue(ast, (path, fallback) => { + let parts = segment(path, '/').map((part) => part.trim()) + + // Multiple `/` separators, which makes this an invalid path + if (parts.length > 2) return null + + // The path contains a `/`, which means that there is a modifier such as + // `theme(colors.red.500/50%)`. + // + // Currently, we are assuming that this is only being used for colors, + // which means that we can typically convert them to a modifier on the + // candidate itself. + // + // If there is more than one node in the AST though, `theme(…)` must not + // be the whole value so it's not safe to use a modifier instead. + // + // E.g.: `inset 0px 1px theme(colors.red.500/50%)` is a shadow, not a color. + if (ast.length === 1 && parts.length === 2 && options & Convert.MigrateModifier) { + let [pathPart, modifierPart] = parts + + // 50% -> /50 + if (/^\d+%$/.test(modifierPart)) { + modifier = { kind: 'named', value: modifierPart.slice(0, -1) } + } + + // .12 -> /12 + // .12345 -> /[12.345] + else if (/^0?\.\d+$/.test(modifierPart)) { + let value = Number(modifierPart) * 100 + modifier = { + kind: Number.isInteger(value) ? 'named' : 'arbitrary', + value: value.toString(), + } + } + + // Anything else becomes arbitrary + else { + modifier = { kind: 'arbitrary', value: modifierPart } + } + + // Update path to be the first part + path = pathPart + } + + return toVar(path, fallback) || toTheme(path, fallback) + }) + + return [result, modifier] + } + + function pathToVariableName(path: string, shouldPrefix = true) { + let variable = `--${keyPathToCssProperty(toKeyPath(path))}` as const + if (!designSystem.theme.get([variable])) return null + + if (shouldPrefix && designSystem.theme.prefix) { + return `--${designSystem.theme.prefix}-${variable.slice(2)}` + } + + return variable + } + + function toVar(path: string, fallback?: string) { + let variable = pathToVariableName(path) + if (variable) return fallback ? `var(${variable}, ${fallback})` : `var(${variable})` + + let keyPath = toKeyPath(path) + if (keyPath[0] === 'spacing' && designSystem.theme.get(['--spacing'])) { + let multiplier = keyPath[1] + if (!isValidSpacingMultiplier(multiplier)) return null + + return `--spacing(${multiplier})` + } + + return null + } + + function toTheme(path: string, fallback?: string) { + let parts = segment(path, '/').map((part) => part.trim()) + path = parts.shift()! + + let variable = pathToVariableName(path, false) + if (!variable) return null + + let modifier = parts.length > 0 ? `/${parts.join('/')}` : '' + return fallback + ? `--theme(${variable}${modifier}, ${fallback})` + : `--theme(${variable}${modifier})` + } + + return convert + } +} + +function substituteFunctionsInValue( + ast: ValueParser.ValueAstNode[], + handle: (value: string, fallback?: string) => string | null, +) { + walk(ast, (node, ctx) => { + if (node.kind === 'function' && node.value === 'theme') { + if (node.nodes.length < 1) return + + // Ignore whitespace before the first argument + if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') { + node.nodes.shift() + } + + let pathNode = node.nodes[0] + if (pathNode.kind !== 'word') return + + let path = pathNode.value + + // For the theme function arguments, we require all separators to contain + // comma (`,`), spaces alone should be merged into the previous word to + // avoid splitting in this case: + // + // theme(--color-red-500 / 75%) theme(--color-red-500 / 75%, foo, bar) + // + // We only need to do this for the first node, as the fallback values are + // passed through as-is. + let skipUntilIndex = 1 + for (let i = skipUntilIndex; i < node.nodes.length; i++) { + if (node.nodes[i].value.includes(',')) { + break + } + path += ValueParser.toCss([node.nodes[i]]) + skipUntilIndex = i + 1 + } + + path = eventuallyUnquote(path) + let fallbackValues = node.nodes.slice(skipUntilIndex + 1) + + let replacement = + fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path) + if (replacement === null) return + + { + let idx = ctx.index - 1 + while (idx !== -1) { + let previous = ctx.siblings[idx] + // Skip the space separator + if (previous.kind === 'separator' && previous.value.trim() === '') { + idx -= 1 + continue + } + + // If the previous node is a word and contains an operator, we need to + // wrap the replacement in parentheses to make the output less + // ambiguous. + // + // Input: + // - `calc(100dvh-theme(spacing.2))` + // + // Output: + // - `calc(100dvh-(--spacing(2)))` + // + // Not: + // -`calc(100dvh---spacing(2))` + // + if (/^[-+*/]$/.test(previous.value.trim())) { + replacement = `(${replacement})` + } + + break + } + } + + return WalkAction.Replace(ValueParser.parse(replacement)) + } + }) + + return ValueParser.toCss(ast) +} + +function eventuallyUnquote(value: string) { + if (value[0] !== "'" && value[0] !== '"') return value + + let unquoted = '' + let quoteChar = value[0] + for (let i = 1; i < value.length - 1; i++) { + let currentChar = value[i] + let nextChar = value[i + 1] + + if (currentChar === '\\' && (nextChar === quoteChar || nextChar === '\\')) { + unquoted += nextChar + i++ + } else { + unquoted += currentChar + } + } + + return unquoted +} + +// ---- + +function* walkVariants(variant: Variant) { + function* inner( + variant: Variant, + parent: Extract | null = null, + ): Iterable<[Variant, Extract | null]> { + yield [variant, parent] + + if (variant.kind === 'compound') { + yield* inner(variant.variant, variant) + } + } + + yield* inner(variant, null) +} + +function parseCandidate(designSystem: DesignSystem, input: string) { + return designSystem.parseCandidate( + designSystem.theme.prefix && !input.startsWith(`${designSystem.theme.prefix}:`) + ? `${designSystem.theme.prefix}:${input}` + : input, + ) +} + +function printUnprefixedCandidate(designSystem: DesignSystem, candidate: Candidate) { + let candidateString = designSystem.printCandidate(candidate) + + return designSystem.theme.prefix && candidateString.startsWith(`${designSystem.theme.prefix}:`) + ? candidateString.slice(designSystem.theme.prefix.length + 1) + : candidateString +} + +// ---- + +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)) + if (!parsed) return null + + let [myValue, myUnit] = parsed + if (myUnit !== unit) return null + + return myValue / value + }) +} + +function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate { + // We are only interested in arbitrary properties and arbitrary values + if ( + // Arbitrary property + candidate.kind !== 'arbitrary' && + // Arbitrary value + !(candidate.kind === 'functional' && candidate.value?.kind === 'arbitrary') + ) { + return candidate + } + + 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) + + // Compute the signature for the target candidate + let targetSignature = signatures.get(targetCandidateString) + if (typeof targetSignature !== 'string') return candidate + + // Try a few options to find a suitable replacement utility + for (let replacementCandidate of tryReplacements(targetSignature, candidate)) { + if (!hasSameSignature(candidate, replacementCandidate)) { + continue + } + + // Ensure that if CSS variables were used, that they are still used + if (!allVariablesAreUsed(designSystem, candidate, replacementCandidate)) { + continue + } + + return replacementCandidate + } + + return candidate + + function* tryReplacements( + targetSignature: string, + candidate: Extract, + ): Generator { + // 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 + } + + // 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 + // modifier later and verify if we have a valid migration. + // + // This is necessary because `text-red-500/50` will not be pre-computed, + // only `text-red-500` will. + if (replacements.length === 0 && candidate.modifier) { + let candidateWithoutModifier = { ...candidate, modifier: null } + let targetSignatureWithoutModifier = signatures.get( + designSystem.printCandidate(candidateWithoutModifier), + ) + if (typeof targetSignatureWithoutModifier === 'string') { + for (let replacementCandidate of tryReplacements( + targetSignatureWithoutModifier, + candidateWithoutModifier, + )) { + yield Object.assign({}, replacementCandidate, { modifier: candidate.modifier }) + } + } + } + + // If only a single utility maps to the signature, we can use that as the + // replacement. + if (replacements.length === 1) { + for (let replacementCandidate of parseCandidate(designSystem, replacements[0])) { + yield replacementCandidate + } + } + + // Find a corresponding functional utility for the same signature + else if (replacements.length === 0) { + // An arbitrary property will only set a single property, we can use that + // to find functional utilities that also set this property. + let value = + 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) { + rootPrefix = '-' + spacingMultiplier = Math.abs(spacingMultiplier) + } + + for (let root of Array.from(designSystem.utilities.keys('functional')).sort( + // Sort negative roots after positive roots so that we can try + // `mt-*` before `-mt-*`. This is especially useful in situations where + // `-mt-[0px]` can be translated to `mt-[0px]`. + (a, z) => Number(a[0] === '-') - Number(z[0] === '-'), + )) { + if (rootPrefix) root = `${rootPrefix}${root}` + + // Try as bare value + for (let replacementCandidate of parseCandidate(designSystem, `${root}-${value}`)) { + yield replacementCandidate + } + + // Try as bare value with modifier + if (candidate.modifier) { + for (let replacementCandidate of parseCandidate( + designSystem, + `${root}-${value}${candidate.modifier}`, + )) { + yield replacementCandidate + } + } + + // 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) + ) { + for (let replacementCandidate of parseCandidate( + designSystem, + `${root}-${spacingMultiplier}`, + )) { + yield replacementCandidate + } + + // Try bare value based on the `--spacing` value, but with a modifier + if (candidate.modifier) { + for (let replacementCandidate of parseCandidate( + designSystem, + `${root}-${spacingMultiplier}${printModifier(candidate.modifier)}`, + )) { + yield replacementCandidate + } + } + } + + // Try as arbitrary value + for (let replacementCandidate of parseCandidate(designSystem, `${root}-[${value}]`)) { + yield replacementCandidate + } + + // Try as arbitrary value with modifier + if (candidate.modifier) { + for (let replacementCandidate of parseCandidate( + designSystem, + `${root}-[${value}]${printModifier(candidate.modifier)}`, + )) { + yield replacementCandidate + } + } + } + } + } +} + +// Let's make sure that all variables used in the value are also all used in the +// found replacement. If not, then we are dealing with a different namespace or +// we could lose functionality in case the variable was changed higher up in the +// DOM tree. +function allVariablesAreUsed( + designSystem: DesignSystem, + candidate: Candidate, + replacement: Candidate, +) { + let value: string | null = null + + // Functional utility with arbitrary value and variables + if ( + candidate.kind === 'functional' && + candidate.value?.kind === 'arbitrary' && + candidate.value.value.includes('var(--') + ) { + value = candidate.value.value + } + + // Arbitrary property with variables + else if (candidate.kind === 'arbitrary' && candidate.value.includes('var(--')) { + value = candidate.value + } + + // No variables in the value, so this is a safe migration + if (value === null) { + return true + } + + let replacementAsCss = designSystem + .candidatesToCss([designSystem.printCandidate(replacement)]) + .join('\n') + + let isSafeMigration = true + walk(ValueParser.parse(value), (node) => { + if (node.kind === 'function' && node.value === 'var') { + let variable = node.nodes[0].value + let r = new RegExp(`var\\(${variable}[,)]\\s*`, 'g') + if ( + // We need to check if the variable is used in the replacement + !r.test(replacementAsCss) || + // The value cannot be set to a different value in the + // replacement because that would make it an unsafe migration + replacementAsCss.includes(`${variable}:`) + ) { + isSafeMigration = false + return WalkAction.Stop + } + } + }) + + return isSafeMigration +} + +// ---- + +function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate { + // We are only interested in bare value utilities + if (candidate.kind !== 'functional' || candidate.value?.kind !== 'named') { + return candidate + } + + 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) + + // Compute the signature for the target candidate + let targetSignature = signatures.get(targetCandidateString) + if (typeof targetSignature !== 'string') return candidate + + // Try a few options to find a suitable replacement utility + for (let replacementCandidate of tryReplacements(targetSignature, candidate)) { + if (!hasSameSignature(candidate, replacementCandidate)) { + continue + } + + return replacementCandidate + } + + return candidate + + function* tryReplacements( + targetSignature: string, + candidate: Extract, + ): Generator { + // 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 + } + + // 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 + // modifier later and verify if we have a valid migration. + // + // This is necessary because `text-red-500/50` will not be pre-computed, + // only `text-red-500` will. + if (replacements.length === 0 && candidate.modifier) { + let candidateWithoutModifier = { ...candidate, modifier: null } + let targetSignatureWithoutModifier = signatures.get( + designSystem.printCandidate(candidateWithoutModifier), + ) + if (typeof targetSignatureWithoutModifier === 'string') { + for (let replacementCandidate of tryReplacements( + targetSignatureWithoutModifier, + candidateWithoutModifier, + )) { + yield Object.assign({}, replacementCandidate, { modifier: candidate.modifier }) + } + } + } + + // If only a single utility maps to the signature, we can use that as the + // replacement. + if (replacements.length === 1) { + for (let replacementCandidate of parseCandidate(designSystem, replacements[0])) { + yield replacementCandidate + } + } + } +} + +// ---- + +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 targetCandidateString = printUnprefixedCandidate(designSystem, candidate) + + for (let replacementString of tryDeprecatedUtilities(targetCandidateString)) { + if (!hasSameSignature(candidate, replacementString)) { + continue + } + + let [replacement] = parseCandidate(designSystem, replacementString) + return replacement + } + + return candidate +} + +// ---- + +function arbitraryVariants( + variant: Variant, + 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 iterator = walkVariants(variant) + for (let [variant] of iterator) { + if (variant.kind === 'compound') continue + + let targetString = designSystem.printVariant(variant) + let targetSignature = signatures.get(targetString) + 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 + + let foundVariant = foundVariants[0] + let parsedVariant = designSystem.parseVariant(foundVariant) + if (parsedVariant === null) continue + + replaceObject(variant, parsedVariant) + } + + return variant +} + +// ---- + +function dropUnnecessaryDataTypes( + candidate: Candidate, + options: InternalCanonicalizeOptions, +): Candidate { + let designSystem = options.designSystem + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) + + if ( + candidate.kind === 'functional' && + candidate.value?.kind === 'arbitrary' && + candidate.value.dataType !== null + ) { + let replacement = designSystem.printCandidate({ + ...candidate, + value: { ...candidate.value, dataType: null }, + }) + + if (signatures.get(designSystem.printCandidate(candidate)) === signatures.get(replacement)) { + candidate.value.dataType = null + } + } + + return candidate +} + +// ---- + +function arbitraryValueToBareValueUtility( + candidate: Candidate, + options: InternalCanonicalizeOptions, +): Candidate { + // We are only interested in functional utilities with arbitrary values + if (candidate.kind !== 'functional' || candidate.value?.kind !== 'arbitrary') { + return candidate + } + + let designSystem = options.designSystem + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) + + let expectedSignature = signatures.get(designSystem.printCandidate(candidate)) + if (expectedSignature === null) return candidate + + for (let value of tryValueReplacements(candidate)) { + let newSignature = signatures.get(designSystem.printCandidate({ ...candidate, value })) + if (newSignature === expectedSignature) { + candidate.value = value + return candidate + } + } + + return candidate +} + +function arbitraryValueToBareValueVariant(variant: Variant): Variant | Variant[] { + let iterator = walkVariants(variant) + for (let [variant] of iterator) { + // Convert `data-[selected]` to `data-selected` + if ( + variant.kind === 'functional' && + variant.root === 'data' && + variant.value?.kind === 'arbitrary' && + !variant.value.value.includes('=') + ) { + variant.value = { + kind: 'named', + value: variant.value.value, + } + } + + // Convert `aria-[selected="true"]` to `aria-selected` + else if ( + variant.kind === 'functional' && + variant.root === 'aria' && + variant.value?.kind === 'arbitrary' && + (variant.value.value.endsWith('=true') || + variant.value.value.endsWith('="true"') || + variant.value.value.endsWith("='true'")) + ) { + let [key, _value] = segment(variant.value.value, '=') + if ( + // aria-[foo~="true"] + key[key.length - 1] === '~' || + // aria-[foo|="true"] + key[key.length - 1] === '|' || + // aria-[foo^="true"] + key[key.length - 1] === '^' || + // aria-[foo$="true"] + key[key.length - 1] === '$' || + // aria-[foo*="true"] + key[key.length - 1] === '*' + ) { + continue + } + + variant.value = { + kind: 'named', + value: variant.value.value.slice(0, variant.value.value.indexOf('=')), + } + } + + // Convert `supports-[gap]` to `supports-gap` + else if ( + variant.kind === 'functional' && + variant.root === 'supports' && + variant.value?.kind === 'arbitrary' && + /^[a-z-][a-z0-9-]*$/i.test(variant.value.value) + ) { + variant.value = { + kind: 'named', + value: variant.value.value, + } + } + } + + return variant +} + +// Convert functional utilities with arbitrary values to bare values if we can. +// We know that bare values can only be: +// +// 1. A number (with increments of .25) +// 2. A percentage (with increments of .25 followed by a `%`) +// 3. A ratio with whole numbers +// +// Not a bare value per se, but if we are dealing with a keyword, that could +// potentially also look like a bare value (aka no `[` or `]`). E.g.: +// ```diff +// grid-cols-[subgrid] +// grid-cols-subgrid +// ``` +function* tryValueReplacements( + candidate: Extract, + value: string = candidate.value?.value ?? '', + seen: Set = new Set(), +): Generator { + if (seen.has(value)) return + seen.add(value) + + // 0. Just try to drop the square brackets and see if it works + // 1. A number (with increments of .25) + yield { + kind: 'named', + value, + fraction: null, + } + + // 2. A percentage (with increments of .25 followed by a `%`) + // Try to drop the `%` and see if it works + if (value.endsWith('%') && isValidSpacingMultiplier(value.slice(0, -1))) { + yield { + kind: 'named', + value: value.slice(0, -1), + fraction: null, + } + } + + // 3. A ratio with whole numbers + if (value.includes('/')) { + let [numerator, denominator] = value.split('/') + if (isPositiveInteger(numerator) && isPositiveInteger(denominator)) { + yield { + kind: 'named', + value: numerator, + fraction: `${numerator}/${denominator}`, + } + } + } + + // It could also be that we have `20px`, we can try just `20` and see if it + // results in the same signature. + let allNumbersAndFractions = new Set() + + // Figure out all numbers and fractions in the value + for (let match of value.matchAll(/(\d+\/\d+)|(\d+\.?\d+)/g)) { + allNumbersAndFractions.add(match[0].trim()) + } + + // Sort the numbers and fractions where the smallest length comes first. This + // will result in the smallest replacement. + let options = Array.from(allNumbersAndFractions).sort((a, z) => { + return a.length - z.length + }) + + // Try all the options + for (let option of options) { + yield* tryValueReplacements(candidate, option, seen) + } +} + +// ---- + +function isSingleSelector(ast: SelectorParser.SelectorAstNode[]): boolean { + if (ast.length === 1 && ast[0].kind === 'list') return false + return true +} + +function isAttributeSelector(node: SelectorParser.SelectorNode): boolean { + return node.value[0] === '[' && node.value[node.value.length - 1] === ']' +} + +function modernizeArbitraryValuesVariant( + variant: Variant, + options: InternalCanonicalizeOptions, +): Variant | Variant[] { + let result = [variant] + let designSystem = options.designSystem + let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY].get(options.signatureOptions.rem) + + let iterator = walkVariants(variant) + for (let [variant, parent] of iterator) { + // Forward modifier from the root to the compound variant + if ( + variant.kind === 'compound' && + (variant.root === 'has' || variant.root === 'not' || variant.root === 'in') + ) { + if (variant.modifier !== null) { + if ('modifier' in variant.variant) { + variant.variant.modifier = variant.modifier + variant.modifier = null + } + } + } + + // Expecting an arbitrary variant + if (variant.kind === 'arbitrary') { + // Expecting a non-relative arbitrary variant + if (variant.relative) continue + + let ast = SelectorParser.parse(variant.selector) + + // 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 + parent === null && + // [&_>_*]:flex + // ^ ^ ^ + ast.length === 3 && + ast[0].kind === 'selector' && + ast[0].value === '&' && + ast[1].kind === 'combinator' && + ast[1].value === '>' && + ast[2].kind === 'selector' && + ast[2].value === '*' + ) { + replaceObject(variant, designSystem.parseVariant('*')) + continue + } + + // `[&_*]` can be replaced with `**` + if ( + // Only top-level, so `has-[&_*]` is not supported + parent === null && + // [&_*]:flex + // ^ ^ + ast.length === 3 && + ast[0].kind === 'selector' && + ast[0].value === '&' && + ast[1].kind === 'combinator' && + ast[1].value === ' ' && + ast[2].kind === 'selector' && + ast[2].value === '*' + ) { + replaceObject(variant, designSystem.parseVariant('**')) + 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. + // + // E.g.: `[[data-visible]_&]` => `in-data-visible` + if ( + // Only top-level, so `in-[&_[data-visible]]` is not supported + parent === null && + // [[data-visible]___&]:flex + // ^^^^^^^^^^^^^^ ^ ^ + ast.length === 3 && + ast[0].kind === 'selector' && + ast[1].kind === 'combinator' && + ast[1].value === ' ' && + ast[2].kind === 'selector' && + ast[2].value === '&' + ) { + ast.pop() // Remove the nesting node + ast.pop() // Remove the combinator + + // When handling a compound like `in-[[data-visible]]`, we will first + // handle `[[data-visible]]`, then the parent `in-*` part. This means + // 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)}]`)) + continue + } + + // Hoist `not` modifier for `@media` or `@supports` variants + // + // E.g.: `[@media_not_(scripting:none)]:` -> `not-[@media_(scripting:none)]:` + if ( + // Only top-level, so something like `in-[@media(scripting:none)]` + // (which is not valid anyway) is not supported + parent === null && + // [@media_not(scripting:none)]:flex + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ast[0].kind === 'selector' && + (ast[0].value === '@media' || ast[0].value === '@supports') + ) { + let targetSignature = signatures.get(designSystem.printVariant(variant)) + + let parsed = ValueParser.parse(SelectorParser.toCss(ast, true)) + let containsNot = false + walk(parsed, (node) => { + if (node.kind === 'word' && node.value === 'not') { + containsNot = true + return WalkAction.Replace([]) + } + }) + + // Remove unnecessary whitespace + parsed = ValueParser.parse(ValueParser.toCss(parsed)) + walk(parsed, (node) => { + if (node.kind === 'separator' && node.value !== ' ' && node.value.trim() === '') { + // node.value contains at least 2 spaces. Normalize it to a single + // space. + node.value = ' ' + } + }) + + if (containsNot) { + let hoistedNot = designSystem.parseVariant(`not-[${ValueParser.toCss(parsed)}]`) + if (hoistedNot === null) continue + let hoistedNotSignature = signatures.get(designSystem.printVariant(hoistedNot)) + if (targetSignature === hoistedNotSignature) { + replaceObject(variant, hoistedNot) + continue + } + } + } + + let prefixedVariant: Variant | null = null + + // Handling a child combinator. E.g.: `[&>[data-visible]]` => `*:data-visible` + if ( + // Only top-level, so `has-[&>[data-visible]]` is not supported + parent === null && + // [&_>_[data-visible]]:flex + // ^ ^ ^^^^^^^^^^^^^^ + ast.length === 3 && + ast[0].kind === 'selector' && + ast[0].value === '&' && + ast[1].kind === 'combinator' && + ast[1].value === '>' && + ast[2].kind === 'selector' && + (ast[2].value[0] === ':' || isAttributeSelector(ast[2])) + ) { + ast = [ast[2]] + prefixedVariant = designSystem.parseVariant('*') + } + + // Handling a grand child combinator. E.g.: `[&_[data-visible]]` => `**:data-visible` + if ( + // Only top-level, so `has-[&_[data-visible]]` is not supported + parent === null && + // [&_[data-visible]]:flex + // ^ ^^^^^^^^^^^^^^ + ast.length === 3 && + ast[0].kind === 'selector' && + ast[0].value === '&' && + ast[1].kind === 'combinator' && + ast[1].value === ' ' && + ast[2].kind === 'selector' && + (ast[2].value[0] === ':' || isAttributeSelector(ast[2])) + ) { + 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) + } + }, + }) + + // Expecting a single selector (normal selector or attribute selector) + if (selectorNodes.length !== 1) continue + + let target = selectorNodes[0] + if (target.kind === 'function' && target.value === ':is') { + // Expecting a single selector node + if ( + !isSingleSelector(target.nodes) || + // [foo][bar] is considered a single selector but has multiple nodes + target.nodes.length !== 1 + ) { + continue + } + + // Expecting a single attribute selector + if (target.nodes[0].kind === 'selector' && !isAttributeSelector(target.nodes[0])) continue + + // Unwrap the selector from inside `&:is(…)` + target = target.nodes[0] + } + + // Expecting a pseudo selector (or function) + if ( + (target.kind === 'function' && target.value[0] === ':') || + (target.kind === 'selector' && target.value[0] === ':') + ) { + let targetNode = target + let compoundNot = false + if (targetNode.kind === 'function' && targetNode.value === ':not') { + compoundNot = true + if (targetNode.nodes.length !== 1) continue + if (targetNode.nodes[0].kind !== 'selector' && targetNode.nodes[0].kind !== 'function') { + continue + } + if (targetNode.nodes[0].value[0] !== ':') continue + + targetNode = targetNode.nodes[0] + } + + let newVariant = ((value) => { + if ( + value === ':nth-child' && + targetNode.kind === 'function' && + targetNode.nodes.length === 1 && + targetNode.nodes[0].kind === 'value' && + targetNode.nodes[0].value === 'odd' + ) { + if (compoundNot) { + compoundNot = false + return 'even' + } + return 'odd' + } + + if ( + value === ':nth-child' && + targetNode.kind === 'function' && + targetNode.nodes.length === 1 && + targetNode.nodes[0].kind === 'value' && + targetNode.nodes[0].value === 'even' + ) { + if (compoundNot) { + compoundNot = false + return 'odd' + } + return 'even' + } + + for (let [selector, variantName] of [ + [':nth-child', 'nth'], + [':nth-last-child', 'nth-last'], + [':nth-of-type', 'nth-of-type'], + [':nth-last-of-type', 'nth-of-last-type'], + ]) { + if ( + value === selector && + targetNode.kind === 'function' && + targetNode.nodes.length === 1 + ) { + if ( + targetNode.nodes.length === 1 && + targetNode.nodes[0].kind === 'value' && + isPositiveInteger(targetNode.nodes[0].value) + ) { + return `${variantName}-${targetNode.nodes[0].value}` + } + + return `${variantName}-[${SelectorParser.toCss(targetNode.nodes, true)}]` + } + } + + // Hoist `not` modifier + if (compoundNot) { + let targetSignature = signatures.get(designSystem.printVariant(variant)) + let replacementSignature = signatures.get(`not-[${value}]`) + if (targetSignature === replacementSignature) { + return `[&${value}]` + } + } + + return null + })(targetNode.value) + + if (newVariant === null) { + if (prefixedVariant) { + replaceObject(variant, { + kind: 'arbitrary', + selector: target.value, + relative: false, + } satisfies Variant) + + return [prefixedVariant, variant] + } + + continue + } + + // Add `not-` prefix + if (compoundNot) newVariant = `not-${newVariant}` + + let parsed = designSystem.parseVariant(newVariant) + if (parsed === null) continue + + // Update original variant + replaceObject(variant, parsed) + } + + // Expecting an attribute selector + else if (target.kind === 'selector' && isAttributeSelector(target)) { + let attributeSelector = AttributeSelectorParser.parse(target.value) + if (attributeSelector === null) continue // Invalid attribute selector + + // Migrate `data-*` + if (attributeSelector.attribute.startsWith('data-')) { + let name = attributeSelector.attribute.slice(5) // Remove `data-` + + replaceObject(variant, { + kind: 'functional', + root: 'data', + modifier: null, + value: + attributeSelector.value === null + ? { kind: 'named', value: name } + : { + kind: 'arbitrary', + value: `${name}${attributeSelector.operator}${attributeSelector.quote ?? ''}${attributeSelector.value}${attributeSelector.quote ?? ''}${attributeSelector.sensitivity ? ` ${attributeSelector.sensitivity}` : ''}`, + }, + } satisfies Variant) + } + + // Migrate `aria-*` + else if (attributeSelector.attribute.startsWith('aria-')) { + let name = attributeSelector.attribute.slice(5) // Remove `aria-` + replaceObject(variant, { + kind: 'functional', + root: 'aria', + modifier: null, + value: + attributeSelector.value === null + ? { kind: 'arbitrary', value: name } // aria-[foo] + : attributeSelector.operator === '=' && + attributeSelector.value === 'true' && + attributeSelector.sensitivity === null + ? { kind: 'named', value: name } // aria-[foo="true"] or aria-[foo='true'] or aria-[foo=true] + : { + kind: 'arbitrary', + value: `${attributeSelector.attribute}${attributeSelector.operator}${attributeSelector.quote ?? ''}${attributeSelector.value}${attributeSelector.quote ?? ''}${attributeSelector.sensitivity ? ` ${attributeSelector.sensitivity}` : ''}`, + }, // aria-[foo~="true"], aria-[foo|="true"], … + } satisfies Variant) + } + + // Arbitrary attributes + else { + replaceObject(variant, { + kind: 'arbitrary', + selector: target.value, + relative: false, + } satisfies Variant) + } + } + + if (prefixedVariant) { + return [prefixedVariant, variant] + } + } + } + + 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 +// +// E.g.: +// +// - `/[25%]` → `/25` +// - `/[100%]` → `/100` → +// - `/100` → +// +function optimizeModifier(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate { + // We are only interested in functional or arbitrary utilities with a modifier + if ( + (candidate.kind !== 'functional' && candidate.kind !== 'arbitrary') || + candidate.modifier === null + ) { + return candidate + } + + let designSystem = options.designSystem + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) + + let targetSignature = signatures.get(designSystem.printCandidate(candidate)) + let modifier = candidate.modifier + + // 1. Try to drop the modifier entirely + if ( + targetSignature === + signatures.get(designSystem.printCandidate({ ...candidate, modifier: null })) + ) { + candidate.modifier = null + return candidate + } + + // 2. Try to remove the square brackets and the `%` sign + { + let newModifier: NamedUtilityValue = { + kind: 'named', + value: modifier.value.endsWith('%') + ? modifier.value.includes('.') + ? `${Number(modifier.value.slice(0, -1))}` + : modifier.value.slice(0, -1) + : modifier.value, + fraction: null, + } + + if ( + targetSignature === + signatures.get(designSystem.printCandidate({ ...candidate, modifier: newModifier })) + ) { + candidate.modifier = newModifier + return candidate + } + } + + // 3. Try to remove the square brackets, but multiply by 100. E.g.: `[0.16]` -> `16` + { + let newModifier: NamedUtilityValue = { + kind: 'named', + value: `${parseFloat(modifier.value) * 100}`, + fraction: null, + } + + if ( + targetSignature === + signatures.get(designSystem.printCandidate({ ...candidate, modifier: newModifier })) + ) { + candidate.modifier = newModifier + return candidate + } + } + + return candidate +} + +export enum SignatureFeatures { + None = 0, + ExpandProperties = 1 << 0, + LogicalToPhysical = 1 << 1, +} + +interface SignatureOptions { + /** + * The root font size in pixels. If provided, `rem` values will be normalized + * to `px` values. + * + * E.g.: `mt-[16px]` with `rem: 16` will become `mt-4` (assuming `--spacing: 0.25rem`). + */ + rem: number | null + + /** + * Features that influence how signatures are computed. + */ + features: SignatureFeatures +} + +// Given a utility, compute a signature that represents the utility. The +// signature will be a normalised form of the generated CSS for the utility, or +// a unique symbol if the utility is not valid. The class in the selector will +// be replaced with the `.x` selector. +// +// This function should only be passed the base utility so `flex`, `hover:flex` +// and `focus:flex` will all use just `flex`. Variants are handled separately. +// +// E.g.: +// +// | UTILITY | GENERATED SIGNATURE | +// | ---------------- | ----------------------- | +// | `[display:flex]` | `.x { display: flex; }` | +// | `flex` | `.x { display: flex; }` | +// +// These produce the same signature, therefore they represent the same utility. +export const UTILITY_SIGNATURE_KEY = Symbol() +function createUtilitySignatureCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof UTILITY_SIGNATURE_KEY] { + return new DefaultMap((options: SignatureOptions) => { + return new DefaultMap((utility) => { + try { + // Ensure the prefix is added to the utility if it is not already present. + utility = + designSystem.theme.prefix && !utility.startsWith(designSystem.theme.prefix) + ? `${designSystem.theme.prefix}:${utility}` + : utility + + // Use `@apply` to normalize the selector to `.x` + let ast: AstNode[] = [styleRule('.x', [atRule('@apply', utility)])] + + temporarilyDisableThemeInline(designSystem, () => { + // There's separate utility caches for respect important vs not + // so we want to compile them both with `@theme inline` disabled + for (let candidate of designSystem.parseCandidate(utility)) { + designSystem.compileAstNodes(candidate, CompileAstFlags.RespectImportant) + } + + substituteAtApply(ast, designSystem) + }) + + // Optimize the AST. This is needed such that any internal intermediate + // nodes are gone. This will also cleanup declaration nodes with undefined + // values or `--tw-sort` declarations. + canonicalizeAst(designSystem, ast, options) + + // Compute the final signature, by generating the CSS for the utility + 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() + } + }) + }) +} + +// #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) { + let { rem } = options + + walk(ast, { + enter(node, ctx) { + // Optimize declarations + if (node.kind === 'declaration') { + if (node.value === undefined || node.property === '--tw-sort') { + return WalkAction.Replace([]) + } + + // Ignore `--tw-{property}` if `{property}` exists with the same value + if (node.property.startsWith('--tw-')) { + if ( + ctx.siblings.some( + (sibling) => + sibling.kind === 'declaration' && + node.value === sibling.value && + node.important === sibling.important && + !sibling.property.startsWith('--tw-'), + ) + ) { + return WalkAction.Replace([]) + } + } + + if (options.features & SignatureFeatures.ExpandProperties) { + let replacement = expandDeclaration(node, options.features) + if (replacement) return WalkAction.Replace(replacement) + } + + // Resolve theme values to their inlined value. + if (node.value.includes('var(')) { + node.value = resolveVariablesInValue(node.value, designSystem) + } + + let valueAst = ValueParser.parse(node.value) + + // Very basic `calc(…)` constant folding to handle the spacing scale + // multiplier: + // + // Input: `--spacing(4)` + // → `calc(var(--spacing, 0.25rem) * 4)` + // → `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) + } + + // We will normalize the `node.value`, this is the same kind of logic + // we use when printing arbitrary values. It will remove unnecessary + // whitespace. + // + // Essentially normalizing the `node.value` to a canonical form. + node.value = printArbitraryValue(node.value) + } + + // Replace special nodes with its children + else if (node.kind === 'context' || node.kind === 'at-root') { + return WalkAction.Replace(node.nodes) + } + + // Remove comments + else if (node.kind === 'comment') { + return WalkAction.Replace([]) + } + + // Remove at-rules that are not needed for the signature + else if (node.kind === 'at-rule' && node.name === '@property') { + return WalkAction.Replace([]) + } + }, + 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.: +// +// `[color:var(--color-red-500)]` → `[color:oklch(63.7%_0.237_25.331)]` +// `[color:oklch(63.7%_0.237_25.331)]` → `[color:oklch(63.7%_0.237_25.331)]` +// +// Due to the `@apply` from above, this will become: +// +// ```css +// .example { +// color: oklch(63.7% 0.237 25.331); +// } +// ``` +// +// Which conveniently will be equivalent to: `text-red-500` when we inline the +// value. +// +// Without inlining: +// ```css +// .example { +// color: var(--color-red-500, oklch(63.7% 0.237 25.331)); +// } +// ``` +// +// Inlined: +// ```css +// .example { +// color: oklch(63.7% 0.237 25.331); +// } +// ``` +// +// 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. +function resolveVariablesInValue(value: string, designSystem: DesignSystem): string { + let changed = false + let valueAst = ValueParser.parse(value) + + let seen = new Set() + walk(valueAst, (valueNode) => { + if (valueNode.kind !== 'function') return + if (valueNode.value !== 'var') return + + // Resolve the underlying value of the variable + if (valueNode.nodes.length !== 1 && valueNode.nodes.length < 3) { + return + } + + let variable = valueNode.nodes[0].value + + // Drop the prefix from the variable name if it is present. The + // internal variable doesn't have the prefix. + if (designSystem.theme.prefix && variable.startsWith(`--${designSystem.theme.prefix}-`)) { + variable = variable.slice(`--${designSystem.theme.prefix}-`.length) + } + let variableValue = designSystem.resolveThemeValue(variable) + // Prevent infinite recursion when the variable value contains the + // variable itself. + if (seen.has(variable)) return + 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. + // + // E.g.: + // + // ``` + // var(--font-sans, ui-sans-serif, system-ui, sans-serif, …) + // ``` + { + // More than 1 argument means that a fallback is already present + if (valueNode.nodes.length === 1) { + // Inject the fallback value into the variable lookup + changed = true + valueNode.nodes.push(...ValueParser.parse(`,${variableValue}`)) + } + } + + // Replace known variable + inlined fallback value with the value + // itself again + { + // We need at least 3 arguments. The variable, the separator and a fallback value. + if (valueNode.nodes.length >= 3) { + let nodeAsString = ValueParser.toCss(valueNode.nodes) // This could include more than just the variable + let constructedValue = `${valueNode.nodes[0].value},${variableValue}` + if (nodeAsString === constructedValue) { + changed = true + return WalkAction.Replace(ValueParser.parse(variableValue)) + } + } + } + }) + + // Replace the value with the new value + if (changed) return ValueParser.toCss(valueAst) + return value +} + +// Index all static utilities by property and value +const STATIC_UTILITIES_KEY = Symbol() +function createStaticUtilitiesCache(): DesignSystem['storage'][typeof STATIC_UTILITIES_KEY] { + return new DefaultMap((_optiones: SignatureOptions) => { + return new DefaultMap((_property: string) => { + return new DefaultMap((_value: string) => { + return new Set() + }) + }) + }) +} + +const UTILITY_PROPERTIES_KEY = Symbol() +function createUtilityPropertiesCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof UTILITY_PROPERTIES_KEY] { + return new DefaultMap((options: SignatureOptions) => { + return new DefaultMap((className) => { + let localPropertyValueLookup = new DefaultMap((_property) => new Set()) + + if (designSystem.theme.prefix && !className.startsWith(designSystem.theme.prefix)) { + className = `${designSystem.theme.prefix}:${className}` + } + 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) => { + if (node.kind === 'declaration') { + localPropertyValueLookup.get(node.property).add(node.value!) + designSystem.storage[STATIC_UTILITIES_KEY].get(options) + .get(node.property) + .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 + }) + }) +} + +// For all static utilities in the system, compute a lookup table that maps the +// utility signature to the utility name. This is used to find the utility name +// for a given utility signature. +// +// For all functional utilities, we can compute static-like utilities by +// essentially pre-computing the values and modifiers. This is a bit slow, but +// also only has to happen once per design system. +const PRE_COMPUTED_UTILITIES_KEY = Symbol() +function createPreComputedUtilitiesCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof PRE_COMPUTED_UTILITIES_KEY] { + return new DefaultMap((options: SignatureOptions) => { + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options) + let lookup = new DefaultMap(() => []) + + // Right now all plugins are implemented using functions so they are a black + // box. Let's use the `getClassList` and consider every known suggestion as a + // static utility for now. + for (let [className, meta] of designSystem.getClassList()) { + let signature = signatures.get(className) + if (typeof signature !== 'string') continue + + // Skip the utility if `-{utility}-0` has the same signature as + // `{utility}-0` (its positive version). This will prefer positive values + // over negative values. + if (className[0] === '-' && className.endsWith('-0')) { + let positiveSignature = signatures.get(className.slice(1)) + if (typeof positiveSignature === 'string' && signature === positiveSignature) { + continue + } + } + + lookup.get(signature).push(className) + designSystem.storage[UTILITY_PROPERTIES_KEY].get(options).get(className) + + for (let modifier of meta.modifiers) { + // Modifiers representing numbers can be computed and don't need to be + // pre-computed. Doing the math and at the time of writing this, this + // would save you 250k additionally pre-computed utilities... + if (isValidSpacingMultiplier(modifier)) { + continue + } + + let classNameWithModifier = `${className}/${modifier}` + let signature = signatures.get(classNameWithModifier) + if (typeof signature !== 'string') continue + lookup.get(signature).push(classNameWithModifier) + designSystem.storage[UTILITY_PROPERTIES_KEY].get(options).get(classNameWithModifier) + } + } + + return lookup + }) +} + +// Given a variant, compute a signature that represents the variant. The +// signature will be a normalised form of the generated CSS for the variant, or +// a unique symbol if the variant is not valid. The class in the selector will +// be replaced with `.x`. +// +// E.g.: +// +// | VARIANT | GENERATED SIGNATURE | +// | ---------------- | ----------------------------- | +// | `[&:focus]:flex` | `.x:focus { display: flex; }` | +// | `focus:flex` | `.x:focus { display: flex; }` | +// +// These produce the same signature, therefore they represent the same variant. +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 + + // 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) + + 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) => { + // 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`. + // + // 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') { + changed = true + } + + // 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)` + // + 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, 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() + } + }) + }) +} + +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(() => []) + + 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) + } + } + } + + return lookup + }) +} + +function temporarilyDisableThemeInline(designSystem: DesignSystem, cb: () => T): T { + // Turn off `@theme inline` feature such that `@theme` and `@theme inline` are + // considered the same. The biggest motivation for this is referencing + // variables in another namespace that happen to contain the same value as the + // utility's own namespaces it is reading from. + // + // E.g.: + // + // The `max-w-*` utility doesn't read from the `--breakpoint-*` namespace. + // But it does read from the `--container-*` namespace. It also happens to + // be the case that `--breakpoint-md` and `--container-3xl` are the exact + // same value. + // + // If you then use the `max-w-(--breakpoint-md)` utility, inlining the + // variable would mean: + // - `max-w-(--breakpoint-md)` → `max-width: 48rem;` → `max-w-3xl` + // - `max-w-(--contianer-3xl)` → `max-width: 48rem;` → `max-w-3xl` + // + // Not inlining the variable would mean: + // - `max-w-(--breakpoint-md)` → `max-width: var(--breakpoint-md);` → `max-w-(--breakpoint-md)` + // - `max-w-(--container-3xl)` → `max-width: var(--container-3xl);` → `max-w-3xl` + + // @ts-expect-error We are monkey-patching a method that's considered private + // in TypeScript + let originalGet = designSystem.theme.values.get + + // Track all values with the inline option set, so we can restore them later. + let restorableInlineOptions = new Set<{ options: ThemeOptions }>() + + // @ts-expect-error We are monkey-patching a method that's considered private + // in TypeScript + designSystem.theme.values.get = (key: string) => { + // @ts-expect-error We are monkey-patching a method that's considered private + // in TypeScript + let value = originalGet.call(designSystem.theme.values, key) + if (value === undefined) return value + + // Remove `inline` if it was set + if (value.options & ThemeOptions.INLINE) { + restorableInlineOptions.add(value) + value.options &= ~ThemeOptions.INLINE + } + + return value + } + + try { + // Run the callback with the `@theme inline` feature disabled + return cb() + } finally { + // Restore the `@theme inline` to the original value + // @ts-expect-error We are monkey-patching a method that's private + designSystem.theme.values.get = originalGet + + // Re-add the `inline` option, in case future lookups are done + for (let value of restorableInlineOptions) { + value.options |= ThemeOptions.INLINE + } + } +} + +// Generator that generates all combinations of the given set. Using a generator +// so we can stop early when we found a suitable combination. +// +// NOTE: +// +// 1. Yield biggest combinations first +// 2. Sets of size 1 and 0 are not yielded +function* combinations(arr: T[]): Generator { + let n = arr.length + let limit = 1n << BigInt(n) + + for (let k = n; k >= 2; k--) { + let mask = (1n << BigInt(k)) - 1n + + while (mask < limit) { + let out = [] + for (let i = 0; i < n; i++) { + if ((mask >> BigInt(i)) & 1n) { + out.push(arr[i]) + } + } + yield out + + // Gosper's hack: + // - https://programmingforinsomniacs.blogspot.com/2018/03/gospers-hack-explained.html + // - https://rosettacode.org/wiki/Gosper%27s_hack + // + // We need to generate the next mask in lexicographical order. + let carry = mask & -mask + let ripple = mask + carry + mask = (((ripple ^ mask) >> 2n) / carry) | ripple + } + } +} + +function intersection(a: Set, b: Set): Set { + // @ts-expect-error Set.prototype.intersection is only available in Node.js v22+ + if (typeof a.intersection === 'function') return a.intersection(b) + + // Polyfill for environments that do not support Set.prototype.intersection yet + if (a.size === 0 || b.size === 0) return new Set() + + let result = new Set(a) + for (let item of b) { + if (!result.has(item)) { + result.delete(item) + } + } + + return result +} diff --git a/packages/tailwindcss/src/cartesian.ts b/packages/tailwindcss/src/cartesian.ts new file mode 100644 index 000000000..f0f7bbba4 --- /dev/null +++ b/packages/tailwindcss/src/cartesian.ts @@ -0,0 +1,45 @@ +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 a781cdcb0..036b9d88e 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -1,7 +1,9 @@ import { Features } from '..' -import { styleRule, toCss, walk, WalkAction, type AstNode } from '../ast' +import { cssContext, styleRule, toCss, type AstNode } from '../ast' import type { DesignSystem } from '../design-system' +import type { SourceLocation } from '../source-maps/source' import { segment } from '../utils/segment' +import { walk, WalkAction } from '../walk' import { applyConfigToTheme } from './apply-config-to-theme' import { applyKeyframesToTheme } from './apply-keyframes-to-theme' import { createCompatConfig } from './config/create-compat-config' @@ -30,20 +32,32 @@ export async function applyCompatibilityHooks({ path: string, base: string, resourceHint: 'plugin' | 'config', - ) => Promise<{ module: any; base: string }> + ) => Promise<{ + path: string + base: string + module: any + }> sources: { base: string; pattern: string; negated: boolean }[] }) { let features = Features.None - let pluginPaths: [{ id: string; base: string; reference: boolean }, CssPluginOptions | null][] = - [] - let configPaths: { id: string; base: string; reference: boolean }[] = [] + let pluginPaths: [ + { id: string; base: string; reference: boolean; src: SourceLocation | undefined }, + CssPluginOptions | null, + ][] = [] + let configPaths: { + id: string + base: string + reference: boolean + src: SourceLocation | undefined + }[] = [] - walk(ast, (node, { parent, replaceWith, context }) => { + walk(ast, (node, _ctx) => { if (node.kind !== 'at-rule') return + let ctx = cssContext(_ctx) // Collect paths from `@plugin` at-rules if (node.name === '@plugin') { - if (parent !== null) { + if (ctx.parent !== null) { throw new Error('`@plugin` cannot be nested.') } @@ -96,13 +110,17 @@ export async function applyCompatibilityHooks({ } pluginPaths.push([ - { id: pluginPath, base: context.base as string, reference: !!context.reference }, + { + id: pluginPath, + base: ctx.context.base as string, + reference: !!ctx.context.reference, + src: node.src, + }, Object.keys(options).length > 0 ? options : null, ]) - replaceWith([]) features |= Features.JsPluginCompat - return + return WalkAction.Replace([]) } // Collect paths from `@config` at-rules @@ -111,18 +129,18 @@ export async function applyCompatibilityHooks({ throw new Error('`@config` cannot have a body.') } - if (parent !== null) { + if (ctx.parent !== null) { throw new Error('`@config` cannot be nested.') } configPaths.push({ id: node.params.slice(1, -1), - base: context.base as string, - reference: !!context.reference, + base: ctx.context.base as string, + reference: !!ctx.context.reference, + src: node.src, }) - replaceWith([]) features |= Features.JsPluginCompat - return + return WalkAction.Replace([]) } }) @@ -158,18 +176,19 @@ export async function applyCompatibilityHooks({ let [configs, pluginDetails] = await Promise.all([ Promise.all( - configPaths.map(async ({ id, base, reference }) => { + configPaths.map(async ({ id, base, reference, src }) => { let loaded = await loadModule(id, base, 'config') return { path: id, base: loaded.base, config: loaded.module as UserConfig, reference, + src, } }), ), Promise.all( - pluginPaths.map(async ([{ id, base, reference }, pluginOptions]) => { + pluginPaths.map(async ([{ id, base, reference, src }, pluginOptions]) => { let loaded = await loadModule(id, base, 'plugin') return { path: id, @@ -177,6 +196,7 @@ export async function applyCompatibilityHooks({ plugin: loaded.module as Plugin, options: pluginOptions, reference, + src, } }), ), @@ -211,6 +231,7 @@ function upgradeToFullPluginSupport({ base: string config: UserConfig reference: boolean + src: SourceLocation | undefined }[] pluginDetails: { path: string @@ -218,6 +239,7 @@ function upgradeToFullPluginSupport({ plugin: Plugin options: CssPluginOptions | null reference: boolean + src: SourceLocation | undefined }[] }) { let features = Features.None @@ -227,6 +249,7 @@ function upgradeToFullPluginSupport({ config: { plugins: [detail.plugin] }, base: detail.base, reference: detail.reference, + src: detail.src, } } @@ -235,6 +258,7 @@ function upgradeToFullPluginSupport({ config: { plugins: [detail.plugin(detail.options)] }, base: detail.base, reference: detail.reference, + src: detail.src, } } @@ -244,15 +268,32 @@ function upgradeToFullPluginSupport({ let userConfig = [...pluginConfigs, ...configs] let { resolvedConfig } = resolveConfig(designSystem, [ - { config: createCompatConfig(designSystem.theme), base, reference: true }, + { config: createCompatConfig(designSystem.theme), base, reference: true, src: undefined }, ...userConfig, - { config: { plugins: [darkModePlugin] }, base, reference: true }, + { config: { plugins: [darkModePlugin] }, base, reference: true, src: undefined }, ]) let { resolvedConfig: resolvedUserConfig, replacedThemeKeys } = resolveConfig( designSystem, userConfig, ) + let pluginApiConfig = { + designSystem, + ast, + resolvedConfig, + featuresRef: { + set current(value: number) { + features |= value + }, + }, + } + + let sharedPluginApi = buildPluginApi({ + ...pluginApiConfig, + referenceMode: false, + src: undefined, + }) + // Replace `resolveThemeValue` with a version that is backwards compatible // with dot-notation but also aware of any JS theme configurations registered // by plugins or JS config files. This is significantly slower than just @@ -266,49 +307,56 @@ function upgradeToFullPluginSupport({ return defaultResolveThemeValue(path, forceInline) } - let resolvedValue = pluginApi.theme(path, undefined) + 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) { - // When a tuple is returned, return the first element return resolvedValue[0] - } else if (Array.isArray(resolvedValue)) { - // Arrays get serialized into a comma-separated lists + } + + // Arrays get serialized into a comma-separated lists + else if (Array.isArray(resolvedValue)) { return resolvedValue.join(', ') - } else if (typeof resolvedValue === 'string') { - // Otherwise only allow string values here, objects (and namespace maps) - // are treated as non-resolved values for the CSS `theme()` function. + } + + // 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') { return resolvedValue } } - let pluginApiConfig = { - designSystem, - ast, - resolvedConfig, - featuresRef: { - set current(value: number) { - features |= value - }, - }, - } + for (let { handler, reference, src } of resolvedConfig.plugins) { + // Each plugin gets its own instance of the plugin API because nodes added + // to the AST may need to point to the `@config` or `@plugin` that they + // originated from + let api = buildPluginApi({ + ...pluginApiConfig, + referenceMode: reference ?? false, + src, + }) - let pluginApi = buildPluginApi({ ...pluginApiConfig, referenceMode: false }) - let referenceModePluginApi = undefined - - for (let { handler, reference } of resolvedConfig.plugins) { - if (reference) { - referenceModePluginApi ||= buildPluginApi({ ...pluginApiConfig, referenceMode: true }) - handler(referenceModePluginApi) - } else { - handler(pluginApi) - } + handler(api) } // Merge the user-configured theme keys into the design system. The compat // config would otherwise expand into namespaces like `background-color` which // core utilities already read from. applyConfigToTheme(designSystem, resolvedUserConfig, replacedThemeKeys) - applyKeyframesToTheme(designSystem, resolvedUserConfig, replacedThemeKeys) + applyKeyframesToTheme(designSystem, resolvedUserConfig) registerThemeVariantOverrides(resolvedUserConfig, designSystem) registerScreensConfig(resolvedUserConfig, designSystem) @@ -341,18 +389,18 @@ function upgradeToFullPluginSupport({ if (typeof resolvedConfig.important === 'string') { let wrappingSelector = resolvedConfig.important - walk(ast, (node, { replaceWith, parent }) => { + walk(ast, (node, _ctx) => { if (node.kind !== 'at-rule') return if (node.name !== '@tailwind' || node.params !== 'utilities') return + let ctx = cssContext(_ctx) + // The AST node was already manually wrapped so there's nothing to do - if (parent?.kind === 'rule' && parent.selector === wrappingSelector) { + if (ctx.parent?.kind === 'rule' && ctx.parent.selector === wrappingSelector) { return WalkAction.Stop } - replaceWith(styleRule(wrappingSelector, [node])) - - return WalkAction.Stop + return WalkAction.ReplaceStop(styleRule(wrappingSelector, [node])) }) } 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 619c99182..12f195e94 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -31,6 +31,16 @@ 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', }, @@ -52,6 +62,13 @@ test('config values can be merged into the theme', () => { lineHeight: '1.5', }, ], + lg: ['1.125rem', '2'], + xl: ['1.5rem', '3rem', 'invalid'], + '2xl': ['2rem'], + }, + + ringColor: { + DEFAULT: '#fff', }, letterSpacing: { @@ -74,12 +91,17 @@ 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', reference: false, + src: undefined, }, ]) applyConfigToTheme(design, resolvedConfig, replacedThemeKeys) @@ -102,13 +124,42 @@ test('config values can be merged into the theme', () => { '1rem', { '--line-height': '1.5' }, ]) - expect(theme.resolve('super-wide', ['--tracking'])).toEqual('0.25em') - expect(theme.resolve('super-loose', ['--leading'])).toEqual('3') + expect(theme.resolve('lg', ['--text'])).toEqual('1.125rem') + expect(theme.resolveWith('lg', ['--text'], ['--line-height'])).toEqual([ + '1.125rem', + { '--line-height': '2' }, + ]) + expect(theme.resolve('xl', ['--text'])).toEqual('1.5rem') + expect(theme.resolveWith('xl', ['--text'], ['--line-height'])).toEqual([ + '1.5rem', + { '--line-height': '3rem' }, + ]) + 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('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', () => { @@ -145,6 +196,8 @@ test('will reset default theme values with overwriting theme values', () => { }, }, base: '/root', + reference: false, + src: undefined, }, ]) applyConfigToTheme(design, resolvedConfig, replacedThemeKeys) @@ -174,6 +227,8 @@ test('invalid keys are not merged into the theme', () => { }, }, base: '/root', + reference: false, + src: undefined, }, ]) @@ -212,6 +267,8 @@ test('converts opacity modifiers from decimal to percentage values', () => { }, }, base: '/root', + reference: false, + src: undefined, }, ]) applyConfigToTheme(design, resolvedConfig, replacedThemeKeys) @@ -246,6 +303,7 @@ test('handles setting theme keys to null', async () => { }, base: '/root', reference: false, + src: undefined, }, ]) applyConfigToTheme(design, resolvedConfig, replacedThemeKeys) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index d1fec30bc..8757520ee 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -130,7 +130,16 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk } if (Array.isArray(value) && value.every((v) => typeof v === 'string')) { - toAdd.push([path, value.join(', ')]) + if (path[0] === 'fontSize') { + toAdd.push([path, value[0]]) + + if (value.length >= 2) { + toAdd.push([[...path, '-line-height'], value[1]]) + } + } else { + toAdd.push([path, value.join(', ')]) + } + return WalkAction.Skip } }) @@ -138,32 +147,57 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk return toAdd } -const IS_VALID_KEY = /^[a-zA-Z0-9-_%/\.]+$/ +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-_%/.]+$/ 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}` + // The legacy container component config should not be included in the Theme if (path[0] === 'container') return null - path = structuredClone(path) - - 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 } + // Map old v3 namespaces to new theme namespaces + let ns = OLD_TO_NEW_NAMESPACE[path[0]] + if (ns) { + path = path.slice() + path[0] = ns + } + return ( path // [1] should move into the nested object tuple. To create the CSS variable @@ -176,11 +210,27 @@ export function keyPathToCssProperty(path: string[]) { .map((path, idx, all) => (path === '1' && idx !== all.length - 1 ? '' : path)) // Resolve the key path to a CSS variable segment - .map((part) => - part - .replaceAll('.', '_') - .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`), - ) + .map((part, idx) => { + part = part.replaceAll('.', '_') + + 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 diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts index a0b1c54d5..aed761fce 100644 --- a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts @@ -9,7 +9,7 @@ test('keyframes can be merged into the theme', () => { let theme = new Theme() let design = buildDesignSystem(theme) - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ + let { resolvedConfig } = resolveConfig(design, [ { config: { theme: { @@ -28,9 +28,11 @@ test('keyframes can be merged into the theme', () => { }, }, base: '/root', + reference: false, + src: undefined, }, ]) - applyKeyframesToTheme(design, resolvedConfig, replacedThemeKeys) + applyKeyframesToTheme(design, resolvedConfig) expect(toCss(design.theme.getKeyframes())).toMatchInlineSnapshot(` "@keyframes fade-in { @@ -70,7 +72,7 @@ test('will append to the default keyframes with new keyframes', () => { ]), ) - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ + let { resolvedConfig } = resolveConfig(design, [ { config: { theme: { @@ -91,9 +93,11 @@ test('will append to the default keyframes with new keyframes', () => { }, }, base: '/root', + reference: false, + src: undefined, }, ]) - applyKeyframesToTheme(design, resolvedConfig, replacedThemeKeys) + applyKeyframesToTheme(design, resolvedConfig) expect(toCss(design.theme.getKeyframes())).toMatchInlineSnapshot(` "@keyframes slide-in { diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts index a58ca13df..d5855545f 100644 --- a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts @@ -6,7 +6,6 @@ import { objectToAst } from './plugin-api' export function applyKeyframesToTheme( designSystem: DesignSystem, resolvedConfig: Pick, - replacedThemeKeys: Set, ) { for (let rule of keyframesToRules(resolvedConfig)) { designSystem.theme.addKeyframes(rule) diff --git a/packages/tailwindcss/src/compat/colors.ts b/packages/tailwindcss/src/compat/colors.ts index 2f443b4b6..64e3bc9c7 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 0)', + '50': 'oklch(98.5% 0 none)', '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 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)', + '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)', }, stone: { '50': 'oklch(98.5% 0.001 106.423)', @@ -69,6 +69,58 @@ 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 b68e6dcff..1f69ae446 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1,6 +1,7 @@ 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 @@ -12,25 +13,28 @@ test('Config files can add content', async () => { ` let compiler = await compile(input, { - loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root' }), + loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root', path: '' }), }) expect(compiler.sources).toEqual([{ base: '/root', pattern: './file.txt', negated: false }]) }) test('Config files can change dark mode (media)', async () => { - let input = css` - @tailwind utilities; - @config "./config.js"; - ` - - let compiler = await compile(input, { - loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root' }), - }) - - expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` - ".dark\\:underline { - @media (prefers-color-scheme: dark) { + 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 { text-decoration-line: underline; } } @@ -39,73 +43,80 @@ test('Config files can change dark mode (media)', async () => { }) test('Config files can change dark mode (selector)', async () => { - let input = css` - @tailwind utilities; - @config "./config.js"; - ` - - let compiler = await compile(input, { - loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root' }), - }) - - expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` - ".dark\\:underline { - &:where(.dark, .dark *) { - text-decoration-line: underline; - } + 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; } " `) }) test('Config files can change dark mode (variant)', async () => { - let input = css` - @tailwind utilities; - @config "./config.js"; - ` - - let compiler = await compile(input, { - loadModule: async () => ({ - module: { darkMode: ['variant', '&:where(:not(.light))'] }, - base: '/root', - }), - }) - - expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` - ".dark\\:underline { - &:where(:not(.light)) { - text-decoration-line: underline; - } + 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; } " `) }) test('Config files can add plugins', async () => { - 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', - }, - }) - }), - ], + 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: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['no-scrollbar'])).toMatchInlineSnapshot(` - ".no-scrollbar { + ), + ).toMatchInlineSnapshot(` + " + .no-scrollbar { scrollbar-width: none; } " @@ -113,137 +124,181 @@ test('Config files can add plugins', async () => { }) test('Plugins loaded from config files can contribute to the config', async () => { - let input = css` - @tailwind utilities; - @config "./config.js"; - ` - - let compiler = await compile(input, { - loadModule: async () => ({ - module: { - plugins: [ - plugin(() => {}, { - darkMode: ['variant', '&:where(:not(.light))'], - }), - ], + expect( + await run( + ['dark:underline'], + css` + @tailwind utilities; + @config "./config.js"; + `, + { + loadModule: async () => ({ + module: { + plugins: [ + plugin(() => {}, { + darkMode: ['variant', '&:where(:not(.light))'], + }), + ], + }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` - ".dark\\:underline { - &:where(:not(.light)) { - text-decoration-line: underline; - } + ), + ).toMatchInlineSnapshot(` + " + .dark\\:underline:where(:not(.light)) { + text-decoration-line: underline; } " `) }) test('Config file presets can contribute to the config', async () => { - let input = css` - @tailwind utilities; - @config "./config.js"; - ` - - let compiler = await compile(input, { - loadModule: async () => ({ - module: { - presets: [ - { - darkMode: ['variant', '&:where(:not(.light))'], - }, - ], + expect( + await run( + ['dark:underline'], + css` + @tailwind utilities; + @config "./config.js"; + `, + { + loadModule: async () => ({ + module: { presets: [{ darkMode: ['variant', '&:where(:not(.light))'] }] }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` - ".dark\\:underline { - &:where(:not(.light)) { - text-decoration-line: underline; - } + ), + ).toMatchInlineSnapshot(` + " + .dark\\:underline:where(:not(.light)) { + text-decoration-line: underline; } " `) }) test('Config files can affect the theme', async () => { - let input = css` - @tailwind utilities; - @config "./config.js"; - ` - - let compiler = await compile(input, { - loadModule: async () => ({ - module: { - theme: { - extend: { - colors: { - primary: '#c0ffee', - }, - }, - }, - - plugins: [ - plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.scrollbar-primary': { - scrollbarColor: theme('colors.primary'), + expect( + await run( + ['bg-primary', 'scrollbar-primary'], + css` + @tailwind utilities; + @config "./config.js"; + `, + { + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + primary: '#c0ffee', + }, }, - }) - }), - ], - }, - base: '/root', - }), - }) + }, - expect(compiler.build(['bg-primary', 'scrollbar-primary'])).toMatchInlineSnapshot(` - ".bg-primary { - background-color: #c0ffee; - } + plugins: [ + plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.scrollbar-primary': { + scrollbarColor: theme('colors.primary'), + }, + }) + }), + ], + }, + base: '/root', + path: '', + }), + }, + ), + ).toMatchInlineSnapshot(` + " .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"; + + .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: var(--foo-foo-bar); + border-color: var(--foo-foo); + } " `) }) test('Variants in CSS overwrite variants from plugins', async () => { - 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)') - }), - ], + 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: '', + }), }, - base: '/root', - }), - }) - - 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; - } + ), + ).toMatchInlineSnapshot(` + " + .dark\\:underline.my-dark, .light\\:underline.my-light { + text-decoration-line: underline; } " `) @@ -253,154 +308,167 @@ describe('theme callbacks', () => { test('tuple values from the config overwrite `@theme default` tuple-ish values from the CSS theme', async ({ expect, }) => { - 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"; - ` + 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 compiler = await compile(input, { - loadModule: async () => ({ - module: { - theme: { - extend: { - fontSize: { - base: ['200rem', { lineHeight: '201rem' }], - md: ['200rem', { lineHeight: '201rem' }], - xl: ['200rem', { lineHeight: '201rem' }], + // 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], + }, + }), + }, }, - // Direct access - lineHeight: ({ theme }) => ({ - base: theme('fontSize.base[1].lineHeight'), - md: theme('fontSize.md[1].lineHeight'), - xl: theme('fontSize.xl[1].lineHeight'), - }), + 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; + } + } + } - // 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], - }, - }), - }, - }, + .prose [class~="lead-base"] { + font-size: 100rem; + line-height: 201rem; + } - plugins: [ - plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.prose': { - ...theme('typography'), - }, - }) - }), - ], - } satisfies Config, - base: '/root', - }), - }) + .prose [class~="lead-md"] { + font-size: 200rem; + line-height: 101rem; + } - 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; - } - } - .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; - } - } - } - " - `) + .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 + } + " + `) }) }) describe('theme overrides order', () => { test('user theme > js config > default theme', async () => { - 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', + 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: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['bg-red', 'bg-blue'])).toMatchInlineSnapshot(` - ":root, :host { + ), + ).toMatchInlineSnapshot(` + " + :root, :host { --color-blue: blue; } + .bg-blue { background-color: var(--color-blue); } + .bg-red { background-color: very-red; } @@ -409,131 +477,134 @@ describe('theme overrides order', () => { }) test('user theme > js config > default theme (with nested object)', async () => { - 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', + 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', + }, + }, }, }, - }, - }, - } satisfies Config, - base: '/root', - } - } 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', - } - } - }, - }) - - 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', - ]), + }, + { values: flattenColorPalette(theme('colors')) }, + ) + }), + base: '/root', + path: '', + } + } + }, + }, + ), ).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; } " `) @@ -542,31 +613,35 @@ describe('theme overrides order', () => { describe('default font family compatibility', () => { test('overriding `fontFamily.sans` sets `--default-font-family`', async () => { - 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', + 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', + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` - ".font-sans { + ), + ).toMatchInlineSnapshot(` + " + .font-sans { font-family: Potato Sans; } " @@ -576,33 +651,37 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans[1].fontFeatureSettings` sets `--default-font-feature-settings`', async ({ expect, }) => { - 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"' }], + 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"' }], + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` - ".font-sans { - font-family: Potato Sans; + ), + ).toMatchInlineSnapshot(` + " + .font-sans { font-feature-settings: "cv06"; + font-family: Potato Sans; } " `) @@ -611,33 +690,37 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans[1].fontVariationSettings` sets `--default-font-variation-settings`', async ({ expect, }) => { - 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' }], + 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' }], + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` - ".font-sans { + ), + ).toMatchInlineSnapshot(` + " + .font-sans { + font-variation-settings: "XHGT" .7; font-family: Potato Sans; - font-variation-settings: "XHGT" 0.7; } " `) @@ -646,37 +729,41 @@ 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, }) => { - 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' }, - ], + 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' }, + ], + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` - ".font-sans { - font-family: Potato Sans; + ), + ).toMatchInlineSnapshot(` + " + .font-sans { font-feature-settings: "cv06"; - font-variation-settings: "XHGT" 0.7; + font-variation-settings: "XHGT" .7; + font-family: Potato Sans; } " `) @@ -685,36 +772,41 @@ describe('default font family compatibility', () => { test('overriding `--font-family-sans` in `@theme` without `default` preserves the original `--default-font-*` values', async ({ expect, }) => { - 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', + 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: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` - ":root, :host { + ), + ).toMatchInlineSnapshot(` + " + :root, :host { --font-sans: Sandwich Sans; } + .font-sans { font-family: var(--font-sans); } @@ -725,31 +817,35 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans` in a config file with an array sets `--default-font-family`', async ({ expect, }) => { - 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'], + 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'], + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` - ".font-sans { + ), + ).toMatchInlineSnapshot(` + " + .font-sans { font-family: Inter, system-ui, sans-serif; } " @@ -759,58 +855,67 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans` in a config file with an unexpected type is ignored', async ({ expect, }) => { - 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' }, + 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' }, + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(`""`) + ), + ).toEqual('') }) test('overriding `fontFamily.mono` sets `--default-mono-font-family`', async () => { - 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', + 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', + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` - ".font-mono { + ), + ).toMatchInlineSnapshot(` + " + .font-mono { font-family: Potato Mono; } " @@ -820,33 +925,39 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.mono[1].fontFeatureSettings` sets `--default-mono-font-feature-settings`', async ({ expect, }) => { - 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"' }], + 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"' }], + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` - ".font-mono { - font-family: Potato Mono; + ), + ).toMatchInlineSnapshot(` + " + .font-mono { font-feature-settings: "cv06"; + font-family: Potato Mono; } " `) @@ -855,33 +966,39 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.mono[1].fontVariationSettings` sets `--default-mono-font-variation-settings`', async ({ expect, }) => { - 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' }], + 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' }], + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` - ".font-mono { + ), + ).toMatchInlineSnapshot(` + " + .font-mono { + font-variation-settings: "XHGT" .7; font-family: Potato Mono; - font-variation-settings: "XHGT" 0.7; } " `) @@ -890,37 +1007,41 @@ 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, }) => { - 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' }, - ], + 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' }, + ], + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` - ".font-mono { - font-family: Potato Mono; + ), + ).toMatchInlineSnapshot(` + " + .font-mono { font-feature-settings: "cv06"; - font-variation-settings: "XHGT" 0.7; + font-variation-settings: "XHGT" .7; + font-family: Potato Mono; } " `) @@ -929,36 +1050,41 @@ describe('default font family compatibility', () => { test('overriding `--font-family-mono` in `@theme` without `default` preserves the original `--default-mono-font-*` values', async ({ expect, }) => { - 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', + 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: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` - ":root, :host { + ), + ).toMatchInlineSnapshot(` + " + :root, :host { --font-mono: Sandwich Mono; } + .font-mono { font-family: var(--font-mono); } @@ -969,115 +1095,117 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.mono` in a config file with an unexpected type is ignored', async ({ expect, }) => { - 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' }, + 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' }, + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(`""`) + ), + ).toEqual('') }) }) test('creates variants for `data`, `supports`, and `aria` theme options at the same level as the core utility ', async () => { - let input = css` - @tailwind utilities; - @config "./config.js"; - ` + expect( + await run( + [ + 'aria-polite:underline', + 'supports-child-combinator:underline', + 'supports-foo:underline', + 'data-checked:underline', - 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"', + // 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"', + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - 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"] { + " + .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 { display: 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 { + + @media print { + .print\\:flex { display: flex; } } @@ -1086,62 +1214,66 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s }) test('merges css breakpoints with js config screens', async () => { - 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', + 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', + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) + ), + ).toMatchInlineSnapshot(` + " + @media (min-width: 44rem) { + .sm\\:flex { + display: flex; + } - 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 not all and (min-width: 50rem) { + .min-sm\\:max-md\\:underline { + text-decoration-line: underline; } } + } + + @media (min-width: 50rem) { .md\\:flex { - @media (width >= 50rem) { - display: flex; - } + display: flex; } + } + + @media (min-width: 64rem) { .lg\\:flex { - @media (width >= 64rem) { - display: flex; - } + display: flex; } - " - `) + } + " + `) }) test('utilities must be prefixed', async () => { @@ -1154,72 +1286,70 @@ test('utilities must be prefixed', async () => { } ` - let compiler = await compile(input, { - loadModule: async (id, base) => ({ + let options: Partial[1]> = { + loadModule: async (_id, base) => ({ + path: '', base, module: { prefix: 'tw' }, }), - }) + } // Prefixed utilities are generated - expect(compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom'])) + expect(await run(['tw:underline', 'tw:hover:line-through', 'tw:custom'], input, options)) .toMatchInlineSnapshot(` - ".tw\\:custom { - color: red; - } - .tw\\:underline { - text-decoration-line: underline; - } - .tw\\:hover\\:line-through { - &:hover { - @media (hover: hover) { + " + .tw\\:custom { + color: red; + } + + .tw\\:underline { + text-decoration-line: underline; + } + + @media (hover: hover) { + .tw\\:hover\\:line-through:hover { text-decoration-line: line-through; } } - } - " - `) + " + `) // Non-prefixed utilities are ignored - compiler = await compile(input, { - loadModule: async (id, base) => ({ - base, - module: { prefix: 'tw' }, - }), - }) - - expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('') + expect(await run(['underline', 'hover:line-through', 'custom'], input, options)).toEqual('') }) test('utilities used in @apply must be prefixed', async () => { - let compiler = await compile( - css` - @config "./config.js"; - - .my-underline { - @apply tw:underline; - } - `, - { - loadModule: async (id, base) => ({ - base, - module: { prefix: 'tw' }, - }), - }, - ) - // Prefixed utilities are generated - expect(compiler.build([])).toMatchInlineSnapshot(` - ".my-underline { + expect( + await compileCss( + css` + @config "./config.js"; + + .my-underline { + @apply tw:underline; + } + `, + { + loadModule: async (_id, base) => ({ + path: '', + base, + module: { prefix: 'tw' }, + }), + }, + ), + ).toMatchInlineSnapshot(` + " + .my-underline { text-decoration-line: underline; } " `) // Non-prefixed utilities cause an error - expect(() => + await expect( compile( css` + @tailwind utilities; @config "./config.js"; .my-underline { @@ -1227,46 +1357,50 @@ test('utilities used in @apply must be prefixed', async () => { } `, { - loadModule: async (id, base) => ({ + loadModule: async (_id, base) => ({ + path: '', base, module: { prefix: 'tw' }, }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Cannot apply unknown utility class: underline]`, + `[Error: Cannot apply unprefixed utility class \`underline\`. Did you mean \`tw:underline\`?]`, ) }) test('Prefixes configured in CSS take precedence over those defined in JS configs', async () => { - 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 { - base, - module: { prefix: 'tw' }, + expect( + await run( + ['wat:custom'], + css` + @theme prefix(wat) { + --color-red: #f00; + --color-green: #0f0; + --breakpoint-sm: 640px; } - }, - }, - ) - expect(compiler.build(['wat:custom'])).toMatchInlineSnapshot(` - ".wat\\:custom { + @config "./plugin.js"; + + @tailwind utilities; + + @utility custom { + color: red; + } + `, + { + async loadModule(_id, base) { + return { + path: '', + base, + module: { prefix: 'tw' }, + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .wat\\:custom { color: red; } " @@ -1280,8 +1414,9 @@ test('a prefix must be letters only', async () => { @config "./plugin.js"; `, { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, module: { prefix: '__' }, } @@ -1294,36 +1429,38 @@ test('a prefix must be letters only', async () => { }) test('important: `#app`', async () => { - let input = css` - @tailwind utilities; - @config "./config.js"; + expect( + await run( + ['underline', 'hover:line-through', 'custom'], + css` + @tailwind utilities; + @config "./config.js"; - @utility custom { + @utility custom { + color: red; + } + `, + { + loadModule: async (_id, base) => ({ + path: '', + base, + module: { important: '#app' }, + }), + }, + ), + ).toMatchInlineSnapshot(` + " + #app .custom { color: red; } - ` - let compiler = await compile(input, { - loadModule: async (_, base) => ({ - base, - module: { important: '#app' }, - }), - }) + #app .underline { + text-decoration-line: underline; + } - 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; - } - } + @media (hover: hover) { + #app .hover\\:line-through:hover { + text-decoration-line: line-through; } } " @@ -1331,34 +1468,38 @@ test('important: `#app`', async () => { }) test('important: true', async () => { - let input = css` - @tailwind utilities; - @config "./config.js"; + expect( + await run( + ['underline', 'hover:line-through', 'custom'], + css` + @tailwind utilities; + @config "./config.js"; - @utility custom { - color: red; - } - ` - - let compiler = await compile(input, { - loadModule: async (_, base) => ({ - base, - module: { important: true }, - }), - }) - - expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` - ".custom { + @utility custom { + color: red; + } + `, + { + loadModule: async (_id, base) => ({ + path: '', + base, + module: { important: true }, + }), + }, + ), + ).toMatchInlineSnapshot(` + " + .custom { color: red !important; } + .underline { text-decoration-line: underline !important; } - .hover\\:line-through { - &:hover { - @media (hover: hover) { - text-decoration-line: line-through !important; - } + + @media (hover: hover) { + .hover\\:line-through:hover { + text-decoration-line: line-through !important; } } " @@ -1366,46 +1507,49 @@ test('important: true', async () => { }) test('blocklisted candidates are not generated', async () => { - let compiler = await compile( - css` - @theme reference { - --color-white: #fff; - --breakpoint-md: 48rem; + 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'], + }, } - @tailwind utilities; - @config "./config.js"; - `, - { - async loadModule(id, base) { - return { - base, - module: { - blocklist: ['bg-white'], - }, - } - }, }, - ) + } // bg-white will not get generated - expect(compiler.build(['bg-white'])).toEqual('') + expect(await run(['bg-white'], input, options)).toEqual('') // underline will as will md:bg-white - 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); + expect(await run(['underline', 'bg-white', 'md:bg-white'], input, options)) + .toMatchInlineSnapshot(` + " + .underline { + text-decoration-line: underline; } - } - " - `) + + @media (min-width: 48rem) { + .md\\:bg-white { + background-color: var(--color-white, #fff); + } + } + " + `) }) test('blocklisted candidates cannot be used with `@apply`', async () => { - await expect(() => + await expect( compile( css` @theme reference { @@ -1419,8 +1563,9 @@ test('blocklisted candidates cannot be used with `@apply`', async () => { } `, { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, module: { blocklist: ['bg-white'], @@ -1430,12 +1575,12 @@ test('blocklisted candidates cannot be used with `@apply`', async () => { }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Cannot apply unknown utility class: bg-white]`, + `[Error: Cannot apply utility class \`bg-white\` because it has been explicitly disabled: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes]`, ) }) test('old theme values are merged with their renamed counterparts in the CSS theme', async () => { - let didCallPluginFn = vi.fn() + using didCallPluginFn = vi.fn() await compile( css` @@ -1471,8 +1616,9 @@ 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, module: plugin(function ({ theme }) { didCallPluginFn() @@ -1558,52 +1704,146 @@ test('old theme values are merged with their renamed counterparts in the CSS the }) test('handles setting theme keys to null', async () => { - 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, + 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, + }, }, }, }, - }, - base: '/root', - } + base: '/root', + path: '', + } + }, }, - }, - ) - - 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); + ), + ).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); } + .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); } " `) }) + +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; + } + + @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; + } + + .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 new file mode 100644 index 000000000..aa1b21cb7 --- /dev/null +++ b/packages/tailwindcss/src/compat/config/create-compat-config.test.ts @@ -0,0 +1,189 @@ +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.test.ts b/packages/tailwindcss/src/compat/config/resolve-config.test.ts index e47fa4eda..d8cc21a10 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.test.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.test.ts @@ -20,6 +20,8 @@ test('top level theme keys are replaced', () => { }, }, base: '/root', + reference: false, + src: undefined, }, { config: { @@ -30,6 +32,8 @@ test('top level theme keys are replaced', () => { }, }, base: '/root', + reference: false, + src: undefined, }, { config: { @@ -40,6 +44,8 @@ test('top level theme keys are replaced', () => { }, }, base: '/root', + reference: false, + src: undefined, }, ]) @@ -73,6 +79,8 @@ test('theme can be extended', () => { }, }, base: '/root', + reference: false, + src: undefined, }, { config: { @@ -85,6 +93,8 @@ test('theme can be extended', () => { }, }, base: '/root', + reference: false, + src: undefined, }, ]) @@ -120,6 +130,8 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, base: '/root', + reference: false, + src: undefined, }, { config: { @@ -133,6 +145,8 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, base: '/root', + reference: false, + src: undefined, }, { config: { @@ -145,6 +159,8 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, base: '/root', + reference: false, + src: undefined, }, ]) @@ -212,6 +228,8 @@ test('theme keys can read from the CSS theme', () => { }, }, base: '/root', + reference: false, + src: undefined, }, ]) @@ -274,6 +292,7 @@ test('handles null as theme values', () => { }, base: '/root', reference: false, + src: undefined, }, { config: { @@ -287,6 +306,7 @@ test('handles null as theme values', () => { }, base: '/root', reference: false, + src: undefined, }, ]) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 6801c7b60..858f4bbbe 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -1,4 +1,5 @@ import type { DesignSystem } from '../../design-system' +import type { SourceLocation } from '../../source-maps/source' import colors from '../colors' import type { PluginWithConfig } from '../plugin-api' import { createThemeFn } from '../plugin-functions' @@ -16,6 +17,7 @@ export interface ConfigFile { base: string config: UserConfig reference: boolean + src: SourceLocation | undefined } interface ResolutionContext { @@ -31,6 +33,7 @@ interface ResolutionContext { let minimal: ResolvedConfig = { blocklist: [], future: {}, + experimental: {}, prefix: '', important: false, darkMode: null, @@ -131,7 +134,7 @@ export type PluginUtils = { function extractConfigs( ctx: ResolutionContext, - { config, base, path, reference }: ConfigFile, + { config, base, path, reference, src }: ConfigFile, ): void { let plugins: PluginWithConfig[] = [] @@ -140,17 +143,17 @@ function extractConfigs( if ('__isOptionsFunction' in plugin) { // Happens with `plugin.withOptions()` when no options were passed: // e.g. `require("my-plugin")` instead of `require("my-plugin")(options)` - plugins.push({ ...plugin(), reference }) + plugins.push({ ...plugin(), reference, src }) } else if ('handler' in plugin) { // Happens with `plugin(…)`: // e.g. `require("my-plugin")` // // or with `plugin.withOptions()` when the user passed options: // e.g. `require("my-plugin")(options)` - plugins.push({ ...plugin, reference }) + plugins.push({ ...plugin, reference, src }) } else { // Just a plain function without using the plugin(…) API - plugins.push({ handler: plugin, reference }) + plugins.push({ handler: plugin, reference, src }) } } @@ -162,7 +165,7 @@ function extractConfigs( } for (let preset of config.presets ?? []) { - extractConfigs(ctx, { path, base, config: preset, reference }) + extractConfigs(ctx, { path, base, config: preset, reference, src }) } // Apply configs from plugins @@ -170,7 +173,13 @@ function extractConfigs( ctx.plugins.push(plugin) if (plugin.config) { - extractConfigs(ctx, { path, base, config: plugin.config, reference: !!plugin.reference }) + extractConfigs(ctx, { + path, + base, + config: plugin.config, + reference: !!plugin.reference, + src: plugin.src ?? src, + }) } } diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts index dbcb30de3..76851a397 100644 --- a/packages/tailwindcss/src/compat/config/types.ts +++ b/packages/tailwindcss/src/compat/config/types.ts @@ -52,7 +52,7 @@ type DarkModeStrategy = // Use the `class` strategy with a custom class instead of `.dark`. | ['class', string] - // Use the `selector` strategy — same as `class` but uses `:where()` for more predicable behavior + // Use the `selector` strategy — same as `class` but uses `:where()` for more predictable behavior | 'selector' // Use the `selector` strategy with a custom selector instead of `.dark`. @@ -105,3 +105,12 @@ 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 b5618de84..39a12557c 100644 --- a/packages/tailwindcss/src/compat/container-config.test.ts +++ b/packages/tailwindcss/src/compat/container-config.test.ts @@ -1,54 +1,74 @@ import { expect, test } from 'vitest' -import { compile } from '..' +import { run } from '../test-utils/run' const css = String.raw test('creates a custom utility to extend the built-in container', 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: { - container: { - center: true, - padding: '2rem', + 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: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['container'])).toMatchInlineSnapshot(` - ".container { + ), + ).toMatchInlineSnapshot(` + " + .container { width: 100%; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: 40rem; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .container { max-width: 48rem; } - @media (width >= 64rem) { + } + + @media (min-width: 64rem) { + .container { max-width: 64rem; } - @media (width >= 80rem) { + } + + @media (min-width: 80rem) { + .container { max-width: 80rem; } - @media (width >= 96rem) { + } + + @media (min-width: 96rem) { + .container { max-width: 96rem; } } + .container { margin-inline: auto; padding-inline: 2rem; @@ -58,61 +78,87 @@ test('creates a custom utility to extend the built-in container', async () => { }) test('allows padding to be defined at custom breakpoints', 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: { - container: { - padding: { - // The order here is messed up on purpose - '2xl': '3rem', - DEFAULT: '1rem', - lg: '2rem', + 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', + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['container'])).toMatchInlineSnapshot(` - ".container { + ), + ).toMatchInlineSnapshot(` + " + .container { width: 100%; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: 40rem; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .container { max-width: 48rem; } - @media (width >= 64rem) { + } + + @media (min-width: 64rem) { + .container { max-width: 64rem; } - @media (width >= 80rem) { + } + + @media (min-width: 80rem) { + .container { max-width: 80rem; } - @media (width >= 96rem) { + } + + @media (min-width: 96rem) { + .container { max-width: 96rem; } } + .container { padding-inline: 1rem; - @media (width >= 64rem) { + } + + @media (min-width: 64rem) { + .container { padding-inline: 2rem; } - @media (width >= 96rem) { + } + + @media (min-width: 96rem) { + .container { padding-inline: 3rem; } } @@ -121,61 +167,87 @@ test('allows padding to be defined at custom breakpoints', async () => { }) test('allows breakpoints to be overwritten', 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: { - container: { - screens: { - xl: '1280px', - '2xl': '1536px', + 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', + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['container'])).toMatchInlineSnapshot(` - ".container { + ), + ).toMatchInlineSnapshot(` + " + .container { width: 100%; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: 40rem; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .container { max-width: 48rem; } - @media (width >= 64rem) { + } + + @media (min-width: 64rem) { + .container { max-width: 64rem; } - @media (width >= 80rem) { + } + + @media (min-width: 80rem) { + .container { max-width: 80rem; } - @media (width >= 96rem) { + } + + @media (min-width: 96rem) { + .container { max-width: 96rem; } } - .container { - @media (width >= 40rem) { + + @media (min-width: 40rem) { + .container { max-width: none; } - @media (width >= 1280px) { + } + + @media (min-width: 1280px) { + .container { max-width: 1280px; } - @media (width >= 1536px) { + } + + @media (min-width: 1536px) { + .container { max-width: 1536px; } } @@ -184,61 +256,84 @@ test('allows breakpoints to be overwritten', async () => { }) test('padding applies to custom `container` screens', 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: { - container: { - padding: { - sm: '2rem', - md: '3rem', - }, - screens: { - md: '48rem', + 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', + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['container'])).toMatchInlineSnapshot(` - ".container { + ), + ).toMatchInlineSnapshot(` + " + .container { width: 100%; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: 40rem; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .container { max-width: 48rem; } - @media (width >= 64rem) { + } + + @media (min-width: 64rem) { + .container { max-width: 64rem; } - @media (width >= 80rem) { + } + + @media (min-width: 80rem) { + .container { max-width: 80rem; } - @media (width >= 96rem) { + } + + @media (min-width: 96rem) { + .container { max-width: 96rem; } } - .container { - @media (width >= 40rem) { + + @media (min-width: 40rem) { + .container { max-width: none; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .container { max-width: 48rem; padding-inline: 3rem; } @@ -248,58 +343,81 @@ 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 () => { - 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: { - DEFAULT: '1rem', - sm: '2rem', - md: '3rem', + 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: {}, + }, }, - screens: {}, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['container'])).toMatchInlineSnapshot(` - ".container { + ), + ).toMatchInlineSnapshot(` + " + .container { width: 100%; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: 40rem; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .container { max-width: 48rem; } - @media (width >= 64rem) { + } + + @media (min-width: 64rem) { + .container { max-width: 64rem; } - @media (width >= 80rem) { + } + + @media (min-width: 80rem) { + .container { max-width: 80rem; } - @media (width >= 96rem) { + } + + @media (min-width: 96rem) { + .container { max-width: 96rem; } } + .container { padding-inline: 1rem; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: none; } } @@ -308,45 +426,50 @@ 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 () => { - 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', + 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', + }, + }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['max-w-sm'])).toMatchInlineSnapshot(` - ":root, :host { + ), + ).toMatchInlineSnapshot(` + " + :root, :host { --container-sm: 24rem; } + .max-w-sm { max-width: var(--container-sm); } @@ -355,107 +478,168 @@ test('legacy container component does not interfere with new --container variabl }) test('combines custom padding and screen overwrites', 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: { - container: { - center: true, - padding: { - DEFAULT: '2rem', - '2xl': '4rem', - }, - screens: { - md: '48rem', // Matches a default --breakpoint - xl: '1280px', - '2xl': '1536px', + 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', + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['container', '!container'])).toMatchInlineSnapshot(` - ".\\!container { + ), + ).toMatchInlineSnapshot(` + " + .\\!container { width: 100% !important; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .\\!container { max-width: 40rem !important; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .\\!container { max-width: 48rem !important; } - @media (width >= 64rem) { + } + + @media (min-width: 64rem) { + .\\!container { max-width: 64rem !important; } - @media (width >= 80rem) { + } + + @media (min-width: 80rem) { + .\\!container { max-width: 80rem !important; } - @media (width >= 96rem) { + } + + @media (min-width: 96rem) { + .\\!container { max-width: 96rem !important; } } + .container { width: 100%; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: 40rem; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .container { max-width: 48rem; } - @media (width >= 64rem) { + } + + @media (min-width: 64rem) { + .container { max-width: 64rem; } - @media (width >= 80rem) { + } + + @media (min-width: 80rem) { + .container { max-width: 80rem; } - @media (width >= 96rem) { + } + + @media (min-width: 96rem) { + .container { max-width: 96rem; } } + .\\!container { margin-inline: auto !important; padding-inline: 2rem !important; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .\\!container { max-width: none !important; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .\\!container { max-width: 48rem !important; } - @media (width >= 1280px) { + } + + @media (min-width: 1280px) { + .\\!container { max-width: 1280px !important; } - @media (width >= 1536px) { + } + + @media (min-width: 1536px) { + .\\!container { max-width: 1536px !important; padding-inline: 4rem !important; } } + .container { margin-inline: auto; padding-inline: 2rem; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: none; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .container { max-width: 48rem; } - @media (width >= 1280px) { + } + + @media (min-width: 1280px) { + .container { max-width: 1280px; } - @media (width >= 1536px) { + } + + @media (min-width: 1536px) { + .container { max-width: 1536px; padding-inline: 4rem; } @@ -465,74 +649,106 @@ test('combines custom padding and screen overwrites', async () => { }) test('filters out complex breakpoints', 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: { - 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' }, + 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' }, + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - expect(compiler.build(['container'])).toMatchInlineSnapshot(` - ".container { + ), + ).toMatchInlineSnapshot(` + " + .container { width: 100%; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: 40rem; } - @media (width >= 48rem) { + } + + @media (min-width: 48rem) { + .container { max-width: 48rem; } - @media (width >= 64rem) { + } + + @media (min-width: 64rem) { + .container { max-width: 64rem; } - @media (width >= 80rem) { + } + + @media (min-width: 80rem) { + .container { max-width: 80rem; } - @media (width >= 96rem) { + } + + @media (min-width: 96rem) { + .container { max-width: 96rem; } } + .container { margin-inline: auto; padding-inline: 2rem; - @media (width >= 40rem) { + } + + @media (min-width: 40rem) { + .container { max-width: none; } - @media (width >= 20px) { + } + + @media (min-width: 20px) { + .container { max-width: 20px; } - @media (width >= 100px) { + } + + @media (min-width: 100px) { + .container { max-width: 100px; } - @media (width >= 300px) { + } + + @media (min-width: 300px) { + .container { max-width: 300px; } } diff --git a/packages/tailwindcss/src/compat/container.ts b/packages/tailwindcss/src/compat/container.ts index 3cf72c4db..98f23ce7a 100644 --- a/packages/tailwindcss/src/compat/container.ts +++ b/packages/tailwindcss/src/compat/container.ts @@ -1,4 +1,4 @@ -import { atRule, decl, type AstNode, type AtRule } from '../ast' +import { atRule, cloneAstNode, decl, type AstNode, type AtRule } from '../ast' import type { DesignSystem } from '../design-system' import { compareBreakpoints } from '../utils/compare-breakpoints' import type { ResolvedConfig } from './config/types' @@ -16,7 +16,7 @@ export function registerContainerCompat(userConfig: ResolvedConfig, designSystem return } - designSystem.utilities.static('container', () => structuredClone(rules)) + designSystem.utilities.static('container', () => rules.map(cloneAstNode)) } export function buildCustomContainerUtilityRules( @@ -50,7 +50,7 @@ export function buildCustomContainerUtilityRules( if (typeof screens === 'object' && screens !== null) { breakpointOverwrites = new Map() - // When setting a the `screens` in v3, you were overwriting the default + // When setting the `screens` in v3, you were overwriting the default // screens config. To do this in v4, you have to manually unset all core // screens. diff --git a/packages/tailwindcss/src/compat/default-theme.ts b/packages/tailwindcss/src/compat/default-theme.ts index f540e63a2..90e699c1c 100644 --- a/packages/tailwindcss/src/compat/default-theme.ts +++ b/packages/tailwindcss/src/compat/default-theme.ts @@ -353,8 +353,13 @@ export default { }, fontFamily: { sans: [ - 'ui-sans-serif', - 'system-ui', + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + '"Noto Sans"', + 'Arial', '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 9a828d673..c90eae845 100644 --- a/packages/tailwindcss/src/compat/legacy-utilities.test.ts +++ b/packages/tailwindcss/src/compat/legacy-utilities.test.ts @@ -1,27 +1,23 @@ import { expect, test } from 'vitest' -import { compileCss, run } from '../test-utils/run' +import { run } from '../test-utils/run' const css = String.raw test('bg-gradient-*', async () => { expect( - 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', - ], - ), + 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', + ]), ).toMatchInlineSnapshot(` - ".bg-gradient-to-b { + " + .bg-gradient-to-b { --tw-gradient-position: to bottom in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } @@ -59,13 +55,21 @@ 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 compileCss( + await run( + [ + 'max-w-screen-sm', + 'max-w-screen-md', + 'max-w-screen-lg', + 'max-w-screen-xl', + 'max-w-screen-2xl', + ], css` @theme { --breakpoint-sm: 40rem; @@ -76,16 +80,10 @@ 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; @@ -111,7 +109,8 @@ test('max-w-screen', async () => { .max-w-screen-xl { max-width: var(--breakpoint-xl); - }" + } + " `) expect( await run([ @@ -134,7 +133,8 @@ 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,21 +142,25 @@ 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; } @@ -166,7 +170,8 @@ test('flex-grow', async () => { .flex-grow-\\[123\\] { flex-grow: 123; - }" + } + " `) expect( await run([ @@ -185,7 +190,8 @@ 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; } @@ -195,7 +201,8 @@ test('flex-shrink', async () => { .flex-shrink-\\[123\\] { flex-shrink: 123; - }" + } + " `) expect( await run([ @@ -211,3 +218,181 @@ 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 e3f9436d1..8e7829035 100644 --- a/packages/tailwindcss/src/compat/legacy-utilities.ts +++ b/packages/tailwindcss/src/compat/legacy-utilities.ts @@ -1,6 +1,7 @@ import { decl } from '../ast' import type { DesignSystem } from '../design-system' -import { isPositiveInteger } from '../utils/infer-data-type' +import { isPositiveInteger, isValidSpacingMultiplier } from '../utils/infer-data-type' +import { segment } from '../utils/segment' export function registerLegacyUtilities(designSystem: DesignSystem) { for (let [value, direction] of [ @@ -13,6 +14,7 @@ export function registerLegacyUtilities(designSystem: DesignSystem) { ['l', 'left'], ['tl', 'top left'], ]) { + designSystem.utilities.suggest(`bg-gradient-to-${value}`, () => []) designSystem.utilities.static(`bg-gradient-to-${value}`, () => [ decl('--tw-gradient-position', `to ${direction} in oklab`), decl('background-image', `linear-gradient(var(--tw-gradient-stops))`), @@ -20,25 +22,34 @@ export function registerLegacyUtilities(designSystem: DesignSystem) { } // Legacy `background-position` utilities for compatibility with v4.0 and earlier + designSystem.utilities.suggest('bg-left-top', () => []) designSystem.utilities.static('bg-left-top', () => [decl('background-position', 'left top')]) + designSystem.utilities.suggest('bg-right-top', () => []) designSystem.utilities.static('bg-right-top', () => [decl('background-position', 'right top')]) + designSystem.utilities.suggest('bg-left-bottom', () => []) designSystem.utilities.static('bg-left-bottom', () => [ decl('background-position', 'left bottom'), ]) + designSystem.utilities.suggest('bg-right-bottom', () => []) designSystem.utilities.static('bg-right-bottom', () => [ decl('background-position', 'right bottom'), ]) // Legacy `object-position` utilities for compatibility with v4.0 and earlier + designSystem.utilities.suggest('object-left-top', () => []) designSystem.utilities.static('object-left-top', () => [decl('object-position', 'left top')]) + designSystem.utilities.suggest('object-right-top', () => []) designSystem.utilities.static('object-right-top', () => [decl('object-position', 'right top')]) + designSystem.utilities.suggest('object-left-bottom', () => []) designSystem.utilities.static('object-left-bottom', () => [ decl('object-position', 'left bottom'), ]) + designSystem.utilities.suggest('object-right-bottom', () => []) designSystem.utilities.static('object-right-bottom', () => [ decl('object-position', 'right bottom'), ]) + designSystem.utilities.suggest('max-w-screen', () => []) designSystem.utilities.functional('max-w-screen', (candidate) => { if (!candidate.value) return if (candidate.value.kind === 'arbitrary') return @@ -47,18 +58,22 @@ export function registerLegacyUtilities(designSystem: DesignSystem) { return [decl('max-width', value)] }) + designSystem.utilities.suggest('overflow-ellipsis', () => []) designSystem.utilities.static(`overflow-ellipsis`, () => [decl('text-overflow', `ellipsis`)]) + designSystem.utilities.suggest('decoration-slice', () => []) designSystem.utilities.static(`decoration-slice`, () => [ decl('-webkit-box-decoration-break', `slice`), decl('box-decoration-break', `slice`), ]) + designSystem.utilities.suggest('decoration-clone', () => []) designSystem.utilities.static(`decoration-clone`, () => [ decl('-webkit-box-decoration-break', `clone`), decl('box-decoration-break', `clone`), ]) + designSystem.utilities.suggest('flex-shrink', () => []) designSystem.utilities.functional('flex-shrink', (candidate) => { if (candidate.modifier) return @@ -75,6 +90,7 @@ export function registerLegacyUtilities(designSystem: DesignSystem) { } }) + designSystem.utilities.suggest('flex-grow', () => []) designSystem.utilities.functional('flex-grow', (candidate) => { if (candidate.modifier) return @@ -90,4 +106,69 @@ export function registerLegacyUtilities(designSystem: DesignSystem) { return [decl('flex-grow', candidate.value.value)] } }) + + designSystem.utilities.suggest('order-none', () => []) + designSystem.utilities.static('order-none', () => [decl('order', '0')]) + + 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 dc415eb1a..5fcacb5cd 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 { optimizeCss } from '../test-utils/run' +import { compileCss, run } from '../test-utils/run' import defaultTheme from './default-theme' import type { CssInJs, PluginAPI } from './plugin-api' @@ -9,57 +9,61 @@ const css = String.raw describe('theme', async () => { test('plugin theme can contain objects', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - 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))', + 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))', + }, + }, }, }, }, }, - }, - }, - ), - } - }, - }) - - expect(compiler.build([])).toMatchInlineSnapshot(` - "@layer base { + ), + } + }, + }, + ), + ).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); @@ -72,30 +76,29 @@ describe('theme', async () => { }) test('keyframes added via addUtilities are appended to the AST', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin(function ({ addUtilities, theme }) { - addUtilities({ - '@keyframes enter': { - from: { - opacity: 'var(--tw-enter-opacity, 1)', - }, - }, - }) - }), - } - }, - }) - - expect(compiler.build([])).toMatchInlineSnapshot(` - "@keyframes enter { + 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 { from { opacity: var(--tw-enter-opacity, 1); } @@ -105,47 +108,40 @@ describe('theme', async () => { }) test('plugin theme can extend colors', async () => { - let input = css` - @theme reference { - --color-red-500: #ef4444; - } - @tailwind utilities; - @plugin "my-plugin"; - ` - - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), + 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') }, + ) }, - { - values: theme('colors'), - }, - ) - }, - { - theme: { - extend: { - colors: { - 'russet-700': '#7a4724', - }, - }, - }, - }, - ), - } - }, - }) - - expect(compiler.build(['scrollbar-red-500', 'scrollbar-russet-700'])).toMatchInlineSnapshot(` - ".scrollbar-red-500 { + { theme: { extend: { colors: { 'russet-700': '#7a4724' } } } }, + ), + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .scrollbar-red-500 { scrollbar-color: #ef4444; } + .scrollbar-russet-700 { scrollbar-color: #7a4724; } @@ -156,45 +152,43 @@ describe('theme', async () => { test('plugin theme values can reference legacy theme keys that have been replaced with bare value support', async ({ expect, }) => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-duration': (value) => ({ 'animation-duration': value }), + 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') }, + ) }, { - values: theme('animationDuration'), - }, - ) - }, - { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => { - return { - ...theme('transitionDuration'), - } + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => { + return { ...theme('transitionDuration') } + }, + }, }, }, - }, - }, - ), - } - }, - }) - - expect(compiler.build(['animate-duration-316'])).toMatchInlineSnapshot(` - ".animate-duration-316 { - animation-duration: 316ms; + ), + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .animate-duration-316 { + animation-duration: .316s; } " `) @@ -203,96 +197,92 @@ describe('theme', async () => { test('plugin theme values that support bare values are merged with other values for that theme key', async ({ expect, }) => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-duration': (value) => ({ 'animation-duration': value }), + 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') }, + ) }, { - values: theme('animationDuration'), - }, - ) - }, - { - theme: { - extend: { - transitionDuration: { - slow: '800ms', + theme: { + extend: { + transitionDuration: { slow: '800ms' }, + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + }, }, - - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), }, - }, - }, - ), - } - }, - }) - - expect(compiler.build(['animate-duration-316', 'animate-duration-slow'])) - .toMatchInlineSnapshot(` - ".animate-duration-316 { - animation-duration: 316ms; + ), + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .animate-duration-316 { + animation-duration: .316s; } + .animate-duration-slow { - animation-duration: 800ms; + animation-duration: .8s; } " `) }) test('plugin theme can have opacity modifiers', async () => { - let input = css` - @tailwind utilities; - @theme { - --color-red-500: #ef4444; + 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); } - @plugin "my-plugin"; - ` - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - 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)) { + } + + @supports (color: color-mix(in lab, red, red)) { + .variable { color: color-mix(in oklab, #ef4444 var(--opacity), transparent); } } @@ -301,88 +291,81 @@ 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 { - 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( - compiler.build([ - 'bg-custom', - 'css-percentage', - 'css-fraction', - 'css-variable', - 'js-percentage', - 'js-fraction', - 'js-variable', - ]), + 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 / )', + }, + }, + }, + ), + } + }, + }, + ), ).toMatchInlineSnapshot(` - ".css-fraction { - color: color-mix(in oklab, rgba(255 0 0 / ) 50%, transparent); - } - .css-percentage { + " + .css-fraction, .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)) { + } + + @supports (color: color-mix(in lab, red, red)) { + .css-variable { color: color-mix(in oklab, rgba(255 0 0 / ) var(--opacity), transparent); } } - .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-fraction, .js-percentage { + color: oklab(62.7955% .224 .125 / .5); } + .js-variable { - 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); + color: red; + } + + @supports (color: color-mix(in lab, red, red)) { + .js-variable { + color: color-mix(in oklab, red var(--opacity), transparent); } } " @@ -390,255 +373,226 @@ describe('theme', async () => { }) test('theme value functions are resolved correctly regardless of order', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-delay': (value) => ({ 'animation-delay': value }), + 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') }, + ) }, { - values: theme('animationDelay'), - }, - ) - }, - { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), - - animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('animationDuration'), - }), - - transitionDuration: { - slow: '800ms', + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('animationDuration'), + }), + transitionDuration: { slow: '800ms' }, + }, }, }, - }, - }, - ), - } - }, - }) - - expect(compiler.build(['animate-delay-316', 'animate-delay-slow'])).toMatchInlineSnapshot(` - ".animate-delay-316 { - animation-delay: 316ms; + ), + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .animate-delay-316 { + animation-delay: .316s; } + .animate-delay-slow { - animation-delay: 800ms; + animation-delay: .8s; } " `) }) test('plugins can override the default key', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-duration': (value) => ({ 'animation-delay': value }), + 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') }, + ) }, - { - values: theme('transitionDuration'), - }, - ) - }, - { - theme: { - extend: { - transitionDuration: { - DEFAULT: '1500ms', - }, - }, - }, - }, - ), - } - }, - }) - - expect(compiler.build(['animate-duration'])).toMatchInlineSnapshot(` - ".animate-duration { - animation-delay: 1500ms; + { theme: { extend: { transitionDuration: { DEFAULT: '1500ms' } } } }, + ), + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .animate-duration { + animation-delay: 1.5s; } " `) }) test('plugins can read CSS theme keys using the old theme key notation', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - @theme reference { - --animate: pulse 1s linear infinite; - --animate-spin: spin 1s linear infinite; + 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 compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin(function ({ matchUtilities, theme }) { - matchUtilities( - { - animation: (value) => ({ animation: value }), - }, - { - values: theme('animation'), - }, - ) + .animation-spin { + animation: 1s linear infinite spin; + } - matchUtilities( - { - animation2: (value) => ({ animation: value }), - }, - { - values: { - DEFAULT: theme('animation.DEFAULT'), - twist: theme('animation.spin'), - }, - }, - ) - }), - } - }, - }) + .animation2 { + animation: 1s linear infinite pulse; + } - 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; - } - " - `) + .animation2-twist { + animation: 1s linear infinite spin; + } + " + `) }) test('CSS theme values are merged with JS theme values', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - @theme reference { - --animate: pulse 1s linear infinite; - --animate-spin: spin 1s linear infinite; + 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') }, + ) + }, + { theme: { extend: { animation: { bounce: 'bounce 1s linear infinite' } } } }, + ), + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .animation { + --animation: pulse 1s linear infinite; } - ` - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - animation: (value) => ({ '--animation': value }), - }, - { - values: theme('animation'), - }, - ) - }, - { - theme: { - extend: { - animation: { - bounce: 'bounce 1s linear infinite', - }, - }, - }, - }, - ), - } - }, - }) + .animation-bounce { + --animation: bounce 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; - } - " - `) + .animation-spin { + --animation: spin 1s linear infinite; + } + " + `) }) test('CSS theme defaults take precedence over JS theme defaults', async () => { - 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 { - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - animation: (value) => ({ '--animation': value }), + 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') }, + ) }, - { - values: theme('animation'), - }, - ) - }, - { - theme: { - extend: { - animation: { - DEFAULT: 'twist 1s linear infinite', - }, - }, - }, - }, - ), - } - }, - }) - - expect(compiler.build(['animation'])).toMatchInlineSnapshot(` - ".animation { + { theme: { extend: { animation: { DEFAULT: 'twist 1s linear infinite' } } } }, + ), + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .animation { --animation: pulse 1s linear infinite; } " @@ -655,24 +609,19 @@ describe('theme', async () => { } ` - let fn = vi.fn() + using fn = vi.fn() await compile(input, { - loadModule: async (id, base) => { + loadModule: async (_id, base) => { return { + path: '', base, module: plugin( function ({ theme }) { fn(theme('animation.simple')) }, { - theme: { - extend: { - animation: { - simple: 'simple 1s linear', - }, - }, - }, + theme: { extend: { animation: { simple: 'simple 1s linear' } } }, }, ), } @@ -686,240 +635,259 @@ describe('theme', async () => { }) }) - test('all necessary theme keys support bare values', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let { build } = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin(function ({ matchUtilities, theme }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - { values: theme(themeKey) }, - ) + 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; + } - 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') - }), - } - }, - }) + .example-foo-bar { + color: #00f; + } + " + `) + }) - 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', - ]) + 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) }, + ) + } - expect(output).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') + }), + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .my-aspect-2\\/5 { --value: 2/5; } - .my-backdrop-brightness-1 { - --value: 1%; - } - .my-backdrop-contrast-1 { - --value: 1%; - } - .my-backdrop-grayscale-1 { + + .my-backdrop-brightness-1, .my-backdrop-contrast-1, .my-backdrop-grayscale-1 { --value: 1%; } + .my-backdrop-hue-rotate-1 { --value: 1deg; } - .my-backdrop-invert-1 { - --value: 1%; - } - .my-backdrop-opacity-1 { - --value: 1%; - } - .my-backdrop-saturate-1 { - --value: 1%; - } - .my-backdrop-sepia-1 { + + .my-backdrop-invert-1, .my-backdrop-opacity-1, .my-backdrop-saturate-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-grow-1, .my-flex-shrink-1 { --value: 1; } - .my-flex-shrink-1 { - --value: 1; - } - .my-gradient-color-stop-positions-1 { + + .my-gradient-color-stop-positions-1, .my-grayscale-1 { --value: 1%; } - .my-grayscale-1 { - --value: 1%; - } - .my-grid-row-end-1 { + + .my-grid-row-end-1, .my-grid-row-start-1 { --value: 1; } - .my-grid-row-start-1 { - --value: 1; - } - .my-grid-template-columns-1 { - --value: repeat(1, minmax(0, 1fr)); - } - .my-grid-template-rows-1 { + + .my-grid-template-columns-1, .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 { - --value: 1px; - } - .my-outline-width-1 { - --value: 1px; - } - .my-ring-offset-width-1 { - --value: 1px; - } - .my-ring-width-1 { + + .my-outline-offset-1, .my-outline-width-1, .my-ring-offset-width-1, .my-ring-width-1 { --value: 1px; } + .my-rotate-1 { --value: 1deg; } - .my-saturate-1 { - --value: 1%; - } - .my-scale-1 { - --value: 1%; - } - .my-sepia-1 { + + .my-saturate-1, .my-scale-1, .my-sepia-1 { --value: 1%; } + .my-skew-1 { --value: 1deg; } + .my-stroke-width-1 { --value: 1; } - .my-text-decoration-thickness-1 { + + .my-text-decoration-thickness-1, .my-text-underline-offset-1 { --value: 1px; } - .my-text-underline-offset-1 { - --value: 1px; - } - .my-transition-delay-1 { - --value: 1ms; - } - .my-transition-duration-1 { + + .my-transition-delay-1, .my-transition-duration-1 { --value: 1ms; } + .my-z-index-1 { --value: 1; } @@ -937,11 +905,12 @@ describe('theme', async () => { } ` - let fn = vi.fn() + using fn = vi.fn() await compile(input, { - loadModule: async (id, base) => { + loadModule: async (_id, base) => { return { + path: '', base, module: plugin( ({ theme }) => { @@ -979,11 +948,12 @@ describe('theme', async () => { } ` - let fn = vi.fn() + using fn = vi.fn() await compile(input, { - loadModule: async (id, base) => { + loadModule: async (_id, base) => { return { + path: '', base, module: plugin(({ theme }) => { fn(theme('transitionTimingFunction.DEFAULT')) @@ -1010,11 +980,12 @@ describe('theme', async () => { } ` - let fn = vi.fn() + using fn = vi.fn() await compile(input, { - loadModule: async (id, base) => { + loadModule: async (_id, base) => { return { + path: '', base, module: plugin(({ theme }) => { fn(theme('color.red.100')) @@ -1038,11 +1009,12 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let fn = vi.fn() + using fn = vi.fn() await compile(input, { - loadModule: async (id, base) => { + loadModule: async (_id, base) => { return { + path: '', base, module: plugin(({ theme }) => { fn(theme('i.do.not.exist')) @@ -1061,271 +1033,242 @@ describe('theme', async () => { }) test('Candidates can match multiple utility definitions', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let { build } = await compile(input, { - loadModule: async (id, base) => { - return { - 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; - } + 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(` + " .foo-bar { color: red; - } - .foo-bar { --my-prop: bar-valuer; + background-color: red; } " `) }) test('spreading `tailwindcss/defaultTheme` exports keeps bare values', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` + 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 { build } = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin(function ({ matchUtilities }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - // @ts-ignore - { values: defaultTheme[themeKey] }, - ) + 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') + }), } - - 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 { + }, + }, + ), + ).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-grow-1, .my-flex-shrink-1 { --value: 1; } - .my-flex-shrink-1 { - --value: 1; - } - .my-gradient-color-stop-positions-1 { + + .my-gradient-color-stop-positions-1, .my-grayscale-1 { --value: 1%; } - .my-grayscale-1 { - --value: 1%; - } - .my-grid-row-end-1 { + + .my-grid-row-end-1, .my-grid-row-start-1 { --value: 1; } - .my-grid-row-start-1 { - --value: 1; - } - .my-grid-template-columns-1 { - --value: repeat(1, minmax(0, 1fr)); - } - .my-grid-template-rows-1 { + + .my-grid-template-columns-1, .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 { - --value: 1px; - } - .my-outline-width-1 { - --value: 1px; - } - .my-ring-offset-width-1 { - --value: 1px; - } - .my-ring-width-1 { + + .my-outline-offset-1, .my-outline-width-1, .my-ring-offset-width-1, .my-ring-width-1 { --value: 1px; } + .my-rotate-1 { --value: 1deg; } - .my-saturate-1 { - --value: 1%; - } - .my-scale-1 { - --value: 1%; - } - .my-sepia-1 { + + .my-saturate-1, .my-scale-1, .my-sepia-1 { --value: 1%; } + .my-skew-1 { --value: 1deg; } + .my-stroke-width-1 { --value: 1; } - .my-text-decoration-thickness-1 { + + .my-text-decoration-thickness-1, .my-text-underline-offset-1 { --value: 1px; } - .my-text-underline-offset-1 { - --value: 1px; - } - .my-transition-delay-1 { - --value: 1ms; - } - .my-transition-duration-1 { + + .my-transition-delay-1, .my-transition-duration-1 { --value: 1ms; } + .my-z-index-1 { --value: 1; } @@ -1334,190 +1277,211 @@ describe('theme', async () => { }) test('can use escaped JS variables in theme values', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - 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', + 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', + }, + }, }, }, - }, - }, - ), - } - }, - }) + ), + } + }, + }, + ), + ).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: 0.375rem; + width: .375rem; } + .my-width-1\\/2 { width: 60%; } " - `, - ) + `) }) test('can use escaped CSS variables in theme values', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; + expect( + await run( + ['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5'], + 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; + @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; } - ` - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin(function ({ matchUtilities, theme }) { - matchUtilities( - { 'my-width': (value) => ({ width: value }) }, - { values: theme('width') }, - ) - }), - } - }, - }) + .my-width-1\\.5 { + width: .375rem; + } - 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; - } - " - `) + .my-width-1\\/2 { + width: 60%; + } + + .my-width-2\\.5 { + width: .625rem; + } + " + `) }) test('can use escaped CSS variables in referenced theme namespace', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; + expect( + await run( + ['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5'], + 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; + @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; } - ` - let compiler = await compile(input, { - loadModule: async (id, base) => { - return { - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'my-width': (value) => ({ width: value }) }, - { values: theme('myWidth') }, - ) - }, - { - theme: { myWidth: ({ theme }) => theme('width') }, - }, - ), - } - }, - }) + .my-width-1\\.5 { + width: .375rem; + } - 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; - } - " - `) + .my-width-1\\/2 { + width: 60%; + } + + .my-width-2\\.5 { + width: .625rem; + } + " + `) }) }) describe('addBase', () => { test('does not create rules when imported via `@import "…" reference`', async () => { - let input = css` - @tailwind utilities; - @plugin "outside"; - @import './inside.css' reference; - ` - - let compiler = await compile(input, { - loadModule: async (id, base) => { - if (id === 'inside') { - return { - base, - module: plugin(function ({ addBase }) { - addBase({ inside: { color: 'red' } }) - }), - } - } - return { - base, - module: plugin(function ({ addBase }) { - addBase({ outside: { color: 'red' } }) - }), - } - }, - async loadStylesheet() { - return { - content: css` - @plugin "inside"; - `, - base: '', - } - }, - }) - - expect(compiler.build([])).toMatchInlineSnapshot(` - "@layer base { + 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 { outside { color: red; } @@ -1527,27 +1491,24 @@ describe('addBase', () => { }) test('does not modify CSS variables', async () => { - let input = css` - @plugin "my-plugin"; - ` - - let compiler = await compile(input, { - loadModule: async () => ({ - module: plugin(function ({ addBase }) { - addBase({ - ':root': { - '--PascalCase': '1', - '--camelCase': '1', - '--UPPERCASE': '1', - }, - }) - }), - base: '/root', - }), - }) - - expect(compiler.build([])).toMatchInlineSnapshot(` - "@layer base { + 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 { :root { --PascalCase: 1; --camelCase: 1; @@ -1557,150 +1518,321 @@ 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 () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', '&:hover, &:focus') - }, + expect( + await run( + ['hocus:underline', 'group-hocus:flex'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } + `, + { + loadModule: async (_id, base) => { + return { + path: '', + base, + 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) *) { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { + :is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) { 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 () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', ['&:hover', '&:focus']) - }, + expect( + await run( + ['hocus:underline', 'group-hocus:flex'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } + `, + { + loadModule: async (_id, base) => { + return { + path: '', + base, + 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 *) { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { + :is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - }" + } + " `) }) test('addVariant with object syntax and @slot', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&:hover': '@slot', - '&:focus': '@slot', - }) - }, + expect( + await run( + ['hocus:underline', 'group-hocus:flex'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } + `, + { + loadModule: async (_id, base) => { + return { + path: '', + base, + 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 *) { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { + :is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - }" + } + " `) }) test('addVariant with object syntax, media, nesting and multiple @slot', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '@media (hover: hover)': { - '&:hover': '@slot', - }, - '&:focus': '@slot', - }) - }, + expect( + await run( + ['hocus:underline', 'group-hocus:flex'], + 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', + }) + }, + } + }, }, - }, - ) - let compiled = build(['hocus:underline', 'group-hocus:flex']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (hover: hover) { - .group-hocus\\:flex:is(:where(.group):hover *) { + :is(:where(.group):hover .group-hocus\\:flex) { display: flex; } } - .group-hocus\\:flex:is(:where(.group):focus *) { + :is(:where(.group):focus .group-hocus\\:flex) { display: flex; } @@ -1713,36 +1845,39 @@ describe('addVariant', () => { .hocus\\:underline:focus { text-decoration-line: underline; } - }" + } + " `) }) test('addVariant with at-rules and placeholder', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ addVariant }: PluginAPI) => { - addVariant( - 'potato', - '@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }', - ) - }, + expect( + await run( + ['potato:underline', 'potato:flex'], + 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 } }', + ) + }, + } + }, }, - }, - ) - let compiled = build(['potato:underline', 'potato:flex']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (max-width: 400px) { @supports (font: bold) { .potato\\:flex:large-potato { @@ -1754,39 +1889,42 @@ describe('addVariant', () => { } } } - }" + } + " `) }) test('@slot is preserved when used as a custom property value', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&': { - '--custom-property': '@slot', - '&:hover': '@slot', - '&:focus': '@slot', - }, - }) - }, + expect( + await run( + ['hocus:underline'], + 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', + }, + }) + }, + } + }, }, - }, - ) - let compiled = build(['hocus:underline']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { .hocus\\:underline { --custom-property: @slot; } @@ -1794,35 +1932,73 @@ 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; + } + `, + { + 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 { + display: flex; + } + } + " `) }) }) describe('matchVariant', () => { test('partial arbitrary variants', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `.potato-${flavor} &`) - }, + expect( + await run( + ['potato-[yellow]:underline', 'potato-[baked]:flex'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } + `, + { + loadModule: async (_id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `.potato-${flavor} &`) + }, + } + }, }, - }, - ) - let compiled = build(['potato-[yellow]:underline', 'potato-[baked]:flex']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { .potato-baked .potato-\\[baked\\]\\:flex { display: flex; } @@ -1830,33 +2006,36 @@ describe('matchVariant', () => { .potato-yellow .potato-\\[yellow\\]\\:underline { text-decoration-line: underline; } - }" + } + " `) }) test('partial arbitrary variants with at-rules', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) - }, + expect( + await run( + ['potato-[yellow]:underline', 'potato-[baked]:flex'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } + `, + { + loadModule: async (_id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) + }, + } + }, }, - }, - ) - let compiled = build(['potato-[yellow]:underline', 'potato-[baked]:flex']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (potato: baked) { .potato-\\[baked\\]\\:flex { display: flex; @@ -1868,37 +2047,38 @@ describe('matchVariant', () => { text-decoration-line: underline; } } - }" + } + " `) }) test('partial arbitrary variants with at-rules and placeholder', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant( - 'potato', - (flavor) => - `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }`, - ) - }, + expect( + await run( + ['potato-[yellow]:underline', 'potato-[baked]:flex'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } + `, + { + loadModule: async (_id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => { + return `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }` + }) + }, + } + }, }, - }, - ) - let compiled = build(['potato-[yellow]:underline', 'potato-[baked]:flex']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (potato: baked) { @supports (font: bold) { .potato-\\[baked\\]\\:flex:large-potato { @@ -1914,38 +2094,41 @@ describe('matchVariant', () => { } } } - }" + } + " `) }) test('partial arbitrary variants with default values', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('tooltip', (side) => `&${side}`, { - values: { - bottom: '[data-location="bottom"]', - top: '[data-location="top"]', - }, - }) - }, + expect( + await run( + ['tooltip-bottom:underline', 'tooltip-top:flex'], + 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"]', + }, + }) + }, + } + }, }, - }, - ) - let compiled = build(['tooltip-bottom:underline', 'tooltip-top:flex']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { .tooltip-bottom\\:underline[data-location="bottom"] { text-decoration-line: underline; } @@ -1953,115 +2136,120 @@ describe('matchVariant', () => { .tooltip-top\\:flex[data-location="top"] { display: flex; } - }" + } + " `) }) test('matched variant values maintain the sort order they are registered in', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - 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"]', - }, - }) - }, + expect( + await run( + [ + 'alphabet-c:underline', + 'alphabet-a:underline', + 'alphabet-d:underline', + 'alphabet-b:underline', + ], + 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"]', + }, + }) + }, + } + }, }, - }, - ) - let compiled = build([ - 'alphabet-c:underline', - 'alphabet-a:underline', - 'alphabet-d:underline', - 'alphabet-b:underline', - ]) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).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 () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('test', (selector) => - selector.split(',').map((selector) => `&.${selector} > *`), - ) - }, + expect( + await run( + ['test-[a,b,c]:underline'], + 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} > *`), + ) + }, + } + }, }, - }, - ) - let compiled = build(['test-[a,b,c]:underline']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).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 () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - }, + expect( + await run( + ['testmin-[600px]:flex', 'testmin-[500px]:underline', 'testmin-[700px]:italic'], + 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 compiled = build([ - 'testmin-[600px]:flex', - 'testmin-[500px]:underline', - 'testmin-[700px]:italic', - ]) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (min-width: 500px) { .testmin-\\[500px\\]\\:underline { text-decoration-line: underline; @@ -2079,44 +2267,41 @@ describe('matchVariant', () => { font-style: italic; } } - }" + } + " `) }) test('should be possible to compare arbitrary variants and hardcoded variants', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - values: { - example: '600px', - }, - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - }, + expect( + await run( + ['testmin-[700px]:italic', 'testmin-example:italic', 'testmin-[500px]:italic'], + 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) + }, + }) + }, + } + }, }, - }, - ) - let compiled = build([ - 'testmin-[700px]:italic', - 'testmin-example:italic', - 'testmin-[500px]:italic', - ]) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (min-width: 500px) { .testmin-\\[500px\\]\\:italic { font-style: italic; @@ -2134,49 +2319,51 @@ describe('matchVariant', () => { font-style: italic; } } - }" + } + " `) }) test('should be possible to sort stacked arbitrary variants correctly', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - 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) - }, - }) - }, + 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) + }, + }) + + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, + } + }, }, - }, - ) - - 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 { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (min-width: 100px) { @media (max-width: 400px) { .testmin-\\[100px\\]\\:testmax-\\[400px\\]\\:order-1 { @@ -2206,47 +2393,50 @@ describe('matchVariant', () => { } } } - }" + } + " `) }) test('should maintain sort from other variants, if sort functions of arbitrary variants return 0', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - 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) - }, - }) - }, - } - }, - }, - ) - 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 { + 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) + }, + }) + + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (min-width: 100px) { @media (max-width: 200px) { @media (hover: hover) { @@ -2260,47 +2450,50 @@ describe('matchVariant', () => { } } } - }" + } + " `) }) test('should sort arbitrary variants left to right (1)', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - 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) - }, - }) - }, + 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; } + `, + { + 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) + }, + }) + }, + } + }, }, - }, - ) - 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 { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (min-width: 100px) { @media (max-width: 400px) { .testmin-\\[100px\\]\\:testmax-\\[400px\\]\\:order-1 { @@ -2332,47 +2525,50 @@ describe('matchVariant', () => { } } } - }" + } + " `) }) test('should sort arbitrary variants left to right (2)', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - 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) - }, - }) - }, + 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; } + `, + { + 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) + }, + }) + }, + } + }, }, - }, - ) - 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 { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (max-width: 400px) { @media (min-width: 100px) { .testmax-\\[400px\\]\\:testmin-\\[100px\\]\\:underline { @@ -2400,55 +2596,58 @@ describe('matchVariant', () => { } } } - }" + } + " `) }) test('should guarantee that we are not passing values from other variants to the wrong function', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - 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) - }, - }) - }, + 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; } + `, + { + 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) + }, + }) + }, + } + }, }, - }, - ) - 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 { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { @media (min-width: 100px) { @media (max-width: 400px) { .testmin-\\[100px\\]\\:testmax-\\[400px\\]\\:order-1 { @@ -2480,165 +2679,178 @@ describe('matchVariant', () => { } } } - }" + } + " `) }) test('should default to the DEFAULT value for variants', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`, { - values: { - DEFAULT: '.bar', - }, - }) - }, + expect( + await run( + ['foo:underline'], + 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' }, + }) + }, + } + }, }, - }, - ) - let compiled = build(['foo:underline']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).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 () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`) - }, + expect( + await run( + ['foo:underline'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } + `, + { + loadModule: async (_id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value} &`) + }, + } + }, }, - }, - ) - let compiled = build(['foo:underline']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`"@layer utilities;"`) + ), + ).toMatchInlineSnapshot(` + " + @layer utilities; + " + `) }) test('should be possible to use `null` as a DEFAULT value', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { - values: { DEFAULT: null }, - }) - }, + expect( + await run( + ['foo:underline'], + 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 }, + }) + }, + } + }, }, - }, - ) - let compiled = build(['foo:underline']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { .foo-good .foo\\:underline { text-decoration-line: underline; } - }" + } + " `) }) test('should be possible to use `undefined` as a DEFAULT value', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (id, base) => { - return { - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { - values: { DEFAULT: undefined }, - }) - }, + expect( + await run( + ['foo:underline'], + 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 }, + }) + }, + } + }, }, - }, - ) - let compiled = build(['foo:underline']) - - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { .foo-good .foo\\:underline { text-decoration-line: underline; } - }" + } + " `) }) test('should be called with eventual modifiers', async () => { - let { build } = await compile( - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - loadModule: async (id, base) => { - return { - 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 + 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 - 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})` : [] + }) + }, + } + }, }, - }, - ) - let compiled = build([ - 'my-container-[250px]:underline', - 'my-container-[250px]/placement:underline', - ]) - expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` - "@container (min-width: 250px) { + ), + ).toMatchInlineSnapshot(` + " + @container (min-width: 250px) { .my-container-\\[250px\\]\\:underline { text-decoration-line: underline; } @@ -2648,230 +2860,372 @@ 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; + } + `, + { + 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 { + 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; + } + `, + { + loadModule: async (_id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (flavor) => `&:is(${flavor})`, { + values: { known: 'known' }, + }) + }, + } + }, + }, + ), + ).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; + } + `, + { + 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 { + .foo-string\\:flex:is(some string), .foo-\\[test\\]\\:flex:is(test) { + display: flex; + } + } + " `) }) }) describe('addUtilities()', () => { test('custom static utility', async () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - - @theme reference { - --breakpoint-lg: 1024px; - } - `, - { - async loadModule(id, base) { - return { - 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; + expect( + await run( + ['text-trim', 'lg:text-trim'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } - @media (min-width: 1024px) { - .lg\\:text-trim { - text-box-trim: both; - text-box-edge: cap alphabetic; - } + @theme reference { + --breakpoint-lg: 1024px; } - }" - `) - }) - - test('return multiple rule objects from a custom utility', async () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(id, base) { - return { - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.text-trim': [ - { 'text-box-trim': 'both' }, - { 'text-box-edge': 'cap alphabetic' }, - ], - }, - ]) - }, - } - }, - }, - ) - - 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 () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(id, base) { - return { - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities([ - { + `, + { + 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', - }, - }, - ]) - }, - } + }) + }, + } + }, }, - }, - ) + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { + .text-trim { + text-box-trim: both; + text-box-edge: cap alphabetic; + } - expect(optimizeCss(compiled.build(['text-trim', 'text-trim-2'])).trim()).toMatchInlineSnapshot(` - ".text-trim, .text-trim-2 { + @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' }, + ], + }, + ]) + }, + } + }, + }, + ), + ).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', + }, + }, + { + '.text-trim-2': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, + }, + ]) + }, + } + }, + }, + ), + ).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 () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(id, base) { - return { - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.outlined': { - outline: ['1px solid ButtonText', '1px auto -webkit-focus-ring-color'], + 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'], + }, }, - }, - ]) - }, - } + ]) + }, + } + }, }, - }, - ) - - expect(optimizeCss(compiled.build(['outlined'])).trim()).toMatchInlineSnapshot(` - ".outlined { + ), + ).toMatchInlineSnapshot(` + " + .outlined { outline: 1px solid buttontext; outline: 1px auto -webkit-focus-ring-color; - }" + } + " `) }) test('camel case properties are converted to kebab-case', async () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - async loadModule(id, base) { - return { - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim': { - WebkitAppearance: 'none', - textBoxTrim: 'both', - textBoxEdge: 'cap alphabetic', - }, - }) - }, + expect( + await run( + ['text-trim'], + 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', + }, + }) + }, + } + }, }, - }, - ) - - expect(optimizeCss(compiled.build(['text-trim'])).trim()).toMatchInlineSnapshot(` - "@layer utilities { + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { .text-trim { -webkit-appearance: none; text-box-trim: both; text-box-edge: cap alphabetic; } - }" + } + " `) }) test('custom static utilities support `@apply`', async () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - - @theme reference { - --breakpoint-lg: 1024px; - } - `, - { - async loadModule(id, base) { - return { - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.foo': { - '@apply flex dark:underline': {}, - }, - }) - }, + expect( + await run( + ['foo', 'lg:foo'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; } - }, - }, - ) - expect(optimizeCss(compiled.build(['foo', 'lg:foo'])).trim()).toMatchInlineSnapshot(` - "@layer utilities { + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(_id, base) { + return { + path: '', + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.foo': { + '@apply flex dark:underline': {}, + }, + }) + }, + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { .foo { display: flex; } @@ -2893,7 +3247,8 @@ describe('addUtilities()', () => { } } } - }" + } + " `) }) @@ -2911,8 +3266,9 @@ describe('addUtilities()', () => { } `, { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -2926,354 +3282,336 @@ describe('addUtilities()', () => { }, }, ) - }).rejects.toThrowError(/invalid utility selector/) + }).rejects.toThrow(/invalid utility selector/) }) test('supports multiple selector names', async () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @tailwind utilities; - - @theme reference { - --breakpoint-lg: 1024px; - } - `, - { - async loadModule(id, base) { - return { - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-textarea': { - appearance: 'none', - 'background-color': '#fff', - }, - }) - }, - } - }, - }, - ) - - 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 () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @tailwind utilities; - - @theme reference { - --breakpoint-lg: 1024px; - } - `, - { - async loadModule(id, base) { - return { - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-input::placeholder, .form-textarea:hover:focus': { - 'background-color': 'red', - }, - }) - }, - } - }, - }, - ) - - 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 () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - async loadModule(id, base) { - return { - 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', - }, - }) - }, - } - }, - }, - ) - expect( - compiled.build(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']).trim(), - ).toMatchInlineSnapshot( - ` - "@layer utilities { - .j { - &.j { - color: red; - } - .j& { - 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 () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - @theme prefix(tw) { - } - `, - { - async loadModule(id, base) { - return { - 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 () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @theme { - --breakpoint-md: 768px; - } - @tailwind utilities; - `, - { - async loadModule(id, base) { - return { - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.foo': { - ':where(.foo > :first-child)': { - color: 'red', - }, - }, - }) - }, - } - }, - }, - ) - - expect(compiled.build(['foo', 'md:foo', 'not-hover:md:foo']).trim()).toMatchInlineSnapshot(` - ".foo { - :where(.foo > :first-child) { - color: red; - } - } - .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; - } - } - } - }" - `) - }) -}) - -describe('matchUtilities()', () => { - test('custom functional utility', async () => { - async function run(candidates: string[]) { - let compiled = await compile( + await run( + ['form-input', 'lg:form-textarea'], css` @plugin "my-plugin"; - @tailwind utilities; @theme reference { --breakpoint-lg: 1024px; } `, - { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value) => ({ 'border-block-width': value }), + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.form-input, .form-textarea': { + appearance: 'none', + 'background-color': '#fff', }, - { - values: { - DEFAULT: '1px', - '2': '2px', - }, - }, - ) + }) }, } }, }, - ) + ), + ).toMatchInlineSnapshot(` + " + .form-input { + appearance: none; + background-color: #fff; + } - return compiled.build(candidates) - } + @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; + + @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', + }, + }) + }, + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .form-input, .form-input::placeholder { + background-color: red; + } + + @media (min-width: 1024px) { + .lg\\:form-textarea: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; + } + } + " + `) + }) + + 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; + } + `, + { + 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; + } + } + " + `) + }) + + 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; + } + } + " + `) + }) + + 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; + } + @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) { + color: red; + } + } + + @media not all and (hover: hover) { + @media (min-width: 768px) { + .not-hover\\:md\\:foo :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, + }, + }) + }, + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .foo { + a: red; + z-index: 0; + } + " + `) + }) +}) + +describe('matchUtilities()', () => { + test('custom functional utility', async () => { + let input = css` + @plugin "my-plugin"; + + @tailwind utilities; + + @theme reference { + --breakpoint-lg: 1024px; + } + ` expect( - optimizeCss( - await run([ + await run( + [ 'border-block', 'border-block-2', 'border-block-[35px]', 'border-block-[var(--foo)]', 'lg:border-block-2', - ]), - ).trim(), + ], + input, + { + async loadModule(_id, base) { + return { + path: '', + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { 'border-block': (value) => ({ 'border-block-width': value }) }, + { values: { DEFAULT: '1px', '2': '2px' } }, + ) + }, + } + }, + }, + ), ).toMatchInlineSnapshot(` - ".border-block { + " + .border-block { border-block-width: 1px; } @@ -3293,250 +3631,257 @@ describe('matchUtilities()', () => { .lg\\:border-block-2 { border-block-width: 2px; } - }" + } + " `) expect( - optimizeCss( - await run([ + await run( + [ '-border-block', '-border-block-2', 'lg:-border-block-2', 'border-block-unknown', 'border-block/1', - ]), - ).trim(), + ], + input, + { + async loadModule(_id, base) { + return { + path: '', + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { 'border-block': (value) => ({ 'border-block-width': value }) }, + { values: { DEFAULT: '1px', '2': '2px' } }, + ) + }, + } + }, + }, + ), ).toEqual('') }) test('custom functional utilities can start with @', async () => { - async function run(candidates: string[]) { - let compiled = await compile( + expect( + await run( + ['@w-1', 'hover:@w-1'], css` @plugin "my-plugin"; @tailwind utilities; `, - { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities({ '@w': (value) => ({ width: value }) }, { values: { 1: '1px' } }) + }, + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .\\@w-1 { + width: 1px; + } + + @media (hover: hover) { + .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( - { '@w': (value) => ({ width: value }) }, { - values: { - 1: '1px', - }, + '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' } }, + ) + }, + } + }, + }, + ), + ).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'], + css` + @plugin "my-plugin"; + + @tailwind utilities; + + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + 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: 'any' }, + ) + }, + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .border-block { + --my-modifier: none; + border-block-width: 1px; + } + + .border-block-2 { + --my-modifier: none; + border-block-width: 2px; + } + + .border-block-2\\/foo { + --my-modifier: foo; + border-block-width: 2px; + } + + .border-block\\/foo { + --my-modifier: foo; + border-block-width: 1px; + } + " + `) + }) + + test('custom functional utility with known modifier', async () => { + let input = css` + @plugin "my-plugin"; + + @tailwind utilities; + + @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) { + 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' }, }, ) }, } }, }, - ) + ), + ).toMatchInlineSnapshot(` + " + .border-block { + --my-modifier: none; + border-block-width: 1px; + } - return compiled.build(candidates) - } + .border-block-2 { + --my-modifier: none; + border-block-width: 2px; + } - expect(optimizeCss(await run(['@w-1', 'hover:@w-1'])).trim()).toMatchInlineSnapshot(` - ".\\@w-1 { - width: 1px; - } + .border-block-2\\/foo { + --my-modifier: foo; + border-block-width: 2px; + } - @media (hover: hover) { - .hover\\:\\@w-1:hover { - width: 1px; - } - }" - `) - }) + .border-block\\/foo { + --my-modifier: foo; + border-block-width: 1px; + } + " + `) - test('custom functional utilities can return an array of rules', async () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(id, base) { + expect( + await run(['border-block/unknown', 'border-block-2/unknown'], input, { + 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[], + 'border-block': (value, { modifier }) => ({ + '--my-modifier': modifier ?? 'none', + 'border-block-width': value, + }), }, { - values: { - DEFAULT: '1px', - }, + values: { DEFAULT: '1px', '2': '2px' }, + modifiers: { foo: 'foo' }, }, ) }, } }, - }, - ) - - 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 () => { - async function run(candidates: string[]) { - let compiled = await compile( - css` - @plugin "my-plugin"; - - @tailwind utilities; - - @theme reference { - --breakpoint-lg: 1024px; - } - `, - - { - async loadModule(id, base) { - return { - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value, { modifier }) => ({ - '--my-modifier': modifier ?? 'none', - 'border-block-width': value, - }), - }, - { - 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 { - --my-modifier: none; - border-block-width: 1px; - } - - .border-block-2 { - --my-modifier: none; - border-block-width: 2px; - } - - .border-block-2\\/foo { - --my-modifier: foo; - border-block-width: 2px; - } - - .border-block\\/foo { - --my-modifier: foo; - border-block-width: 1px; - }" - `) - }) - - test('custom functional utility with known modifier', async () => { - async function run(candidates: string[]) { - let compiled = await compile( - css` - @plugin "my-plugin"; - - @tailwind utilities; - - @theme reference { - --breakpoint-lg: 1024px; - } - `, - - { - async loadModule(id, base) { - return { - 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', - }, - }, - ) - }, - } - }, - }, - ) - - return compiled.build(candidates) - } - - expect( - optimizeCss( - await run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']), - ).trim(), - ).toMatchInlineSnapshot(` - ".border-block { - --my-modifier: none; - border-block-width: 1px; - } - - .border-block-2 { - --my-modifier: none; - border-block-width: 2px; - } - - .border-block-2\\/foo { - --my-modifier: foo; - border-block-width: 2px; - } - - .border-block\\/foo { - --my-modifier: foo; - border-block-width: 1px; - }" - `) - - expect( - optimizeCss(await run(['border-block/unknown', 'border-block-2/unknown'])).trim(), + }), ).toEqual('') }) @@ -3544,190 +3889,127 @@ 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 () => { - async function run(candidates: string[]) { - let compiled = await compile( - css` - @tailwind utilities; - @plugin "my-plugin"; - `, + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` - { - async loadModule(id, base) { - return { - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { type: ['color', 'any'] }, - ) - - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { type: ['length'] }, - ) - }, - } + 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'] }, + ) }, - }, - ) - - return compiled.build(candidates) + } + }, } expect( - optimizeCss( - await run(['scrollbar-[2px]', 'scrollbar-[#08c]', 'scrollbar-[#08c]/50']), - ).trim(), + await run(['scrollbar-[2px]', 'scrollbar-[#08c]', 'scrollbar-[#08c]/50'], input, options), ).toMatchInlineSnapshot(` - ".scrollbar-\\[\\#08c\\] { + " + .scrollbar-\\[2px\\] { + scrollbar-width: 2px; + } + + .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('') + expect(await run(['scrollbar-[2px]/50'], input, options)).toEqual('') }) test('no modifiers are supported by the plugins', async () => { - async function run(candidates: string[]) { - let compiled = await compile( + expect( + await run( + ['scrollbar-[2px]/50'], 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 }), - }, + { scrollbar: (value) => ({ '--scrollbar-angle': value }) }, { type: ['angle', 'any'] }, ) matchUtilities( - { - scrollbar: (value) => ({ '--scrollbar-width': value }), - }, + { scrollbar: (value) => ({ '--scrollbar-width': value }) }, { type: ['length'] }, ) }, } }, }, - ) - - return compiled.build(candidates) - } - - expect(optimizeCss(await run(['scrollbar-[2px]/50'])).trim()).toEqual('') + ), + ).toEqual('') }) test('invalid named modifier', async () => { - async function run(candidates: string[]) { - let compiled = await compile( + expect( + await run( + ['scrollbar-[2px]/foo'], 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-color': value }) }, { type: ['color', 'any'], modifiers: { foo: 'foo' } }, ) matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, + { scrollbar: (value) => ({ 'scrollbar-width': value }) }, { type: ['length'], modifiers: { bar: 'bar' } }, ) }, } }, }, - ) - - return compiled.build(candidates) - } - - expect(optimizeCss(await run(['scrollbar-[2px]/foo'])).trim()).toEqual('') + ), + ).toEqual('') }) }) test('custom functional utilities with different types', async () => { - async function run(candidates: string[]) { - let compiled = await compile( - css` - @plugin "my-plugin"; + let input = css` + @plugin "my-plugin"; - @tailwind utilities; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; - } - `, - - { - async loadModule(id, base) { - return { - 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) - } + @theme reference { + --breakpoint-lg: 1024px; + } + ` expect( - optimizeCss( - await run([ + await run( + [ 'scrollbar-black', 'scrollbar-black/50', 'scrollbar-2', @@ -3739,13 +4021,38 @@ describe('matchUtilities()', () => { 'scrollbar-[color:var(--my-color)]', 'scrollbar-[color:var(--my-color)]/50', 'scrollbar-[length:var(--my-width)]', - ]), - ).trim(), + ], + 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' } }, + ) + }, + } + }, + }, + ), ).toMatchInlineSnapshot(` - ".scrollbar-2 { + " + .scrollbar-2, .scrollbar-\\[2px\\] { scrollbar-width: 2px; } + .scrollbar-\\[length\\:var\\(--my-width\\)\\] { + scrollbar-width: var(--my-width); + } + .scrollbar-\\[\\#fff\\] { scrollbar-color: #fff; } @@ -3754,10 +4061,6 @@ 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); } @@ -3768,10 +4071,6 @@ 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); } @@ -3788,23 +4087,48 @@ describe('matchUtilities()', () => { .scrollbar-black\\/50 { scrollbar-color: oklab(0% none none / .5); - }" + } + " `) expect( - optimizeCss( - await run([ - 'scrollbar-2/50', - 'scrollbar-[2px]/50', - 'scrollbar-[length:var(--my-width)]/50', - ]), - ).trim(), + 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' } }, + ) + }, + } + }, + }, + ), ).toEqual('') }) test('functional utilities with `type: color` automatically support opacity', async () => { - async function run(candidates: string[]) { - let compiled = await compile( + expect( + await run( + [ + 'scrollbar-current', + 'scrollbar-current/45', + 'scrollbar-black', + 'scrollbar-black/33', + 'scrollbar-black/[50%]', + 'scrollbar-[var(--my-color)]/[25%]', + ], css` @plugin "my-plugin"; @@ -3814,45 +4138,24 @@ 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); } @@ -3882,13 +4185,15 @@ describe('matchUtilities()', () => { .scrollbar-current\\/45 { scrollbar-color: color-mix(in oklab, currentcolor 45%, transparent); } - }" + } + " `) }) test('functional utilities with explicit modifiers', async () => { - async function run(candidates: string[]) { - let compiled = await compile( + expect( + await run( + ['scrollbar-[12px]', 'scrollbar-[12px]/foo', 'scrollbar-[12px]/bar'], css` @plugin "my-plugin"; @@ -3899,10 +4204,10 @@ describe('matchUtilities()', () => { --opacity-my-opacity: 0.5; } `, - { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3912,29 +4217,16 @@ 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; } @@ -3942,50 +4234,43 @@ describe('matchUtilities()', () => { .scrollbar-\\[12px\\]\\/foo { --modifier: foo; scrollbar-width: 12px; - }" + } + " `) }) test('functional utilities support `@apply`', async () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - - @theme reference { - --breakpoint-lg: 1024px; - } - `, - { - async loadModule(id, base) { - return { - 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(), + await run( + ['foo-bar', 'lg:foo-bar', 'foo-[12px]', 'lg:foo-[12px]'], + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + + @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' } }, + ) + }, + } + }, + }, + ), ).toMatchInlineSnapshot(` - "@layer utilities { + " + @layer utilities { .foo-\\[12px\\] { --foo: 12px; display: flex; @@ -4007,7 +4292,8 @@ describe('matchUtilities()', () => { display: flex; } } - }" + } + " `) }) @@ -4026,8 +4312,9 @@ describe('matchUtilities()', () => { `, { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities({ @@ -4041,119 +4328,103 @@ describe('matchUtilities()', () => { }, }, ) - }).rejects.toThrowError(/invalid utility name/) + }).rejects.toThrow(/invalid utility name/) }) test('replaces the class name with variants in nested selectors', async () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @theme { - --breakpoint-md: 768px; - } - @tailwind utilities; - `, - { - async loadModule(base) { - return { - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - foo: (value) => ({ - ':where(.foo > :first-child)': { - color: value, - }, - }), - }, - { - values: { - red: 'red', - }, - }, - ) - }, + 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; + } - expect(compiled.build(['foo-red', 'md:foo-red', 'not-hover:md:foo-red']).trim()) - .toMatchInlineSnapshot(` - ".foo-red { - :where(.foo-red > :first-child) { + @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; + } + } + + @media not all and (hover: hover) { + @media (min-width: 768px) { + .not-hover\\:md\\:foo-red :where(.not-hover\\:md\\: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 () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(id, base) { - return { - base, - module: ({ addComponents }: PluginAPI) => { - addComponents({ - '.btn': { - padding: '.5rem 1rem', - borderRadius: '.25rem', - fontWeight: '600', - }, - '.btn-blue': { - backgroundColor: '#3490dc', - color: '#fff', - '&:hover': { - backgroundColor: '#2779bd', + 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', }, - }, - '.btn-red': { - backgroundColor: '#e3342f', - color: '#fff', - '&:hover': { - backgroundColor: '#cc1f1a', + '.btn-blue': { + backgroundColor: '#3490dc', + color: '#fff', + '&:hover': { + backgroundColor: '#2779bd', + }, }, - }, - }) - }, - } + '.btn-red': { + backgroundColor: '#e3342f', + color: '#fff', + '&:hover': { + backgroundColor: '#cc1f1a', + }, + }, + }) + }, + } + }, }, - }, - ) - - expect(optimizeCss(compiled.build(['btn', 'btn-blue', 'btn-red'])).trim()) - .toMatchInlineSnapshot(` - ".btn { + ), + ).toMatchInlineSnapshot(` + " + .btn { border-radius: .25rem; padding: .5rem 1rem; font-weight: 600; @@ -4175,44 +4446,39 @@ describe('addComponents()', () => { .btn-red:hover { background-color: #cc1f1a; - }" + } + " `) }) }) describe('matchComponents()', () => { test('is an alias for matchUtilities', async () => { - let compiled = await compile( - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(id, base) { - return { - base, - module: ({ matchComponents }: PluginAPI) => { - matchComponents( - { - prose: (value) => ({ '--container-size': value }), - }, - { - values: { - DEFAULT: 'normal', - sm: 'sm', - lg: 'lg', - }, - }, - ) - }, - } + 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' } }, + ) + }, + } + }, }, - }, - ) - - expect(optimizeCss(compiled.build(['prose', 'sm:prose-sm', 'hover:prose-lg'])).trim()) - .toMatchInlineSnapshot(` - ".prose { + ), + ).toMatchInlineSnapshot(` + " + .prose { --container-size: normal; } @@ -4220,21 +4486,23 @@ describe('matchComponents()', () => { .hover\\:prose-lg:hover { --container-size: lg; } - }" + } + " `) }) }) describe('prefix()', () => { test('is an identity function', async () => { - let fn = vi.fn() + using fn = vi.fn() await compile( css` @plugin "my-plugin"; `, { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, module: ({ prefix }: PluginAPI) => { fn(prefix('btn')) @@ -4250,14 +4518,15 @@ describe('prefix()', () => { describe('config()', () => { test('can return the resolved config when passed no arguments', async () => { - let fn = vi.fn() + using fn = vi.fn() await compile( css` @plugin "my-plugin"; `, { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, module: ({ config }: PluginAPI) => { fn(config()) @@ -4277,14 +4546,15 @@ describe('config()', () => { }) test('can return part of the config', async () => { - let fn = vi.fn() + using fn = vi.fn() await compile( css` @plugin "my-plugin"; `, { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, module: ({ config }: PluginAPI) => { fn(config('theme')) @@ -4302,14 +4572,15 @@ describe('config()', () => { }) test('falls back to default value if requested path does not exist', async () => { - let fn = vi.fn() + using fn = vi.fn() await compile( css` @plugin "my-plugin"; `, { - async loadModule(id, base) { + async loadModule(_id, base) { return { + path: '', base, module: ({ config }: PluginAPI) => { fn(config('somekey', 'defaultvalue')) @@ -4321,4 +4592,40 @@ 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 dc27993cb..11878fb30 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -1,10 +1,12 @@ import type { Features } from '..' import { substituteAtApply } from '../apply' -import { atRule, decl, rule, walk, type AstNode } from '../ast' +import { atRule, cloneAstNode, context, decl, rule, type AstNode } from '../ast' import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candidate' import { substituteFunctions } from '../css-functions' import * as CSS from '../css-parser' import type { DesignSystem } from '../design-system' +import * as SelectorParser from '../selector-parser' +import type { SourceLocation } from '../source-maps/source' import { withAlpha } from '../utilities' import { DefaultMap } from '../utils/default-map' import { escape } from '../utils/escape' @@ -12,9 +14,9 @@ import { inferDataType } from '../utils/infer-data-type' import { segment } from '../utils/segment' import { toKeyPath } from '../utils/to-key-path' import { compoundsForSelectors, IS_VALID_VARIANT_NAME, substituteAtSlot } from '../variants' +import { walk, WalkAction } from '../walk' import type { ResolvedConfig, UserConfig } from './config/types' import { createThemeFn } from './plugin-functions' -import * as SelectorParser from './selector-parser' export type Config = UserConfig export type PluginFn = (api: PluginAPI) => void @@ -24,6 +26,7 @@ export type PluginWithConfig = { /** @internal */ reference?: boolean + src?: SourceLocation | undefined } export type PluginWithOptions = { (options?: T): PluginWithConfig @@ -93,28 +96,50 @@ export function buildPluginApi({ resolvedConfig, featuresRef, referenceMode, + src, }: { designSystem: DesignSystem ast: AstNode[] resolvedConfig: ResolvedConfig featuresRef: { current: Features } referenceMode: boolean + src: SourceLocation | undefined }): PluginAPI { let api: PluginAPI = { addBase(css) { if (referenceMode) return let baseNodes = objectToAst(css) featuresRef.current |= substituteFunctions(baseNodes, designSystem) - ast.push(atRule('@layer', 'base', baseNodes)) + let rule = atRule('@layer', 'base', baseNodes) + walk([rule], (node) => { + node.src = src + }) + ast.push(rule) }, addVariant(name, variant) { if (!IS_VALID_VARIANT_NAME.test(name)) { throw new Error( - `\`addVariant('${name}')\` defines an invalid variant name. Variants should only contain alphanumeric, dashes or underscore characters.`, + `\`addVariant('${name}')\` defines an invalid variant name. Variants should only contain alphanumeric, dashes, or underscore characters and start with a lowercase letter or number.`, ) } + // Ignore variants emitting v3 `:merge(…)` rules. In v4, the `group-*` and `peer-*` variants + // compound automatically. + if (typeof variant === 'string') { + if (variant.includes(':merge(')) return + } else if (Array.isArray(variant)) { + if (variant.some((v) => v.includes(':merge('))) return + } else if (typeof variant === 'object') { + function keyIncludes(object: Record, search: string): boolean { + return Object.entries(object).some( + ([key, value]) => + key.includes(search) || (typeof value === 'object' && keyIncludes(value, search)), + ) + } + if (keyIncludes(variant, ':merge(')) return + } + // Single selector or multiple parallel selectors if (typeof variant === 'string' || Array.isArray(variant)) { designSystem.variants.static( @@ -130,7 +155,7 @@ export function buildPluginApi({ // CSS-in-JS object else if (typeof variant === 'object') { - designSystem.variants.fromAst(name, objectToAst(variant)) + designSystem.variants.fromAst(name, objectToAst(variant), designSystem) } }, matchVariant(name, fn, options) { @@ -143,6 +168,17 @@ export function buildPluginApi({ return parseVariantValue(resolved, nodes) } + try { + // Sample variant value and ignore variants emitting v3 `:merge` rules. In + // v4, the `group-*` and `peer-*` variants compound automatically. + let sample = fn('a', { modifier: null }) + if (typeof sample === 'string' && sample.includes(':merge(')) { + return + } else if (Array.isArray(sample) && sample.some((r) => r.includes(':merge('))) { + return + } + } catch {} + let defaultOptionKeys = Object.keys(options?.values ?? {}) designSystem.variants.group( () => { @@ -166,12 +202,17 @@ 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 + return null } ruleNodes.nodes = resolveVariantValue(defaultValue, variant.modifier, ruleNodes.nodes) + } else { + return null } }) }, @@ -185,8 +226,14 @@ export function buildPluginApi({ let aValueKey = a.value ? a.value.value : 'DEFAULT' let zValueKey = z.value ? z.value.value : 'DEFAULT' - let aValue = options?.values?.[aValueKey] ?? aValueKey - let zValue = options?.values?.[zValueKey] ?? zValueKey + 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 if (options && typeof options.sort === 'function') { return options.sort( @@ -210,6 +257,10 @@ export function buildPluginApi({ return aValue < zValue ? -1 : 1 }, ) + + designSystem.variants.suggest(name, () => + Object.keys(options?.values ?? {}).filter((v) => v !== 'DEFAULT'), + ) }, addUtilities(utilities) { @@ -228,7 +279,11 @@ export function buildPluginApi({ for (let [name, css] of entries) { if (name.startsWith('@keyframes ')) { if (!referenceMode) { - ast.push(rule(name, objectToAst(css))) + let keyframes = rule(name, objectToAst(css)) + walk([keyframes], (node) => { + node.src = src + }) + ast.push(keyframes) } continue } @@ -236,7 +291,7 @@ export function buildPluginApi({ let selectorAst = SelectorParser.parse(name) let foundValidUtility = false - SelectorParser.walk(selectorAst, (node) => { + walk(selectorAst, (node) => { if ( node.kind === 'selector' && node.value[0] === '.' && @@ -255,8 +310,15 @@ export function buildPluginApi({ return } - if (node.kind === 'function' && node.value === ':not') { - return SelectorParser.SelectorWalkAction.Skip + 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') + ) { + return WalkAction.Skip } }) @@ -273,7 +335,7 @@ export function buildPluginApi({ walk(ast, (node) => { if (node.kind === 'rule') { let selectorAst = SelectorParser.parse(node.selector) - SelectorParser.walk(selectorAst, (node) => { + walk(selectorAst, (node) => { if (node.kind === 'selector' && node.value[0] === '.') { node.value = `.${designSystem.theme.prefix}\\:${node.value.slice(1)}` } @@ -284,7 +346,7 @@ export function buildPluginApi({ } designSystem.utilities.static(className, (candidate) => { - let clonedAst = structuredClone(ast) + let clonedAst = ast.map(cloneAstNode) replaceNestedClassNameReferences(clonedAst, className, candidate.raw) featuresRef.current |= substituteAtApply(clonedAst, designSystem) return clonedAst @@ -360,10 +422,13 @@ export function buildPluginApi({ value = values.DEFAULT ?? null } else if (candidate.value.kind === 'arbitrary') { value = candidate.value.value - } else if (candidate.value.fraction && values[candidate.value.fraction]) { + } else if ( + candidate.value.fraction && + Object.hasOwn(values, candidate.value.fraction) + ) { value = values[candidate.value.fraction] ignoreModifier = true - } else if (values[candidate.value.value]) { + } else if (Object.hasOwn(values, candidate.value.value)) { value = values[candidate.value.value] } else if (values.__BARE_VALUE__) { value = values.__BARE_VALUE__(candidate.value) ?? null @@ -384,7 +449,7 @@ export function buildPluginApi({ modifier = null } else if (modifiers === 'any' || candidate.modifier.kind === 'arbitrary') { modifier = candidate.modifier.value - } else if (modifiers?.[candidate.modifier.value]) { + } else if (modifiers && Object.hasOwn(modifiers, candidate.modifier.value)) { modifier = modifiers[candidate.modifier.value] } else if (isColor && !Number.isNaN(Number(candidate.modifier.value))) { modifier = `${candidate.modifier.value}%` @@ -427,6 +492,10 @@ export function buildPluginApi({ // work even with legacy configs and plugins valueKeys.delete('__BARE_VALUE__') + // The `__CSS_VALUES__` key is a special key used by the theme function + // to transport `@theme` metadata about values from CSS + valueKeys.delete('__CSS_VALUES__') + // The `DEFAULT` key is represented as `null` in the utility API if (valueKeys.has('DEFAULT')) { valueKeys.delete('DEFAULT') @@ -501,6 +570,13 @@ export function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] { let entries = rules.flatMap((rule) => Object.entries(rule)) for (let [name, value] of entries) { + if (value === null || value === undefined) continue + + // @ts-expect-error + // We do not want `false` present in the types but still need to discard these nodes for + // compatibility purposes + if (value === false) continue + if (typeof value !== 'object') { if (!name.startsWith('--')) { if (value === '@slot') { @@ -522,7 +598,7 @@ export function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] { ast.push(rule(name, objectToAst(item))) } } - } else if (value !== null) { + } else { ast.push(rule(name, objectToAst(value))) } } @@ -532,14 +608,28 @@ 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) => { + return resolvedArray.flatMap((r): AstNode | AstNode[] => { 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 { - return rule(r, nodes) + 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 } }) } @@ -556,7 +646,7 @@ function replaceNestedClassNameReferences( walk(ast, (node) => { if (node.kind === 'rule') { let selectorAst = SelectorParser.parse(node.selector) - SelectorParser.walk(selectorAst, (node) => { + walk(selectorAst, (node) => { if (node.kind === 'selector' && node.value === `.${utilityName}`) { node.value = `.${escape(rawCandidate)}` } diff --git a/packages/tailwindcss/src/compat/plugin-functions.ts b/packages/tailwindcss/src/compat/plugin-functions.ts index 339bd11c6..bbd5fac46 100644 --- a/packages/tailwindcss/src/compat/plugin-functions.ts +++ b/packages/tailwindcss/src/compat/plugin-functions.ts @@ -100,6 +100,28 @@ 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 })() @@ -208,7 +230,12 @@ 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. - if ('DEFAULT' in obj && Object.keys(obj).length === 1) { + // + // 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) { return [obj.DEFAULT as any, optionsObj.DEFAULT ?? ThemeOptions.NONE] as const } @@ -223,8 +250,10 @@ 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 - if (obj?.[key] === undefined) { + // 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)) { if (path[i + 1] === undefined) { return undefined } diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index ac73d8216..d81cd8de6 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -1,85 +1,89 @@ import { describe, expect, test } from 'vitest' -import { compile } from '..' +import { run } from '../test-utils/run' const css = String.raw test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { - 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', + 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', + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - 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; } - .sm\\:flex { - @media (width >= 44rem) { + + @media (min-width: 44rem) { + .sm\\:flex { display: flex; } - } - .min-sm\\:max-md\\:underline { - @media (width >= 44rem) { - @media (width < 50rem) { + + @media not all and (min-width: 50rem) { + .min-sm\\:max-md\\:underline { text-decoration-line: underline; } } } - .md\\:flex { - @media (width >= 50rem) { + + @media (min-width: 50rem) { + .md\\:flex { display: flex; } - } - .min-md\\:max-lg\\:underline { - @media (width >= 50rem) { - @media (width < 64rem) { + + @media not all and (min-width: 64rem) { + .min-md\\:max-lg\\:underline { text-decoration-line: underline; } } } - .lg\\:flex { - @media (width >= 64rem) { + + @media (min-width: 64rem) { + .lg\\:flex { display: flex; } } - .print\\:items-end { - @media print { + + @media print { + .print\\:items-end { align-items: flex-end; } } @@ -88,100 +92,98 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { }) test('JS config `screens` extend CSS `--breakpoint-*`', async () => { - let input = css` - @theme default { - --breakpoint-xs: 39rem; - --breakpoint-md: 49rem; - } - @theme { - --breakpoint-md: 50rem; - } - @config "./config.js"; - @tailwind utilities; - ` + 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 compiler = await compile(input, { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - xs: '30rem', - sm: '40rem', - md: '48rem', - lg: '60rem', + // 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', + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - 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(` - ".min-xs\\:flex { - @media (width >= 30rem) { + " + @media (min-width: 30rem) { + .min-xs\\:flex, .xs\\:flex { display: flex; } - } - .xs\\:flex { - @media (width >= 30rem) { - display: flex; - } - } - .min-xs\\:max-md\\:underline { - @media (width >= 30rem) { - @media (width < 50rem) { + + @media not all and (min-width: 50rem) { + .min-xs\\:max-md\\:underline { text-decoration-line: underline; } } } - .sm\\:flex { - @media (width >= 40rem) { + + @media (min-width: 40rem) { + .sm\\:flex { display: flex; } - } - .min-sm\\:max-md\\:underline { - @media (width >= 40rem) { - @media (width < 50rem) { + + @media not all and (min-width: 50rem) { + .min-sm\\:max-md\\:underline { text-decoration-line: underline; } } } - .md\\:flex { - @media (width >= 50rem) { + + @media (min-width: 50rem) { + .md\\:flex { display: flex; } - } - .min-md\\:max-lg\\:underline { - @media (width >= 50rem) { - @media (width < 60rem) { + + @media not all and (min-width: 60rem) { + .min-md\\:max-lg\\:underline { text-decoration-line: underline; } } } - .lg\\:flex { - @media (width >= 60rem) { + + @media (min-width: 60rem) { + .lg\\:flex { display: flex; } } - .print\\:items-end { - @media print { + + @media print { + .print\\:items-end { align-items: flex-end; } } @@ -190,70 +192,73 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { }) test('JS config `screens` only setup, even if those match the default-theme export', async () => { - let input = css` - @config "./config.js"; - @tailwind utilities; - ` - - let compiler = await compile(input, { - loadModule: async () => ({ - module: { - theme: { - screens: { - sm: '40rem', - md: '48rem', - lg: '64rem', - }, - }, - }, - base: '/root', - }), - }) - 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', + await run( + [ + // 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', - ]), + // 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', + }, + }, + }, + base: '/root', + path: '', + }), + }, + ), ).toMatchInlineSnapshot(` - ".sm\\:flex { - @media (width >= 40rem) { + " + @media (min-width: 40rem) { + .sm\\:flex { display: flex; } - } - .min-sm\\:max-md\\:underline { - @media (width >= 40rem) { - @media (width < 48rem) { + + @media not all and (min-width: 48rem) { + .min-sm\\:max-md\\:underline { text-decoration-line: underline; } } } - .md\\:flex { - @media (width >= 48rem) { + + @media (min-width: 48rem) { + .md\\:flex { display: flex; } - } - .min-md\\:max-lg\\:underline { - @media (width >= 48rem) { - @media (width < 64rem) { + + @media not all and (min-width: 64rem) { + .min-md\\:max-lg\\:underline { text-decoration-line: underline; } } } - .lg\\:flex { - @media (width >= 64rem) { + + @media (min-width: 64rem) { + .lg\\:flex { display: flex; } } - .print\\:items-end { - @media print { + + @media print { + .print\\:items-end { align-items: flex-end; } } @@ -262,106 +267,85 @@ test('JS config `screens` only setup, even if those match the default-theme expo }) test('JS config `screens` overwrite 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; - ` + 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 compiler = await compile(input, { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', + // 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', + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - 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(` - ".mini\\:flex { - @media (width >= 40rem) { + " + @media (min-width: 40rem) { + .mini\\:flex, .sm\\:flex { display: flex; } - } - .sm\\:flex { - @media (width >= 40rem) { - display: flex; - } - } - .min-mini\\:max-midi\\:underline { - @media (width >= 40rem) { - @media (width < 48rem) { + + @media not all and (min-width: 48rem) { + .min-mini\\:max-midi\\:underline, .min-sm\\:max-md\\:underline { text-decoration-line: underline; } } } - .min-sm\\:max-md\\:underline { - @media (width >= 40rem) { - @media (width < 48rem) { + + @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 { text-decoration-line: underline; } } } - .md\\:flex { - @media (width >= 48rem) { + + @media (min-width: 64rem) { + .maxi\\:flex { display: flex; } } - .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 { + + @media print { + .print\\:items-end { align-items: flex-end; } } @@ -375,70 +359,91 @@ test('JS config with `theme: { extends }` should not include the `default-config @tailwind utilities; ` - let compiler = await compile(input, { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', + 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', + }, }, }, }, - }, - base: '/root', + base: '/root', + path: '', + }), }), - }) - - expect( - compiler.build(['sm:flex', 'md:flex', 'min-md:max-lg:underline', 'min-sm:max-md:underline']), ).toBe('') expect( - compiler.build([ - 'mini:flex', - 'midi:flex', - 'maxi:flex', - 'min-midi:max-maxi:underline', - 'min-mini:max-midi:underline', + await run( + [ + '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', - ]), + // 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: '', + }), + }, + ), ).toMatchInlineSnapshot(` - ".mini\\:flex { - @media (width >= 40rem) { + " + @media (min-width: 40rem) { + .mini\\:flex { display: flex; } - } - .min-mini\\:max-midi\\:underline { - @media (width >= 40rem) { - @media (width < 48rem) { + + @media not all and (min-width: 48rem) { + .min-mini\\:max-midi\\:underline { text-decoration-line: underline; } } } - .midi\\:flex { - @media (width >= 48rem) { + + @media (min-width: 48rem) { + .midi\\:flex { display: flex; } - } - .min-midi\\:max-maxi\\:underline { - @media (width >= 48rem) { - @media (width < 64rem) { + + @media not all and (min-width: 64rem) { + .min-midi\\:max-maxi\\:underline { text-decoration-line: underline; } } } - .maxi\\:flex { - @media (width >= 64rem) { + + @media (min-width: 64rem) { + .maxi\\:flex { display: flex; } } - .print\\:items-end { - @media print { + + @media print { + .print\\:items-end { align-items: flex-end; } } @@ -453,76 +458,104 @@ describe('complex screen configs', () => { @tailwind utilities; ` - 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)' }, + 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)' }, + }, }, }, }, - }, - base: '/root', + base: '/root', + path: '', + }), }), - }) - - expect(compiler.build(['min-sm:flex', 'min-md:flex', 'min-xl:flex', 'min-tall:flex'])).toBe('') + ).toBe('') expect( - compiler.build([ - 'sm:flex', - 'md:flex', - 'lg:flex', - 'min-lg:flex', - 'xl:flex', - 'tall:flex', + await run( + [ + 'sm:flex', + 'md:flex', + 'lg:flex', + 'min-lg:flex', + 'xl:flex', + 'tall:flex', - // Ensure other core variants appear at the end - 'print:items-end', - ]), + // 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: '', + }), + }, + ), ).toMatchInlineSnapshot(` - ".lg\\:flex { - @media (width >= 868px) { + " + @media (min-width: 868px) { + .lg\\:flex, .min-lg\\:flex { display: flex; } } - .min-lg\\:flex { - @media (width >= 868px) { + + @media (max-width: 639px) { + .sm\\:flex { display: flex; } } - .sm\\:flex { - @media (639px >= width) { + + @media (max-width: 767px) and (min-width: 668px), (min-width: 868px) { + .md\\:flex { display: flex; } } - .md\\:flex { - @media (767px >= width >= 668px), (width >= 868px) { + + @media (max-width: 1279px) and (min-width: 1024px) { + .xl\\:flex { display: flex; } } - .xl\\:flex { - @media (1279px >= width >= 1024px) { + + @media (min-height: 800px) { + .tall\\:flex { display: flex; } } - .tall\\:flex { - @media (min-height: 800px) { - display: flex; - } - } - .print\\:items-end { - @media print { + + @media print { + .print\\:items-end { align-items: flex-end; } } @@ -531,70 +564,65 @@ describe('complex screen configs', () => { }) test("don't interfere with `min-*` and `max-*` variants of non-complex screen configs", async () => { - 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)' }, + 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)' }, + }, + }, }, }, - }, + base: '/root', + path: '', + }), }, - base: '/root', - }), - }) - - 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(` - ".min-sm\\:flex { - @media (width >= 40rem) { + " + @media (min-width: 40rem) { + .min-sm\\:flex, .sm\\:flex { display: flex; } } - .sm\\:flex { - @media (width >= 40rem) { + + @media (min-width: 48rem) { + .md\\:flex, .min-md\\:flex { display: flex; } } - .md\\:flex { - @media (width >= 48rem) { + + @media screen and (orientation: portrait) { + .portrait\\:flex { display: flex; } } - .min-md\\:flex { - @media (width >= 48rem) { - display: flex; - } - } - .portrait\\:flex { - @media screen and (orientation: portrait) { - display: flex; - } - } - .print\\:items-end { - @media print { + + @media print { + .print\\:items-end { align-items: flex-end; } } @@ -604,37 +632,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', - }), - }) - // 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( - compiler.build(['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-2xl:flex']), - ).toMatchInlineSnapshot(`""`) + 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('') }) diff --git a/packages/tailwindcss/src/compat/selector-parser.test.ts b/packages/tailwindcss/src/compat/selector-parser.test.ts deleted file mode 100644 index f995c9de7..000000000 --- a/packages/tailwindcss/src/compat/selector-parser.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { parse, toCss, walk } from './selector-parser' - -describe('parse', () => { - it('should parse a simple selector', () => { - expect(parse('.foo')).toEqual([{ kind: 'selector', value: '.foo' }]) - }) - - it('should parse a compound selector', () => { - expect(parse('.foo.bar:hover#id')).toEqual([ - { 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: 'selector', value: '.foo' }, - { kind: 'separator', value: ',' }, - { kind: 'selector', value: '.bar' }, - ]) - }) - - it('should combine everything within attribute selectors', () => { - expect(parse('.foo[bar="baz"]')).toEqual([ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: '[bar="baz"]' }, - ]) - }) - - it('should parse functions', () => { - expect(parse('.foo:hover:not(.bar:focus)')).toEqual([ - { kind: 'selector', value: '.foo' }, - { kind: 'selector', value: ':hover' }, - { - kind: 'function', - nodes: [ - { - kind: 'selector', - value: '.bar', - }, - { - kind: 'selector', - value: ':focus', - }, - ], - value: ':not', - }, - ]) - }) - - it('should handle next-children combinator', () => { - expect(parse('.foo + p')).toEqual([ - { kind: 'selector', value: '.foo' }, - { kind: 'combinator', value: ' + ' }, - { kind: 'selector', value: 'p' }, - ]) - }) - - it('should handle escaped characters', () => { - expect(parse('foo\\.bar')).toEqual([{ kind: 'selector', value: 'foo\\.bar' }]) - }) - - it('parses :nth-child()', () => { - expect(parse(':nth-child(n+1)')).toEqual([ - { - kind: 'function', - value: ':nth-child', - nodes: [ - { - kind: 'value', - value: 'n+1', - }, - ], - }, - ]) - }) - - it('parses &:has(.child:nth-child(2))', () => { - expect(parse('&:has(.child:nth-child(2))')).toEqual([ - { - kind: 'selector', - value: '&', - }, - { - kind: 'function', - value: ':has', - nodes: [ - { - kind: 'selector', - value: '.child', - }, - { - kind: 'function', - value: ':nth-child', - nodes: [ - { - kind: 'value', - value: '2', - }, - ], - }, - ], - }, - ]) - }) - - it('parses &:has(:nth-child(2))', () => { - expect(parse('&:has(:nth-child(2))')).toEqual([ - { - kind: 'selector', - value: '&', - }, - { - kind: 'function', - value: ':has', - nodes: [ - { - kind: 'function', - value: ':nth-child', - nodes: [ - { - kind: 'value', - value: '2', - }, - ], - }, - ], - }, - ]) - }) -}) - -describe('toCss', () => { - it('should print a simple selector', () => { - expect(toCss(parse('.foo'))).toBe('.foo') - }) - - it('should print a compound selector', () => { - expect(toCss(parse('.foo.bar:hover#id'))).toBe('.foo.bar:hover#id') - }) - - it('should print a selector list', () => { - expect(toCss(parse('.foo,.bar'))).toBe('.foo,.bar') - }) - - it('should print an attribute selectors', () => { - expect(toCss(parse('.foo[bar="baz"]'))).toBe('.foo[bar="baz"]') - }) - - it('should print a function', () => { - expect(toCss(parse('.foo:hover:not(.bar:focus)'))).toBe('.foo:hover:not(.bar:focus)') - }) - - it('should print escaped characters', () => { - expect(toCss(parse('foo\\.bar'))).toBe('foo\\.bar') - }) - - it('should print :nth-child()', () => { - expect(toCss(parse(':nth-child(n+1)'))).toBe(':nth-child(n+1)') - }) -}) - -describe('walk', () => { - it('can be used to replace a function call', () => { - const ast = parse('.foo:hover:not(.bar:focus)') - walk(ast, (node, { replaceWith }) => { - if (node.kind === 'function' && node.value === ':not') { - replaceWith({ kind: 'selector', value: '.inverted-bar' }) - } - }) - expect(toCss(ast)).toBe('.foo:hover.inverted-bar') - }) -}) diff --git a/packages/tailwindcss/src/compat/selector-parser.ts b/packages/tailwindcss/src/compat/selector-parser.ts deleted file mode 100644 index 77a1ce4d4..000000000 --- a/packages/tailwindcss/src/compat/selector-parser.ts +++ /dev/null @@ -1,467 +0,0 @@ -export type SelectorCombinatorNode = { - kind: 'combinator' - value: string -} - -export type SelectorFunctionNode = { - kind: 'function' - value: string - 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 = - | SelectorCombinatorNode - | SelectorFunctionNode - | SelectorNode - | SelectorSeparatorNode - | SelectorValueNode -type SelectorParentNode = SelectorFunctionNode | null - -function combinator(value: string): SelectorCombinatorNode { - return { - kind: 'combinator', - value, - } -} - -function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode { - return { - kind: 'function', - value: value, - nodes, - } -} - -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', - value, - } -} - -export const enum SelectorWalkAction { - /** Continue walking, which is the default */ - Continue, - - /** Skip visiting the children of this node */ - Skip, - - /** Stop the walk entirely */ - Stop, -} - -export function walk( - ast: SelectorAstNode[], - visit: ( - node: SelectorAstNode, - utils: { - parent: SelectorParentNode - replaceWith(newNode: SelectorAstNode | SelectorAstNode[]): void - }, - ) => void | SelectorWalkAction, - parent: SelectorParentNode = null, -) { - for (let i = 0; i < ast.length; i++) { - let node = ast[i] - let replacedNode = false - let replacedNodeOffset = 0 - let status = - visit(node, { - parent, - replaceWith(newNode) { - if (replacedNode) return - replacedNode = true - - if (Array.isArray(newNode)) { - if (newNode.length === 0) { - ast.splice(i, 1) - replacedNodeOffset = 0 - } else if (newNode.length === 1) { - ast[i] = newNode[0] - replacedNodeOffset = 1 - } else { - ast.splice(i, 1, ...newNode) - replacedNodeOffset = newNode.length - } - } else { - ast[i] = newNode - replacedNodeOffset = 1 - } - }, - }) ?? SelectorWalkAction.Continue - - // We want to visit or skip the newly replaced node(s), which start at the - // current index (i). By decrementing the index here, the next loop will - // process this position (containing the replaced node) again. - if (replacedNode) { - if (status === SelectorWalkAction.Continue) { - i-- - } else { - i += replacedNodeOffset - 1 - } - continue - } - - // Stop the walk entirely - if (status === SelectorWalkAction.Stop) return SelectorWalkAction.Stop - - // Skip visiting the children of this node - if (status === SelectorWalkAction.Skip) continue - - if (node.kind === 'function') { - if (walk(node.nodes, visit, node) === SelectorWalkAction.Stop) { - return SelectorWalkAction.Stop - } - } - } -} - -export function toCss(ast: SelectorAstNode[]) { - let css = '' - for (const node of ast) { - switch (node.kind) { - case 'combinator': - case 'selector': - case 'separator': - case 'value': { - css += node.value - break - } - case 'function': { - css += node.value + '(' + toCss(node.nodes) + ')' - } - } - } - return css -} - -const BACKSLASH = 0x5c -const CLOSE_BRACKET = 0x5d -const CLOSE_PAREN = 0x29 -const COLON = 0x3a -const COMMA = 0x2c -const DOUBLE_QUOTE = 0x22 -const FULL_STOP = 0x2e -const GREATER_THAN = 0x3e -const NEWLINE = 0x0a -const NUMBER_SIGN = 0x23 -const OPEN_BRACKET = 0x5b -const OPEN_PAREN = 0x28 -const PLUS = 0x2b -const SINGLE_QUOTE = 0x27 -const SPACE = 0x20 -const TAB = 0x09 -const TILDE = 0x7e - -export function parse(input: string) { - input = input.replaceAll('\r\n', '\n') - - let ast: SelectorAstNode[] = [] - - let stack: (SelectorFunctionNode | null)[] = [] - - let parent = null as SelectorFunctionNode | null - - let buffer = '' - - let peekChar - - for (let i = 0; i < input.length; i++) { - let currentChar = input.charCodeAt(i) - - switch (currentChar) { - // E.g.: - // - // ```css - // .foo .bar - // ^ - // - // .foo > .bar - // ^^^ - // ``` - case COMMA: - case GREATER_THAN: - case NEWLINE: - case SPACE: - case PLUS: - case TAB: - case TILDE: { - // 1. Handle everything before the combinator as a selector - if (buffer.length > 0) { - let node = selector(buffer) - if (parent) { - parent.nodes.push(node) - } else { - ast.push(node) - } - buffer = '' - } - - // 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 && - peekChar !== PLUS && - peekChar !== TAB && - peekChar !== TILDE - ) { - break - } - } - i = end - 1 - - let contents = input.slice(start, end) - let node = contents.trim() === ',' ? separator(contents) : combinator(contents) - if (parent) { - parent.nodes.push(node) - } else { - ast.push(node) - } - - break - } - - // Start of a function call. - // - // E.g.: - // - // ```css - // .foo:not(.bar) - // ^ - // ``` - case OPEN_PAREN: { - let node = fun(buffer, []) - buffer = '' - - // If the function is not one of the following, we combine all it's - // contents into a single value node - if ( - node.value !== ':not' && - node.value !== ':where' && - node.value !== ':has' && - node.value !== ':is' - ) { - // Find the end of the function call - let start = i + 1 - let nesting = 0 - - // Find the closing bracket. - for (let j = i + 1; j < input.length; j++) { - peekChar = input.charCodeAt(j) - if (peekChar === OPEN_PAREN) { - nesting++ - continue - } - if (peekChar === CLOSE_PAREN) { - if (nesting === 0) { - i = j - break - } - nesting-- - } - } - let end = i - - node.nodes.push(value(input.slice(start, end))) - buffer = '' - i = end - - if (parent) { - parent.nodes.push(node) - } else { - ast.push(node) - } - - break - } - - if (parent) { - parent.nodes.push(node) - } else { - ast.push(node) - } - stack.push(node) - parent = node - - break - } - - // End of a function call. - // - // E.g.: - // - // ```css - // foo(bar, baz) - // ^ - // ``` - case CLOSE_PAREN: { - let tail = stack.pop() - - // Handle everything before the closing paren a selector - if (buffer.length > 0) { - let node = selector(buffer) - tail!.nodes.push(node) - buffer = '' - } - - if (stack.length > 0) { - parent = stack[stack.length - 1] - } else { - parent = null - } - - break - } - - // Split compound selectors. - // - // E.g.: - // - // ```css - // .foo.bar - // ^ - // ``` - case FULL_STOP: - case COLON: - case NUMBER_SIGN: { - // Handle everything before the combinator as a selector and - // start a new selector - if (buffer.length > 0) { - let node = selector(buffer) - if (parent) { - parent.nodes.push(node) - } else { - ast.push(node) - } - } - buffer = String.fromCharCode(currentChar) - break - } - - // Start of an attribute selector. - case OPEN_BRACKET: { - // Handle everything before the combinator as a selector - if (buffer.length > 0) { - 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. - for (let j = i + 1; j < input.length; j++) { - peekChar = input.charCodeAt(j) - if (peekChar === OPEN_BRACKET) { - nesting++ - continue - } - if (peekChar === CLOSE_BRACKET) { - if (nesting === 0) { - i = j - break - } - nesting-- - } - } - - // Adjust `buffer` to include the string. - buffer += input.slice(start, i + 1) - break - } - - // Start of a string. - case SINGLE_QUOTE: - case DOUBLE_QUOTE: { - let start = i - - // We need to ensure that the closing quote is the same as the opening - // quote. - // - // E.g.: - // - // ```css - // "This is a string with a 'quote' in it" - // ^ ^ -> 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. - if (peekChar === BACKSLASH) { - j += 1 - } - - // End of the string. - else if (peekChar === currentChar) { - i = j - break - } - } - - // Adjust `buffer` to include the string. - buffer += input.slice(start, i + 1) - break - } - - // Escaped characters. - case BACKSLASH: { - let nextChar = input.charCodeAt(i + 1) - buffer += String.fromCharCode(currentChar) + String.fromCharCode(nextChar) - i += 1 - break - } - - // Everything else will be collected in the buffer - default: { - buffer += String.fromCharCode(currentChar) - } - } - } - - // Collect the remainder as a word - if (buffer.length > 0) { - ast.push(selector(buffer)) - } - - return ast -} diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 6530541a0..46f562918 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -1,25 +1,20 @@ -import { - atRule, - decl, - rule, - walk, - WalkAction, - type AstNode, - type Rule, - type StyleRule, -} from './ast' +import { atRule, context, decl, rule, type AstNode, type Rule, type StyleRule } from './ast' import { type Candidate, type Variant } from './candidate' -import { type DesignSystem } from './design-system' +import { CompileAstFlags, type DesignSystem } from './design-system' import GLOBAL_PROPERTY_ORDER from './property-order' import { asColor, type Utility } from './utilities' import { compare } from './utils/compare' import { escape } from './utils/escape' import type { Variants } from './variants' +import { walk, WalkAction } from './walk' export function compileCandidates( rawCandidates: Iterable, designSystem: DesignSystem, - { onInvalidCandidate }: { onInvalidCandidate?: (candidate: string) => void } = {}, + { + onInvalidCandidate, + respectImportant, + }: { onInvalidCandidate?: (candidate: string) => void; respectImportant?: boolean } = {}, ) { let nodeSorting = new Map< AstNode, @@ -44,6 +39,12 @@ export function compileCandidates( matches.set(rawCandidate, candidates) } + let flags = CompileAstFlags.None + + if (respectImportant ?? true) { + flags |= CompileAstFlags.RespectImportant + } + let variantOrderMap = designSystem.getVariantOrder() // Create the AST @@ -51,7 +52,7 @@ export function compileCandidates( let found = false for (let candidate of candidates) { - let rules = designSystem.compileAstNodes(candidate) + let rules = designSystem.compileAstNodes(candidate, flags) if (rules.length === 0) continue found = true @@ -119,10 +120,16 @@ export function compileCandidates( } } -export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem) { +export function compileAstNodes( + candidate: Candidate, + designSystem: DesignSystem, + flags: CompileAstFlags, +) { let asts = compileBaseUtility(candidate, designSystem) if (asts.length === 0) return [] + let respectImportant = designSystem.important && Boolean(flags & CompileAstFlags.RespectImportant) + let rules: { node: AstNode propertySort: { @@ -136,7 +143,10 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem for (let nodes of asts) { let propertySort = getPropertySort(nodes) - if (candidate.important || designSystem.important) { + // If the candidate itself is important then we want to always mark + // the utility as important. However, at a design system level we want + // to be able to opt-out when using things like `@apply` + if (candidate.important || respectImportant) { applyImportant(nodes) } @@ -177,7 +187,18 @@ export function applyVariant( // E.g. `[>img]:flex` is not valid, but `has-[>img]:flex` is if (variant.relative && depth === 0) return null - node.nodes = [rule(variant.selector, node.nodes)] + 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] return } @@ -277,7 +298,19 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) return asts + 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 + } asts.push(compiledNodes) } @@ -289,7 +322,19 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) return asts + 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 + } asts.push(compiledNodes) } diff --git a/packages/tailwindcss/src/constant-fold-declaration.test.ts b/packages/tailwindcss/src/constant-fold-declaration.test.ts new file mode 100644 index 000000000..5a0c867b6 --- /dev/null +++ b/packages/tailwindcss/src/constant-fold-declaration.test.ts @@ -0,0 +1,137 @@ +import { expect, it } from 'vitest' +import { constantFoldDeclaration } from './constant-fold-declaration' + +it.each([ + // Simple expression + ['calc(1 + 1)', '2'], + ['calc(3 - 2)', '1'], + ['calc(2 * 3)', '6'], + ['calc(8 / 2)', '4'], + + // Nested + ['calc(1 + calc(1 + 1))', '3'], + ['calc(3 - calc(1 + 2))', '0'], + ['calc(2 * calc(1 + 3))', '8'], + ['calc(8 / calc(2 + 2))', '2'], + ['calc(1 + (1 + 1))', '3'], + ['calc(3 - (1 + 2))', '0'], + ['calc(2 * (1 + 3))', '8'], + ['calc(8 / (2 + 2))', '2'], + + // With units + ['calc(1rem * 2)', '2rem'], + ['calc(2rem - 0.5rem)', '1.5rem'], + ['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) => { + expect(constantFoldDeclaration(input)).toBe(expected) +}) + +it.each([ + ['calc(1rem * 2%)'], + ['calc(1rem * 2px)'], + ['calc(2rem - 6)'], + ['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) +}) + +it.each([ + ['calc(0 * 100vw)'], + ['calc(0 * calc(1 * 2))'], + ['calc(0 * var(--foo))'], + ['calc(0 * calc(var(--spacing) * 32))'], + + ['calc(100vw * 0)'], + ['calc(calc(1 * 2) * 0)'], + ['calc(var(--foo) * 0)'], + ['calc(calc(var(--spacing, 0.25rem) * 32) * 0)'], + ['calc(var(--spacing, 0.25rem) * -0)'], + + // Zeroes + ['0px'], + ['0rem'], + ['0em'], + ['0dvh'], + ['-0'], + ['+0'], + ['-0.0rem'], + ['+0.00rem'], + + // Expressions + ['calc(-0px * -1)'], + ['calc(-1 * -0px)'], +])('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%'], + ['0turn', '0deg'], + ['0fr', '0fr'], + ['0ms', '0s'], + ['0s', '0s'], + ['-0.0deg', '0deg'], + ['-0.0rad', '0deg'], + ['-0.0%', '0%'], + ['-0.0turn', '0deg'], + ['-0.0fr', '0fr'], + ['-0.0ms', '0s'], + ['-0.0s', '0s'], +])('should not fold non-foldable units to `0`. Constant fold `%s` into `%s`', (input, expected) => { + expect(constantFoldDeclaration(input)).toBe(expected) +}) + +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 new file mode 100644 index 000000000..c15420150 --- /dev/null +++ b/packages/tailwindcss/src/constant-fold-declaration.ts @@ -0,0 +1,357 @@ +import { dimensions } from './utils/dimensions' +import { isLength } from './utils/infer-data-type' +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 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[]] { + let folded = false + + walk(ast, { + exit(valueNode, ctx) { + // Canonicalize dimensions to their simplest form. This includes: + // - Convert `-0`, `+0`, `0.0`, … 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) + 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)) + } + + // Constant fold `calc()` expressions with two operands and one operator + else if ( + valueNode.kind === 'function' && + (valueNode.value === 'calc' || valueNode.value === '') + ) { + // [ + // { kind: 'word', value: '0.25rem' }, 0 + // { kind: 'separator', value: ' ' }, 1 + // { kind: 'word', value: '*' }, 2 + // { kind: 'separator', value: ' ' }, 3 + // { kind: 'word', value: '256' } 4 + // ] + if (valueNode.nodes.length !== 5) return + if (valueNode.nodes[2].kind !== 'word') return + + let lhsNode = valueNode.nodes[0] + 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 + + // Nullify entire expression when multiplying by `0`, e.g.: `calc(0 * 100vw)` -> `0` + // + // TODO: Ensure it's safe to do so based on the data types? + if ( + operator === '*' && + ((lhs?.[0] === 0 && lhs?.[1] === null) || // 0 * something + (rhs?.[0] === 0 && rhs?.[1] === null)) // something * 0 + ) { + folded = true + 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 + } + + switch (operator) { + case '*': { + if ( + lhs[1] === rhs[1] || // Same Units, e.g.: `1rem * 2rem`, `8 * 6` + (lhs[1] === null && rhs[1] !== null) || // Unitless * Unit, e.g.: `2 * 1rem` + (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] ?? ''}`), + ) + } + break + } + + case '+': { + if ( + lhs[1] === rhs[1] // Same unit or unitless, e.g.: `1rem + 2rem`, `8 + 6` + ) { + folded = true + return WalkAction.ReplaceSkip(ValueParser.word(`${lhs[0] + rhs[0]}${lhs[1] ?? ''}`)) + } + break + } + + case '-': { + if ( + lhs[1] === rhs[1] // Same unit or unitless, e.g.: `2rem - 1rem`, `8 - 6` + ) { + folded = true + return WalkAction.ReplaceSkip(ValueParser.word(`${lhs[0] - rhs[0]}${lhs[1] ?? ''}`)) + } + break + } + + case '/': { + if ( + rhs[0] !== 0 && // Don't divide by zero + ((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] ?? ''}`)) + } + break + } + } + } + }, + }) + + return [folded, ast] +} + +function canonicalizeDimension( + input: string, + rem: number | null = null, + normalizeUnit = true, +): string | null { + let dimension = dimensions.get(input) + if (dimension === null) return null // This shouldn't happen + + let [value, unit] = dimension + 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}` + + // prettier-ignore + switch (unit.toLowerCase()) { + // to px, https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#lengths + case 'in': return `${value * 96}px` // 1in = 96.000px + case 'cm': return `${value * 96 / 2.54}px` // 1cm = 37.795px + case 'mm': return `${value * 96 / 2.54 / 10}px` // 1mm = 3.779px + case 'q': return `${value * 96 / 2.54 / 10 / 4}px` // 1q = 0.945px + case 'pc': return `${value * 96 / 6}px` // 1pc = 16.000px + case 'pt': return `${value * 96 / 72}px` // 1pt = 1.333px + case 'rem': return rem !== null ? `${value * rem}px` : null // 1rem = 16.000px (Assuming root font-size is 16px) + + // to deg, https://developer.mozilla.org/en-US/docs/Web/CSS/angle + case 'grad': return `${value * 0.9}deg` // 1grad = 0.900deg + case 'rad': return `${value * 180 / Math.PI}deg` // 1rad = 57.296deg + case 'turn': return `${value * 360}deg` // 1turn = 360.000deg + + //
`, ) - expect(await getPropertyValue('#x', 'outline')).toEqual('rgb(255, 255, 255) dotted 2px') + expect(await getPropertyValue('#x', 'outline')).toEqual('2px dotted rgb(255, 255, 255)') await page.locator('#x').hover() - expect(await getPropertyValue('#x', 'outline')).toEqual('rgb(255, 255, 255) dotted 4px') + expect(await getPropertyValue('#x', 'outline')).toEqual('4px dotted rgb(255, 255, 255)') }) test('borders can be added without a border-style utility', async ({ page }) => { @@ -2177,6 +2177,330 @@ 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') @@ -2216,7 +2540,7 @@ async function render(page: Page, content: string, extraCss: string = '') { let scanner = new Scanner({}) let candidates = scanner.scanFiles([{ content, extension: 'html' }]) - let styles = optimize(build(candidates)) + let { code: styles } = optimize(build(candidates)) content = `${content}` await page.setContent(content) @@ -2249,7 +2573,7 @@ async function getPropertyValue( selector: [string, string | undefined], property: string, ) { - return page.evaluate( + let value = await page.evaluate( ([[selector, pseudo], property]) => { return window .getComputedStyle(document.querySelector(selector)!, pseudo) @@ -2257,4 +2581,16 @@ 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 52d447f7f..05364f5c2 100644 --- a/packages/tailwindcss/theme.css +++ b/packages/tailwindcss/theme.css @@ -1,7 +1,7 @@ @theme default { --font-sans: - ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; + -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-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 0); + --color-zinc-50: oklch(98.5% 0 none); --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 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-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-stone-50: oklch(98.5% 0.001 106.423); --color-stone-100: oklch(97% 0.001 106.424); @@ -271,6 +271,54 @@ --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 6ae022f65..d2131cbeb 100644 --- a/packages/tailwindcss/tsconfig.json +++ b/packages/tailwindcss/tsconfig.json @@ -1,3 +1,6 @@ { "extends": "../tsconfig.base.json", + "compilerOptions": { + "verbatimModuleSyntax": false, + }, } diff --git a/packages/tailwindcss/vitest.config.ts b/packages/tailwindcss/vitest.config.mts similarity index 100% rename from packages/tailwindcss/vitest.config.ts rename to packages/tailwindcss/vitest.config.mts diff --git a/patches/@napi-rs__cli@3.7.4.patch b/patches/@napi-rs__cli@3.7.4.patch new file mode 100644 index 000000000..e72212499 --- /dev/null +++ b/patches/@napi-rs__cli@3.7.4.patch @@ -0,0 +1,80 @@ +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.5.1.patch b/patches/@parcel__watcher@2.6.0.patch similarity index 52% rename from patches/@parcel__watcher@2.5.1.patch rename to patches/@parcel__watcher@2.6.0.patch index ed9fa9040..37ff83f04 100644 --- a/patches/@parcel__watcher@2.5.1.patch +++ b/patches/@parcel__watcher@2.6.0.patch @@ -1,20 +1,22 @@ diff --git a/index.js b/index.js -index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2a9a311e3 100644 +index 78ee744d74900c39a839cafa46b295258fc86307..f28fd092644ccfb8aff2618b966cb5219a181a02 100644 --- a/index.js +++ b/index.js -@@ -1,41 +1,34 @@ --const {createWrapper} = require('./wrapper'); -+const { createWrapper } = require('./wrapper') +@@ -1,43 +1,32 @@ + const {createWrapper} = require('./wrapper'); -let name = `@parcel/watcher-${process.platform}-${process.arch}`; -if (process.platform === 'linux') { -- const { MUSL, family } = require('detect-libc'); +- const {MUSL, familySync} = require('detect-libc'); +- const family = familySync(); - 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") { @@ -22,7 +24,8 @@ index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2 + } else if (process.env.PLATFORM_LIBC === "glibc") { + return require(`@parcel/watcher-${process.platform}-${process.arch}-glibc`) + } else { -+ let { MUSL, GLIBC, family, familySync } = require('detect-libc') ++ let { MUSL, GLIBC, familySync } = require('detect-libc') ++ let family = familySync() -let binding; -try { @@ -37,14 +40,12 @@ index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2 - 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') { @@ -63,12 +64,7 @@ index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2 } -const wrapper = createWrapper(binding); --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 ++const wrapper = createWrapper(loadPackage()); + exports.writeSnapshot = wrapper.writeSnapshot; + exports.getEventsSince = wrapper.getEventsSince; + exports.subscribe = wrapper.subscribe; diff --git a/patches/lightningcss@1.29.2.patch b/patches/lightningcss@1.33.0.patch similarity index 58% rename from patches/lightningcss@1.29.2.patch rename to patches/lightningcss@1.33.0.patch index 204f06ab3..e70be2315 100644 --- a/patches/lightningcss@1.29.2.patch +++ b/patches/lightningcss@1.33.0.patch @@ -1,8 +1,8 @@ diff --git a/node/index.js b/node/index.js -index 011d04b450687083e7a0ec9023c41ee79104831c..ef26db49d98c60b5b31f3601f6ad045f0b0e8f4f 100644 +index 6fe25aef457aa5448f9ee6204da1e253f5534800..742be0229b94450b631b6e069ffc372d2b35df47 100644 --- a/node/index.js +++ b/node/index.js -@@ -1,28 +1,41 @@ +@@ -1,25 +1,40 @@ -let parts = [process.platform, process.arch]; -if (process.platform === 'linux') { - const { MUSL, familySync } = require('detect-libc'); @@ -11,12 +11,8 @@ index 011d04b450687083e7a0ec9023c41ee79104831c..ef26db49d98c60b5b31f3601f6ad045f - parts.push('musl'); - } else if (process.arch === 'arm') { - parts.push('gnueabihf'); -- } else { -- parts.push('gnu'); -- } --} else if (process.platform === 'win32') { -- parts.push('msvc'); --} ++// 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') { @@ -28,19 +24,8 @@ index 011d04b450687083e7a0ec9023c41ee79104831c..ef26db49d98c60b5b31f3601f6ad045f + return require(`lightningcss-${process.platform}-${process.arch}-gnu`) + } + } else { -+ 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() ++ let { MUSL, GLIBC, familySync } = require('detect-libc') ++ let family = familySync() + + if (family === MUSL) { + return require(`lightningcss-${process.platform}-${process.arch}-musl`) @@ -56,15 +41,22 @@ index 011d04b450687083e7a0ec9023c41ee79104831c..ef26db49d98c60b5b31f3601f6ad045f + } + } else if (process.platform === 'win32') { + return require(`lightningcss-${process.platform}-${process.arch}-msvc`) -+ } else { + } else { +- parts.push('gnu'); + return require(`lightningcss-${process.platform}-${process.arch}`) } +-} else if (process.platform === 'win32') { +- parts.push('msvc'); } - --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 + +-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); diff --git a/playgrounds/nextjs/app/page.module.css b/playgrounds/nextjs/app/page.module.css index 4c5bdd253..b0117f44c 100644 --- a/playgrounds/nextjs/app/page.module.css +++ b/playgrounds/nextjs/app/page.module.css @@ -1,5 +1,5 @@ @reference "./globals.css"; .heading { - @apply skew-7; + @apply text-red-500; } diff --git a/playgrounds/nextjs/package.json b/playgrounds/nextjs/package.json index e691bc2a5..5487af5a5 100644 --- a/playgrounds/nextjs/package.json +++ b/playgrounds/nextjs/package.json @@ -5,23 +5,20 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", - "lint": "next lint" + "start": "next start" }, "dependencies": { "@tailwindcss/postcss": "workspace:^", "fast-glob": "^3.3.3", - "next": "15.2.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "next": "16.2.7", + "react": "19.2.7", + "react-dom": "19.2.7", "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", - "@types/react": "^19.0.12", - "@types/react-dom": "^19.0.4", - "eslint": "^9.22.0", - "eslint-config-next": "^15.2.4", - "typescript": "^5.5.4" + "@types/react": "19.2.15", + "@types/react-dom": "19.2.3", + "typescript": "^6.0.3" } } diff --git a/playgrounds/nextjs/tsconfig.json b/playgrounds/nextjs/tsconfig.json index 862143548..27414aae7 100644 --- a/playgrounds/nextjs/tsconfig.json +++ b/playgrounds/nextjs/tsconfig.json @@ -10,7 +10,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -20,7 +20,14 @@ "paths": { "@/*": ["./*"], }, + "target": "ES2017", }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + ], "exclude": ["node_modules"], } diff --git a/playgrounds/v3/package.json b/playgrounds/v3/package.json index f6d5c00b1..8c08310db 100644 --- a/playgrounds/v3/package.json +++ b/playgrounds/v3/package.json @@ -9,18 +9,16 @@ "upgrade": "node scripts/upgrade.mjs" }, "dependencies": { - "next": "15.2.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "next": "16.2.7", + "react": "19.2.7", + "react-dom": "19.2.7", "tailwindcss": "^3" }, "devDependencies": { - "@types/node": "^20.14.8", - "@types/react": "^19.0.12", - "@types/react-dom": "^19.0.4", - "autoprefixer": "^10.4.21", - "eslint": "^9.22.0", - "eslint-config-next": "^15.2.4", - "typescript": "^5.5.4" + "@types/node": "catalog:", + "@types/react": "19.2.15", + "@types/react-dom": "19.2.3", + "autoprefixer": "^10.5.0", + "typescript": "^6.0.3" } } diff --git a/playgrounds/v3/tsconfig.json b/playgrounds/v3/tsconfig.json index 862143548..27414aae7 100644 --- a/playgrounds/v3/tsconfig.json +++ b/playgrounds/v3/tsconfig.json @@ -10,7 +10,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -20,7 +20,14 @@ "paths": { "@/*": ["./*"], }, + "target": "ES2017", }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + ], "exclude": ["node_modules"], } diff --git a/playgrounds/vite/package.json b/playgrounds/vite/package.json index 90ea4af0f..25ab1a33b 100644 --- a/playgrounds/vite/package.json +++ b/playgrounds/vite/package.json @@ -10,15 +10,15 @@ }, "dependencies": { "@tailwindcss/vite": "workspace:^", - "@vitejs/plugin-react": "^4.3.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "@vitejs/plugin-react": "^6.0.2", + "react": "^19.2.6", + "react-dom": "^19.2.6", "tailwindcss": "workspace:^" }, "devDependencies": { - "@types/react": "^19.0.12", - "@types/react-dom": "^19.0.4", - "bun": "^1.2.8", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "bun": "^1.3.14", "vite": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b95015664..8382b2ba5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,87 +6,136 @@ 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: ^20.14.8 - version: 20.14.13 + 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 lightningcss: - specifier: 1.29.2 - version: 1.29.2 + specifier: 1.33.0 + version: 1.33.0 lightningcss-darwin-arm64: - specifier: 1.29.2 - version: 1.29.2 + specifier: 1.33.0 + version: 1.33.0 lightningcss-darwin-x64: - specifier: 1.29.2 - version: 1.29.2 + specifier: 1.33.0 + version: 1.33.0 lightningcss-linux-arm64-gnu: - specifier: 1.29.2 - version: 1.29.2 + specifier: 1.33.0 + version: 1.33.0 lightningcss-linux-arm64-musl: - specifier: 1.29.2 - version: 1.29.2 + specifier: 1.33.0 + version: 1.33.0 lightningcss-linux-x64-gnu: - specifier: 1.29.2 - version: 1.29.2 + specifier: 1.33.0 + version: 1.33.0 lightningcss-linux-x64-musl: - specifier: 1.29.2 - version: 1.29.2 + specifier: 1.33.0 + version: 1.33.0 lightningcss-win32-x64-msvc: - specifier: 1.29.2 - version: 1.29.2 + specifier: 1.33.0 + version: 1.33.0 + postcss: + specifier: ^8.5.25 + version: 8.5.25 prettier: - specifier: 3.5.0 - version: 3.5.0 + specifier: 3.9.6 + version: 3.9.6 vite: - specifier: ^6.0.0 - version: 6.0.0 + specifier: ^8.2.0 + version: 8.2.0 + webpack: + specifier: ^5.109.2 + version: 5.109.2 patchedDependencies: - '@parcel/watcher@2.5.1': - hash: zs2vvlrje3h42xp5ed2v44fep4 - path: patches/@parcel__watcher@2.5.1.patch - lightningcss@1.29.2: - hash: tzyxy3asfxcqc7ihrooumyi5fm - path: patches/lightningcss@1.29.2.patch + '@napi-rs/cli@3.7.4': 9912bf0a9c2cef8329d11c41fbce4d33ce2bdcf660a630b258c962c0a3c44bed + '@parcel/watcher@2.6.0': 705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da + lightningcss@1.33.0: 1d4a8800d60d13d42887b88b3a86576df4b451670308145fb432ec8abbf40930 importers: .: devDependencies: '@playwright/test': - specifier: ^1.51.1 - version: 1.51.1 + specifier: ^1.62.1 + version: 1.62.1 '@types/node': specifier: 'catalog:' - version: 20.14.13 + version: 22.20.1 postcss: - specifier: 8.5.3 - version: 8.5.3 + specifier: 'catalog:' + version: 8.5.25 postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.5.3) + specifier: ^16.1.1 + version: 16.1.1(postcss@8.5.25) prettier: specifier: 'catalog:' - version: 3.5.0 + version: 3.9.6 prettier-plugin-embed: - specifier: ^0.5.0 - version: 0.5.0 + specifier: ^0.5.1 + version: 0.5.1 prettier-plugin-organize-imports: - specifier: ^4.0.0 - version: 4.0.0(prettier@3.5.0)(typescript@5.5.4) + specifier: ^4.3.0 + version: 4.3.0(prettier@3.9.6)(typescript@5.9.3) tsup: - specifier: ^8.4.0 - version: 8.4.0(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.1)(typescript@5.5.4)(yaml@2.6.0) + specifier: ^8.5.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.25)(typescript@5.9.3)(yaml@2.9.0) turbo: - specifier: ^2.4.4 - version: 2.4.4 + specifier: ^2.10.8 + version: 2.10.8 typescript: - specifier: ^5.5.4 - version: 5.5.4 + specifier: ^5.9.3 + version: 5.9.3 vitest: - specifier: ^2.0.5 - version: 2.0.5(@types/node@20.14.13)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6) + 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)) 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:* @@ -115,16 +164,15 @@ importers: '@tailwindcss/oxide-linux-x64-musl': specifier: workspace:* version: link:npm/linux-x64-musl + '@tailwindcss/oxide-wasm32-wasi': + specifier: workspace:* + version: link:npm/wasm32-wasi '@tailwindcss/oxide-win32-arm64-msvc': specifier: workspace:* version: link:npm/win32-arm64-msvc '@tailwindcss/oxide-win32-x64-msvc': specifier: workspace:* version: link:npm/win32-x64-msvc - devDependencies: - '@napi-rs/cli': - specifier: ^2.18.4 - version: 2.18.4 crates/node/npm/android-arm-eabi: {} @@ -146,6 +194,27 @@ importers: crates/node/npm/linux-x64-musl: {} + crates/node/npm/wasm32-wasi: + dependencies: + '@emnapi/core': + specifier: ^1.11.3 + version: 1.11.3 + '@emnapi/runtime': + specifier: ^1.11.3 + version: 1.11.3 + '@emnapi/wasi-threads': + specifier: ^1.2.3 + version: 1.2.3 + '@napi-rs/wasm-runtime': + specifier: ^1.2.2 + version: 1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + '@tybys/wasm-util': + specifier: ^0.10.3 + version: 0.10.3 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + crates/node/npm/win32-arm64-msvc: {} crates/node/npm/win32-x64-msvc: {} @@ -153,20 +222,26 @@ importers: integrations: devDependencies: dedent: - specifier: 1.5.3 - version: 1.5.3 + specifier: 'catalog:' + version: 1.7.2 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.1 - version: 1.15.1 + specifier: ^1.15.11 + version: 1.15.11 listhen: - specifier: ^1.9.0 - version: 1.9.0 + specifier: ^1.10.1 + version: 1.10.1(@parcel/watcher@2.6.0(patch_hash=705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da)) tailwindcss: specifier: workspace:* version: link:../tailwindcss @@ -174,8 +249,8 @@ importers: packages/@tailwindcss-cli: dependencies: '@parcel/watcher': - specifier: ^2.5.1 - version: 2.5.1(patch_hash=zs2vvlrje3h42xp5ed2v44fep4) + specifier: 'catalog:' + version: 2.6.0(patch_hash=705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da) '@tailwindcss/node': specifier: workspace:* version: link:../@tailwindcss-node @@ -183,8 +258,8 @@ importers: specifier: workspace:* version: link:../../crates/node enhanced-resolve: - specifier: ^5.18.1 - version: 5.18.1 + specifier: 'catalog:' + version: 5.24.5 mri: specifier: ^1.2.0 version: 1.2.0 @@ -197,15 +272,24 @@ importers: packages/@tailwindcss-node: dependencies: + '@jridgewell/remapping': + specifier: ^2.3.5 + version: 2.3.5 enhanced-resolve: - specifier: ^5.18.1 - version: 5.18.1 + specifier: 'catalog:' + version: 5.24.5 jiti: - specifier: ^2.4.2 - version: 2.4.2 + specifier: ^2.7.0 + version: 2.7.0 lightningcss: specifier: 'catalog:' - version: 1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) + version: 1.33.0(patch_hash=1d4a8800d60d13d42887b88b3a86576df4b451670308145fb432ec8abbf40930) + magic-string: + specifier: ^1.1.0 + version: 1.1.0 + source-map-js: + specifier: ^1.2.1 + version: 1.2.1 tailwindcss: specifier: workspace:* version: link:../tailwindcss @@ -222,27 +306,27 @@ importers: specifier: workspace:* version: link:../../crates/node postcss: - specifier: ^8.4.41 - version: 8.4.41 + specifier: 'catalog:' + version: 8.5.25 tailwindcss: specifier: workspace:* version: link:../tailwindcss devDependencies: '@types/node': specifier: 'catalog:' - version: 20.14.13 + version: 22.20.1 '@types/postcss-import': specifier: 14.0.3 version: 14.0.3 dedent: - specifier: 1.5.3 - version: 1.5.3 + specifier: 'catalog:' + version: 1.7.2 internal-example-plugin: specifier: workspace:* version: link:../internal-example-plugin postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) + specifier: ^16.1.1 + version: 16.1.1(postcss@8.5.25) packages/@tailwindcss-standalone: dependencies: @@ -253,69 +337,91 @@ importers: specifier: workspace:* version: link:../@tailwindcss-cli '@tailwindcss/forms': - specifier: ^0.5.10 - version: 0.5.10(tailwindcss@packages+tailwindcss) + specifier: ^0.5.11 + version: 0.5.11(tailwindcss@packages+tailwindcss) '@tailwindcss/typography': - specifier: ^0.5.16 - version: 0.5.16(tailwindcss@packages+tailwindcss) + specifier: ^0.5.20 + version: 0.5.20(tailwindcss@packages+tailwindcss) detect-libc: specifier: 1.0.3 version: 1.0.3 enhanced-resolve: - specifier: ^5.18.1 - version: 5.18.1 + specifier: 'catalog:' + version: 5.24.5 tailwindcss: specifier: workspace:* version: link:../tailwindcss devDependencies: '@parcel/watcher-darwin-arm64': - specifier: ^2.5.1 - version: 2.5.1 + specifier: 'catalog:' + version: 2.6.0 '@parcel/watcher-darwin-x64': - specifier: ^2.5.1 - version: 2.5.1 + specifier: 'catalog:' + version: 2.6.0 '@parcel/watcher-linux-arm64-glibc': - specifier: ^2.5.1 - version: 2.5.1 + specifier: 'catalog:' + version: 2.6.0 '@parcel/watcher-linux-arm64-musl': - specifier: ^2.5.1 - version: 2.5.1 + specifier: 'catalog:' + version: 2.6.0 '@parcel/watcher-linux-x64-glibc': - specifier: ^2.5.1 - version: 2.5.1 + specifier: 'catalog:' + version: 2.6.0 '@parcel/watcher-linux-x64-musl': - specifier: ^2.5.1 - version: 2.5.1 + specifier: 'catalog:' + version: 2.6.0 '@parcel/watcher-win32-x64': - specifier: ^2.5.1 - version: 2.5.1 + specifier: 'catalog:' + version: 2.6.0 '@types/bun': - specifier: ^1.2.8 - version: 1.2.8 + specifier: ^1.3.14 + version: 1.3.14 bun: - specifier: ^1.2.8 - version: 1.2.8 + specifier: ^1.3.14 + version: 1.3.14 lightningcss-darwin-arm64: specifier: 'catalog:' - version: 1.29.2 + version: 1.33.0 lightningcss-darwin-x64: specifier: 'catalog:' - version: 1.29.2 + version: 1.33.0 lightningcss-linux-arm64-gnu: specifier: 'catalog:' - version: 1.29.2 + version: 1.33.0 lightningcss-linux-arm64-musl: specifier: 'catalog:' - version: 1.29.2 + version: 1.33.0 lightningcss-linux-x64-gnu: specifier: 'catalog:' - version: 1.29.2 + version: 1.33.0 lightningcss-linux-x64-musl: specifier: 'catalog:' - version: 1.29.2 + version: 1.33.0 lightningcss-win32-x64-msvc: specifier: 'catalog:' - version: 1.29.2 + 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) packages/@tailwindcss-upgrade: dependencies: @@ -325,21 +431,18 @@ importers: '@tailwindcss/oxide': specifier: workspace:* version: link:../../crates/node - braces: - specifier: ^3.0.3 - version: 3.0.3 dedent: - specifier: 1.5.3 - version: 1.5.3 + specifier: 'catalog:' + version: 1.7.2 enhanced-resolve: - specifier: ^5.18.1 - version: 5.18.1 + specifier: 'catalog:' + version: 5.24.5 globby: - specifier: ^14.1.0 - version: 14.1.0 + specifier: ^16.2.2 + version: 16.2.2 jiti: - specifier: ^2.0.0-beta.3 - version: 2.4.2 + specifier: ^2.7.0 + version: 2.7.0 mri: specifier: ^1.2.0 version: 1.2.0 @@ -347,36 +450,39 @@ importers: specifier: ^1.1.1 version: 1.1.1 postcss: - specifier: ^8.4.41 - version: 8.4.41 + specifier: 'catalog:' + version: 8.5.25 postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) + specifier: ^16.1.1 + version: 16.1.1(postcss@8.5.25) postcss-selector-parser: - specifier: ^7.1.0 - version: 7.1.0 + specifier: ^7.1.4 + version: 7.1.4 prettier: specifier: 'catalog:' - version: 3.5.0 + version: 3.9.6 + semver: + specifier: ^7.8.5 + version: 7.8.5 tailwindcss: specifier: workspace:* version: link:../tailwindcss tree-sitter: - specifier: ^0.22.4 - version: 0.22.4 + specifier: ^0.25.1 + version: 0.25.1 tree-sitter-typescript: specifier: ^0.23.2 - version: 0.23.2(tree-sitter@0.22.4) + version: 0.23.2(tree-sitter@0.25.1) devDependencies: - '@types/braces': - specifier: ^3.0.5 - version: 3.0.5 '@types/node': specifier: 'catalog:' - version: 20.14.13 + version: 22.20.1 '@types/postcss-import': specifier: ^14.0.3 version: 14.0.3 + '@types/semver': + specifier: ^7.8.0 + version: 7.8.0 packages/@tailwindcss-vite: dependencies: @@ -392,27 +498,61 @@ importers: devDependencies: '@types/node': specifier: 'catalog:' - version: 20.14.13 + version: 22.20.1 vite: specifier: 'catalog:' - version: 6.0.0(@types/node@20.14.13)(jiti@2.4.2)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0) + 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) packages/internal-example-plugin: {} packages/tailwindcss: devDependencies: + '@jridgewell/remapping': + specifier: ^2.3.5 + version: 2.3.5 '@tailwindcss/oxide': specifier: workspace:^ version: link:../../crates/node '@types/node': specifier: 'catalog:' - version: 20.14.13 + version: 22.20.1 dedent: - specifier: 1.5.3 - version: 1.5.3 + specifier: 'catalog:' + version: 1.7.2 lightningcss: specifier: 'catalog:' - version: 1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) + version: 1.33.0(patch_hash=1d4a8800d60d13d42887b88b3a86576df4b451670308145fb432ec8abbf40930) + magic-string: + specifier: ^1.1.0 + version: 1.1.0 + source-map-js: + specifier: ^1.2.1 + version: 1.2.1 playgrounds/nextjs: dependencies: @@ -423,73 +563,61 @@ importers: specifier: ^3.3.3 version: 3.3.3 next: - specifier: 15.2.4 - version: 15.2.4(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + 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) react: - specifier: ^19.0.0 - version: 19.0.0 + specifier: 19.2.7 + version: 19.2.7 react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) + specifier: 19.2.7 + version: 19.2.7(react@19.2.7) tailwindcss: specifier: workspace:^ version: link:../../packages/tailwindcss devDependencies: '@types/node': specifier: 'catalog:' - version: 20.14.13 + version: 22.20.1 '@types/react': - specifier: ^19.0.12 - version: 19.0.12 + specifier: 19.2.15 + version: 19.2.15 '@types/react-dom': - specifier: ^19.0.4 - version: 19.0.4(@types/react@19.0.12) - eslint: - specifier: ^9.22.0 - version: 9.22.0(jiti@2.4.2) - eslint-config-next: - specifier: ^15.2.4 - version: 15.2.4(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4) + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.15) typescript: - specifier: ^5.5.4 - version: 5.5.4 + specifier: ^6.0.3 + version: 6.0.3 playgrounds/v3: dependencies: next: - specifier: 15.2.4 - version: 15.2.4(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + 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) react: - specifier: ^19.0.0 - version: 19.0.0 + specifier: 19.2.7 + version: 19.2.7 react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) + specifier: 19.2.7 + version: 19.2.7(react@19.2.7) tailwindcss: specifier: ^3 - version: 3.4.14 + version: 3.4.19(yaml@2.9.0) devDependencies: '@types/node': - specifier: ^20.14.8 - version: 20.14.13 + specifier: 'catalog:' + version: 22.20.1 '@types/react': - specifier: ^19.0.12 - version: 19.0.12 + specifier: 19.2.15 + version: 19.2.15 '@types/react-dom': - specifier: ^19.0.4 - version: 19.0.4(@types/react@19.0.12) + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.15) autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.4.47) - eslint: - specifier: ^9.22.0 - version: 9.22.0(jiti@2.4.2) - eslint-config-next: - specifier: ^15.2.4 - version: 15.2.4(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.25) typescript: - specifier: ^5.5.4 - version: 5.6.3 + specifier: ^6.0.3 + version: 6.0.3 playgrounds/vite: dependencies: @@ -497,30 +625,30 @@ importers: specifier: workspace:^ version: link:../../packages/@tailwindcss-vite '@vitejs/plugin-react': - specifier: ^4.3.4 - version: 4.3.4(vite@6.0.0(@types/node@20.14.13)(jiti@2.4.2)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0)) + 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)) react: - specifier: ^19.0.0 - version: 19.0.0 + specifier: ^19.2.6 + version: 19.2.6 react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) + specifier: ^19.2.6 + version: 19.2.6(react@19.2.6) tailwindcss: specifier: workspace:^ version: link:../../packages/tailwindcss devDependencies: '@types/react': - specifier: ^19.0.12 - version: 19.0.12 + specifier: ^19.2.15 + version: 19.2.15 '@types/react-dom': - specifier: ^19.0.4 - version: 19.0.4(@types/react@19.0.12) + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.15) bun: - specifier: ^1.2.8 - version: 1.2.8 + specifier: ^1.3.14 + version: 1.3.14 vite: specifier: 'catalog:' - version: 6.0.0(@types/node@20.14.13)(jiti@2.4.2)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0) + 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) packages: @@ -528,911 +656,906 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} - '@babel/compat-data@7.26.3': - resolution: {integrity: sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==} - engines: {node: '>=6.9.0'} + '@emnapi/core@2.0.0-alpha.3': + resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} - '@babel/core@7.26.0': - resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} - engines: {node: '>=6.9.0'} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@babel/generator@7.26.3': - resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} - engines: {node: '>=6.9.0'} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@babel/helper-compilation-targets@7.25.9': - resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} - engines: {node: '>=6.9.0'} + '@emnapi/runtime@2.0.0-alpha.3': + resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} - '@babel/helper-module-imports@7.25.9': - resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} - engines: {node: '>=6.9.0'} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} - '@babel/helper-plugin-utils@7.25.9': - resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} - engines: {node: '>=6.9.0'} + '@emnapi/wasi-threads@2.0.1': + resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.25.9': - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.26.0': - resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.26.3': - resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-transform-react-jsx-self@7.25.9': - resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.25.9': - resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.25.9': - resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.26.4': - resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.26.3': - resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} - engines: {node: '>=6.9.0'} - - '@emnapi/runtime@1.3.1': - resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} - - '@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==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.24.0': - resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@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==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.24.0': - resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@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==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.24.0': - resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@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==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.24.0': - resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@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==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.24.0': - resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@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==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.24.0': - resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@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==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.24.0': - resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@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==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.24.0': - resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@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==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.24.0': - resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@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==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.24.0': - resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@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==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.24.0': - resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@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==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.24.0': - resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@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==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.24.0': - resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@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==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.24.0': - resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@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==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.24.0': - resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@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==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.24.0': - resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@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==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.24.0': - resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@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==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@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==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.24.0': - resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@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==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.24.0': - resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] - os: [openbsd] + os: [openharmony] - '@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/openbsd-x64@0.24.0': - resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.0': - resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@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==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.24.0': - resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@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==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.24.0': - resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@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==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.24.0': - resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@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==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.24.0': - resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.0': - resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - 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.19.2': - resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.1.0': - resolution: {integrity: sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.12.0': - resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.0': - resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.22.0': - resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==} - 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.2.7': - resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} - 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.33.5': - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.0.4': - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.0.4': - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-arm@1.0.5': - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.0.4': - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + 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==} cpu: [s390x] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-x64@1.0.4': - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] - '@img/sharp-linux-arm64@0.33.5': - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] - '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] - '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + 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==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] - '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] - '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] - '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + 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==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} + '@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'} + 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'} + 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'} + 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'} + 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'} + 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'} + 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/input@5.1.2': + resolution: {integrity: sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + 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'} + 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'} + 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'} + 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'} + 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'} + 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'} + 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'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@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@2.18.4': - resolution: {integrity: sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==} - engines: {node: '>= 10'} + '@napi-rs/cli@3.7.4': + resolution: {integrity: sha512-idELIUceJ9zPgx/shcMt1fSSsteFG68v56RIXi4qUm7OYuJOt7CoMj2KnHVmbOqXgUHWQU1lCULrtQdZDG4F5A==} + engines: {node: '>= 16'} hasBin: true + peerDependencies: + '@emnapi/runtime': ^1.7.1 + peerDependenciesMeta: + '@emnapi/runtime': + optional: true - '@next/env@15.2.4': - resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==} + '@napi-rs/cross-toolchain@1.0.3': + resolution: {integrity: sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg==} + peerDependencies: + '@napi-rs/cross-toolchain-arm64-target-aarch64': ^1.0.3 + '@napi-rs/cross-toolchain-arm64-target-armv7': ^1.0.3 + '@napi-rs/cross-toolchain-arm64-target-ppc64le': ^1.0.3 + '@napi-rs/cross-toolchain-arm64-target-s390x': ^1.0.3 + '@napi-rs/cross-toolchain-arm64-target-x86_64': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-aarch64': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-armv7': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-ppc64le': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-s390x': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-x86_64': ^1.0.3 + peerDependenciesMeta: + '@napi-rs/cross-toolchain-arm64-target-aarch64': + optional: true + '@napi-rs/cross-toolchain-arm64-target-armv7': + optional: true + '@napi-rs/cross-toolchain-arm64-target-ppc64le': + optional: true + '@napi-rs/cross-toolchain-arm64-target-s390x': + optional: true + '@napi-rs/cross-toolchain-arm64-target-x86_64': + optional: true + '@napi-rs/cross-toolchain-x64-target-aarch64': + optional: true + '@napi-rs/cross-toolchain-x64-target-armv7': + optional: true + '@napi-rs/cross-toolchain-x64-target-ppc64le': + optional: true + '@napi-rs/cross-toolchain-x64-target-s390x': + optional: true + '@napi-rs/cross-toolchain-x64-target-x86_64': + optional: true - '@next/eslint-plugin-next@15.2.4': - resolution: {integrity: sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==} + '@napi-rs/lzma-android-arm-eabi@1.4.5': + resolution: {integrity: sha512-Up4gpyw2SacmyKWWEib06GhiDdF+H+CCU0LAV8pnM4aJIDqKKd5LHSlBht83Jut6frkB0vwEPmAkv4NjQ5u//Q==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] - '@next/swc-darwin-arm64@15.2.4': - resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==} + '@napi-rs/lzma-android-arm64@1.4.5': + resolution: {integrity: sha512-uwa8sLlWEzkAM0MWyoZJg0JTD3BkPknvejAFG2acUA1raXM8jLrqujWCdOStisXhqQjZ2nDMp3FV6cs//zjfuQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/lzma-darwin-arm64@1.4.5': + resolution: {integrity: sha512-0Y0TQLQ2xAjVabrMDem1NhIssOZzF/y/dqetc6OT8mD3xMTDtF8u5BqZoX3MyPc9FzpsZw4ksol+w7DsxHrpMA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.2.4': - resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==} + '@napi-rs/lzma-darwin-x64@1.4.5': + resolution: {integrity: sha512-vR2IUyJY3En+V1wJkwmbGWcYiT8pHloTAWdW4pG24+51GIq+intst6Uf6D/r46citObGZrlX0QvMarOkQeHWpw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.2.4': - resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==} + '@napi-rs/lzma-freebsd-x64@1.4.5': + resolution: {integrity: sha512-XpnYQC5SVovO35tF0xGkbHYjsS6kqyNCjuaLQ2dbEblFRr5cAZVvsJ/9h7zj/5FluJPJRDojVNxGyRhTp4z2lw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/lzma-linux-arm-gnueabihf@1.4.5': + resolution: {integrity: sha512-ic1ZZMoRfRMwtSwxkyw4zIlbDZGC6davC9r+2oX6x9QiF247BRqqT94qGeL5ZP4Vtz0Hyy7TEViWhx5j6Bpzvw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/lzma-linux-arm64-gnu@1.4.5': + resolution: {integrity: sha512-asEp7FPd7C1Yi6DQb45a3KPHKOFBSfGuJWXcAd4/bL2Fjetb2n/KK2z14yfW8YC/Fv6x3rBM0VAZKmJuz4tysg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] - '@next/swc-linux-arm64-musl@15.2.4': - resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==} + '@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] - '@next/swc-linux-x64-gnu@15.2.4': - resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==} + '@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] - '@next/swc-linux-x64-musl@15.2.4': - resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==} + '@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] - '@next/swc-win32-arm64-msvc@15.2.4': - resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==} + '@napi-rs/lzma-wasm32-wasi@1.4.5': + resolution: {integrity: sha512-+VyHHlr68dvey6fXc2hehw9gHVFIW3TtGF1XkcbAu65qVXsA9D/T+uuoRVqhE+JCyFHFrO0ixRbZDRK1XJt1sA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@napi-rs/lzma-win32-arm64-msvc@1.4.5': + resolution: {integrity: sha512-eewnqvIyyhHi3KaZtBOJXohLvwwN27gfS2G/YDWdfHlbz1jrmfeHAmzMsP5qv8vGB+T80TMHNkro4kYjeh6Deg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.2.4': - resolution: {integrity: sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==} + '@napi-rs/lzma-win32-ia32-msvc@1.4.5': + resolution: {integrity: sha512-OeacFVRCJOKNU/a0ephUfYZ2Yt+NvaHze/4TgOwJ0J0P4P7X1mHzN+ig9Iyd74aQDXYqc7kaCXA2dpAOcH87Cg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/lzma-win32-x64-msvc@1.4.5': + resolution: {integrity: sha512-T4I1SamdSmtyZgDXGAGP+y5LEK5vxHUFwe8mz6D4R7Sa5/WCxTcCIgPJ9BD7RkpO17lzhlaM2vmVvMy96Lvk9Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/lzma@1.4.5': + resolution: {integrity: sha512-zS5LuN1OBPAyZpda2ZZgYOEDC+xecUdAGnrvbYzjnLXkrq/OBC3B9qcRvlxbDR3k5H/gVfvef1/jyUqPknqjbg==} + engines: {node: '>= 10'} + + '@napi-rs/tar-android-arm-eabi@1.1.0': + resolution: {integrity: sha512-h2Ryndraj/YiKgMV/r5by1cDusluYIRT0CaE0/PekQ4u+Wpy2iUVqvzVU98ZPnhXaNeYxEvVJHNGafpOfaD0TA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/tar-android-arm64@1.1.0': + resolution: {integrity: sha512-DJFyQHr1ZxNZorm/gzc1qBNLF/FcKzcH0V0Vwan5P+o0aE2keQIGEjJ09FudkF9v6uOuJjHCVDdK6S6uHtShAw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/tar-darwin-arm64@1.1.0': + resolution: {integrity: sha512-Zz2sXRzjIX4e532zD6xm2SjXEym6MkvfCvL2RMpG2+UwNVDVscHNcz3d47Pf3sysP2e2af7fBB3TIoK2f6trPw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/tar-darwin-x64@1.1.0': + resolution: {integrity: sha512-EI+CptIMNweT0ms9S3mkP/q+J6FNZ1Q6pvpJOEcWglRfyfQpLqjlC0O+dptruTPE8VamKYuqdjxfqD8hifZDOA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/tar-freebsd-x64@1.1.0': + resolution: {integrity: sha512-J0PIqX+pl6lBIAckL/c87gpodLbjZB1OtIK+RDscKC9NLdpVv6VGOxzUV/fYev/hctcE8EfkLbgFOfpmVQPg2g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/tar-linux-arm-gnueabihf@1.1.0': + resolution: {integrity: sha512-SLgIQo3f3EjkZ82ZwvrEgFvMdDAhsxCYjyoSuWfHCz0U16qx3SuGCp8+FYOPYCECHN3ZlGjXnoAIt9ERd0dEUg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/tar-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-d014cdle52EGaH6GpYTQOP9Py7glMO1zz/+ynJPjjzYFSxvdYx0byrjumZk2UQdIyGZiJO2MEFpCkEEKFSgPYA==} + 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==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@napi-rs/tar-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-vfpG71OB0ijtjemp3WTdmBKJm9R70KM8vsSExMsIQtV0lVzP07oM1CW6JbNRPXNLhRoue9ofYLiUDk8bE0Hckg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/tar-win32-ia32-msvc@1.1.0': + resolution: {integrity: sha512-hGPyPW60YSpOSgzfy68DLBHgi6HxkAM+L59ZZZPMQ0TOXjQg+p2EW87+TjZfJOkSpbYiEkULwa/f4a2hcVjsqQ==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/tar-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-L6Ed1DxXK9YSCMyvpR8MiNAyKNkQLjsHsHK9E0qnHa8NzLFqzDKhvs5LfnWxM2kJ+F7m/e5n9zPm24kHb3LsVw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/tar@1.1.0': + 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-tools-android-arm-eabi@1.0.1': + resolution: {integrity: sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/wasm-tools-android-arm64@1.0.1': + resolution: {integrity: sha512-WDR7S+aRLV6LtBJAg5fmjKkTZIdrEnnQxgdsb7Cf8pYiMWBHLU+LC49OUVppQ2YSPY0+GeYm9yuZWW3kLjJ7Bg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/wasm-tools-darwin-arm64@1.0.1': + resolution: {integrity: sha512-qWTI+EEkiN0oIn/N2gQo7+TVYil+AJ20jjuzD2vATS6uIjVz+Updeqmszi7zq7rdFTLp6Ea3/z4kDKIfZwmR9g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/wasm-tools-darwin-x64@1.0.1': + resolution: {integrity: sha512-bA6hubqtHROR5UI3tToAF/c6TDmaAgF0SWgo4rADHtQ4wdn0JeogvOk50gs2TYVhKPE2ZD2+qqt7oBKB+sxW3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/wasm-tools-freebsd-x64@1.0.1': + resolution: {integrity: sha512-90+KLBkD9hZEjPQW1MDfwSt5J1L46EUKacpCZWyRuL6iIEO5CgWU0V/JnEgFsDOGyyYtiTvHc5bUdUTWd4I9Vg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/wasm-tools-linux-arm64-gnu@1.0.1': + resolution: {integrity: sha512-rG0QlS65x9K/u3HrKafDf8cFKj5wV2JHGfl8abWgKew0GVPyp6vfsDweOwHbWAjcHtp2LHi6JHoW80/MTHm52Q==} + 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==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@napi-rs/wasm-tools-win32-arm64-msvc@1.0.1': + resolution: {integrity: sha512-PFi7oJIBu5w7Qzh3dwFea3sHRO3pojMsaEnUIy22QvsW+UJfNQwJCryVrpoUt8m4QyZXI+saEq/0r4GwdoHYFQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/wasm-tools-win32-ia32-msvc@1.0.1': + resolution: {integrity: sha512-gXkuYzxQsgkj05Zaq+KQTkHIN83dFAwMcTKa2aQcpYPRImFm2AQzEyLtpXmyCWzJ0F9ZYAOmbSyrNew8/us6bw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/wasm-tools-win32-x64-msvc@1.0.1': + resolution: {integrity: sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-tools@1.0.1': + resolution: {integrity: sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ==} + engines: {node: '>= 10'} + + '@next/env@16.2.7': + resolution: {integrity: sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==} + + '@next/swc-darwin-arm64@16.2.7': + resolution: {integrity: sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.2.7': + resolution: {integrity: sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.2.7': + resolution: {integrity: sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.2.7': + resolution: {integrity: sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==} + 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==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.2.7': + resolution: {integrity: sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.2.7': + resolution: {integrity: sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.2.7': + resolution: {integrity: sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1449,351 +1572,539 @@ 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'} - '@oven/bun-darwin-aarch64@1.2.8': - resolution: {integrity: sha512-O97RiReqBVzno5qLgoFX/dYI+xO+jSTIw0S+Cp1MRedXMZEqEgi8ql3ExWpteZyvUGBiXdvc0sQSOQwPoEwiPA==} + '@octokit/core@7.0.6': + resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==} + engines: {node: '>= 20'} + + '@octokit/endpoint@11.0.3': + resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==} + engines: {node: '>= 20'} + + '@octokit/graphql@9.0.3': + resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} + engines: {node: '>= 20'} + + '@octokit/openapi-types@27.0.0': + resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} + + '@octokit/plugin-paginate-rest@14.0.0': + resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@6.0.0': + resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@17.0.0': + resolution: {integrity: sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@7.1.0': + resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.9': + resolution: {integrity: sha512-o8Bi3f608eyM+7BmBiUWxFsdjLb3/ym1cQek5LZOv9KkZcxRrHCPhhRzm6xjO6HVZ85ItD6+sTsjxo821SVa/A==} + engines: {node: '>= 20'} + + '@octokit/rest@22.0.1': + resolution: {integrity: sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==} + engines: {node: '>= 20'} + + '@octokit/types@16.0.0': + resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + + '@oven/bun-darwin-aarch64@1.3.14': + resolution: {integrity: sha512-Omj20SuiHBOUjUBIyqtkNjSUIjOtEOJwmbix/ZyFH4BaQ6OZTaaRWIR4TjHVz0yadHgli6lLTiAh1uarnvD49A==} cpu: [arm64] os: [darwin] - '@oven/bun-darwin-x64-baseline@1.2.8': - resolution: {integrity: sha512-SqckVEKEALPJ1TLE+w1xlieZW5coRmmm2nxV5sxMs5BqUb6asrhJ+Wp51KePScvb2ctN7A00SGpROxVq2IA5GQ==} + '@oven/bun-darwin-x64-baseline@1.3.14': + resolution: {integrity: sha512-OSfsTZstc898HHElhU4NccaBGOSSDn5VfahiVTnidZ9B/+wb7WTyfZJaBeJcfjwJ9H2W9uTh2TGtl3UfcXgV9g==} cpu: [x64] os: [darwin] - '@oven/bun-darwin-x64@1.2.8': - resolution: {integrity: sha512-aofXwEl3ZzQgiOw/ITzAlXIpQiWeslvIVxol5/n/7Dv8Ekfrqhvo+j/uvEvY0KDECUn8gNIHwa839d701u/rhA==} + '@oven/bun-darwin-x64@1.3.14': + resolution: {integrity: sha512-FFj3QdU/OhlDyZOJ8CWfN5eWLpRlT4qjZg7lMQi7jA6GuoY5ajlO1zWLP/MuHYRSbXQUvV52RejNi8DVnAp13w==} cpu: [x64] os: [darwin] - '@oven/bun-linux-aarch64-musl@1.2.8': - resolution: {integrity: sha512-rl15SGTFCNj1PE/I2VpUc4XvOZXDCPlqgJGTAObH7+731ET6wgclAehp0x7s2qdyDN4awPnOtold+FYvMH/g9Q==} - cpu: [aarch64] - os: [linux] + '@oven/bun-freebsd-aarch64@1.3.14': + resolution: {integrity: sha512-LIKrXaFxAHybVO5Pf+9XP2FHUj/5APvXTUKk9dqHm5iFz4oH+W24cmhjkJirNujh9hKeTyrpWSe3no9JZKowIw==} + cpu: [arm64] + os: [freebsd] - '@oven/bun-linux-aarch64@1.2.8': - resolution: {integrity: sha512-BYzMj7H49xwmUBlHwfIXm/ky1D8yZyyVOnyRDNLTT+3+/odw7TTz3/+lZEBPGyet2tZ6oyBeis1xSZ9vYwSzZA==} + '@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==} cpu: [arm64] os: [linux] - '@oven/bun-linux-x64-baseline@1.2.8': - resolution: {integrity: sha512-j0SxtM9XYCE0N7vnUzuYfnP2iFSoTsQ7pF+K0DPUr/+zmBVJ5tcZUU3xRPc4St86zSrMUD3J2tIUTNksxo81sw==} + '@oven/bun-linux-aarch64@1.3.14': + resolution: {integrity: sha512-X5SsPZHs+iYO8R/efIcRtc7gT2Q2DgPfliCxEkx4cXBumwkw0c/EsHMNwH3EgGpCDaZ7IYVPhpCG/xBOQHEwZw==} + 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==} cpu: [x64] os: [linux] - '@oven/bun-linux-x64-musl-baseline@1.2.8': - resolution: {integrity: sha512-JbkQ452sYyK/TZGCvYz5eCmnlIazhPqkuEfZDwr9K2hfaY8JGJKwkC1sLxM3yvfoXzPgDrPFvYGBU75hmrJHTQ==} + '@oven/bun-linux-x64-musl-baseline@1.3.14': + resolution: {integrity: sha512-n6iE71G4lQE4XkrZhQQcL5YUlxDbnq6nqV7zeQi33PMsLT/0kYE+RvHOtBWZ3w0wMdXZfINmp63hIb9ijUBGtw==} cpu: [x64] os: [linux] - '@oven/bun-linux-x64-musl@1.2.8': - resolution: {integrity: sha512-NgjwsqRWRbCycTlLxg2mka3ZW8cnLPwkmg/zWRe9yjMutU7TGPV0Ak8cCf9PvA6Xgmrz+/HSO1ohO6Pii6Tb3w==} + '@oven/bun-linux-x64-musl@1.3.14': + resolution: {integrity: sha512-GBCB/k/sIqcr06eTNgg7g46qiUv35Jasx4XiccJ/n7RGqrE4RWUD/XJBbWFprVPjvqd59+QtSnS99XGqvftHfg==} cpu: [x64] os: [linux] - '@oven/bun-linux-x64@1.2.8': - resolution: {integrity: sha512-AzvrOAUGsKI9U7eYIbJbgtLVq9Jhx8dExt9QY6PtCgBklUlpTBU+9T+HPgXJqtIJYGtn9C9W4EBGjq9fEgy08A==} + '@oven/bun-linux-x64@1.3.14': + resolution: {integrity: sha512-7OVTAKvwfPmSbIV1HpdOoVVx5VRc427GuPPne93N6vk4eQBPId9nXmZDh9/zGaKPdbVjVtQSZafWQoUjx38Utw==} cpu: [x64] os: [linux] - '@oven/bun-windows-x64-baseline@1.2.8': - resolution: {integrity: sha512-fVZ1hNhA9Gph76jToSc5lglNO7Y349v4IUwZaBH25ypenyPPrBoxH11D8v/EB95CsUej4KyWieYcxUiTNZUVmg==} + '@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==} cpu: [x64] os: [win32] - '@oven/bun-windows-x64@1.2.8': - resolution: {integrity: sha512-9nQ45l6LCG+v0AUoBQrrSqsznO0Y36EyQ9sJmDYsI4faPmkHd+cQiyNnMFO5vIwjTXbB3Hb65ql7bjNYbP/O5g==} + '@oven/bun-windows-x64@1.3.14': + resolution: {integrity: sha512-mUFWL3BoYkNpjd8e9PqROiFF/1Xeotq20mABJsiQH62jM1g5zqWh4khw1RZ6bX8Q8fWvlPaxG1PjofkmjUi3vg==} cpu: [x64] os: [win32] - '@parcel/watcher-android-arm64@2.5.0': - resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} + '@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==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [android] - '@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==} + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [darwin] - '@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==} + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [darwin] - '@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==} + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [freebsd] - '@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==} + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] - '@parcel/watcher-linux-arm-glibc@2.5.1': - resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] - '@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==} + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - '@parcel/watcher-linux-arm64-glibc@2.5.1': - resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - '@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==} + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - '@parcel/watcher-linux-x64-glibc@2.5.1': - resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - '@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==} + '@parcel/watcher-wasm@2.5.6': + resolution: {integrity: sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA==} engines: {node: '>= 10.0.0'} bundledDependencies: - napi-wasm - '@parcel/watcher-win32-arm64@2.5.0': - resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [win32] - '@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==} + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [win32] - '@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==} + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} engines: {node: '>= 10.0.0'} - '@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.51.1': - resolution: {integrity: sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==} - engines: {node: '>=18'} + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} hasBin: true - '@rollup/rollup-android-arm-eabi@4.34.9': - resolution: {integrity: sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.34.9': - resolution: {integrity: sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==} + '@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] - '@rollup/rollup-darwin-arm64@4.34.9': - resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==} + '@rolldown/binding-darwin-arm64@1.2.1': + resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.34.9': - resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==} + '@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] - '@rollup/rollup-freebsd-arm64@4.34.9': - resolution: {integrity: sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.34.9': - resolution: {integrity: sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==} + '@rolldown/binding-freebsd-x64@1.2.1': + resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.34.9': - resolution: {integrity: sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==} + '@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] - '@rollup/rollup-linux-arm-musleabihf@4.34.9': - resolution: {integrity: sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.34.9': - resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==} + '@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] - '@rollup/rollup-linux-arm64-musl@4.34.9': - resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==} + '@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] - '@rollup/rollup-linux-loongarch64-gnu@4.34.9': - resolution: {integrity: sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': - resolution: {integrity: sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==} + '@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] - '@rollup/rollup-linux-riscv64-gnu@4.34.9': - resolution: {integrity: sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.34.9': - resolution: {integrity: sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==} + '@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] - '@rollup/rollup-linux-x64-gnu@4.34.9': - resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==} + '@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] - '@rollup/rollup-linux-x64-musl@4.34.9': - resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==} + '@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] - '@rollup/rollup-win32-arm64-msvc@4.34.9': - resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==} + '@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] - '@rollup/rollup-win32-ia32-msvc@4.34.9': - resolution: {integrity: sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==} + '@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==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.34.9': - resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==} + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} cpu: [x64] os: [win32] - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] - '@rushstack/eslint-patch@1.10.4': - resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + '@rspack/binding-darwin-arm64@2.0.2': + resolution: {integrity: sha512-0o7lbgBBsDlICWdjIH0q3e0BsSco4GRiImHWVfZSVEG+q2+ykZJvSvYCVhPM1Co375Z0S3VMPa/8SjcY1FHwlw==} + cpu: [arm64] + os: [darwin] - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + '@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==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@2.0.2': + resolution: {integrity: sha512-y7Q0S1FE+OlkL5GMqLG0PwxrPw6E1r892KhGrGKE1Vdufe5YTEx6xTPxzZ+b7N2KPD7s9G1/iJmWHQxb1+Bjkg==} + cpu: [x64] + os: [win32] + + '@rspack/binding@2.0.2': + resolution: {integrity: sha512-0kZPplW9GWx8mfC6DfsaRY3QBIYPuUs42JfmSM6aSb8tMHZAXQeLeMB8M+h8i4SeI+aFtCgO6UuYGtyWf7+L+A==} + + '@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 + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@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==} @@ -1803,169 +2114,200 @@ packages: peerDependencies: tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' - '@tailwindcss/forms@0.5.10': - resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - '@tailwindcss/typography@0.5.16': - resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} + '@tailwindcss/typography@0.5.20': + resolution: {integrity: sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==} peerDependencies: - tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + tailwindcss: '>=3.0.0 || >=4.0.0 || insiders' - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + '@turbo/darwin-64@2.10.8': + resolution: {integrity: sha512-po+7rfJfUnFXjWlcoN2RwhErgzCdRtBc1T26vYPcywHlggmCQiQe1uWaE4j+BibI2uY9/2pDoFzMN0rmSaPFOw==} + cpu: [x64] + os: [darwin] - '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@turbo/darwin-arm64@2.10.8': + resolution: {integrity: sha512-+zB2btDJ00lnPRuqOvpVvgl4x34k/djZQGZTTCfjn7JgNCl8QFY5Njo5+dqkY1g/+9gbbsnAvWm9CmJg9ebcXA==} + cpu: [arm64] + os: [darwin] - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + '@turbo/linux-64@2.10.8': + resolution: {integrity: sha512-K1dxqiVisyN7cViVsfQLs6xscQbYuI8aO2nbUhFURDACgEDfZRdP/b4CCxeosBJpcMfhYyiibWqJorCnvz9kKg==} + cpu: [x64] + os: [android, linux] - '@types/babel__traverse@7.20.6': - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@turbo/linux-arm64@2.10.8': + resolution: {integrity: sha512-Gi77ibVnrE1fEmvr+/wBD/yvRqhwp/RQuCp2+//lv1U1wNFFyVg0V7Wj8FG9FXPFAw5QHReo8rxc9+wBSDZjzA==} + cpu: [arm64] + os: [android, linux] - '@types/braces@3.0.5': - resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} + '@turbo/windows-64@2.10.8': + resolution: {integrity: sha512-znnLO1haJPYTHoKMKwlAvlkjRiYbbhBzME6wIGaMd+fwir23U6jVd1ecaTWWi1fbnRVqxMfgDBKseQ/hLKb83g==} + cpu: [x64] + os: [win32] - '@types/bun@1.2.8': - resolution: {integrity: sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w==} + '@turbo/windows-arm64@2.10.8': + resolution: {integrity: sha512-VN30vh3b3Czh2WzYHNTfF1FE0YMZ5aHsLO8dBMGHJewA6792wX6iJR8ZxlzFW6WdOu0gEAKIvlYhfyT81Wkm4Q==} + cpu: [arm64] + os: [win32] - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@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.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/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} - '@types/node@20.14.13': - resolution: {integrity: sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==} + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} '@types/postcss-import@14.0.3': resolution: {integrity: sha512-raZhRVTf6Vw5+QbmQ7LOHSDML71A5rj4+EqDzAbrZPfxfoGzFxMHRCq16VlddGIZpHELw0BG4G0YE2ANkdZiIQ==} - '@types/react-dom@19.0.4': - resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^19.0.0 + '@types/react': ^19.2.0 - '@types/react@19.0.12': - resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==} + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} - '@types/ws@8.5.12': - resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + '@types/semver@7.8.0': + resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} - '@typescript-eslint/eslint-plugin@8.11.0': - resolution: {integrity: sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '*' + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 peerDependenciesMeta: - typescript: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: 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} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '*' + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: - typescript: + msw: + optional: true + vite: 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} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@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 + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@typescript-eslint/types@8.11.0': - resolution: {integrity: sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@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 + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@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 + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - '@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} + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - '@vitejs/plugin-react@4.3.4': - resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - '@vitest/expect@2.0.5': - resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - '@vitest/pretty-format@2.0.5': - resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - '@vitest/runner@2.0.5': - resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - '@vitest/snapshot@2.0.5': - resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - '@vitest/spy@2.0.5': - resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - '@vitest/utils@2.0.5': - resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + '@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==} engines: {node: '>=0.4.0'} hasBin: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 - 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'} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -1980,105 +2322,48 @@ 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'} - ast-types-flow@0.0.8: - resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} + hasBin: true - 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==} binary-extensions@2.3.0: 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.24.2: - resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} 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.2.7: - resolution: {integrity: sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA==} + bun-types@1.3.14: + resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} - bun@1.2.8: - resolution: {integrity: sha512-X8r9UuXAruvpE37u/JVfvJI8KRdFf9hUdTLw00DMScnBe7Xerawd/VvmFVT9Y/NrmXDAdDp0Dm6N6bulZYTGvA==} - cpu: [arm64, x64, aarch64] - os: [darwin, linux, win32] + bun@1.3.14: + resolution: {integrity: sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg==} + cpu: [arm64, x64] + os: [darwin, linux, android, freebsd, win32] hasBin: true bundle-require@5.1.0: @@ -2087,40 +2372,23 @@ packages: peerDependencies: esbuild: '>=0.18' - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - cac@6.7.14: 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.30001705: - resolution: {integrity: sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} - chai@5.1.1: - resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} - engines: {node: '>=12'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} @@ -2130,29 +2398,27 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - citty@0.1.6: - resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + 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==} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - clipboardy@4.0.0: - resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} - engines: {node: '>=18'} + clipanion@4.0.0-rc.4: + resolution: {integrity: sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==} + peerDependencies: + typanion: '*' - 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==} commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2161,69 +2427,44 @@ 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.2.3: - resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - consola@3.4.0: - resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} - engines: {node: ^14.18.0 || >=16.10.0} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-es@1.2.2: - resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - crossws@0.3.1: - resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==} - - crossws@0.3.4: - resolution: {integrity: sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==} + 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.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - 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==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2231,51 +2472,27 @@ packages: 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 - - dedent@1.5.3: - resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: babel-plugin-macros: optional: true - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - 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.3: - resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} hasBin: true - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} didyoumean@1.2.2: @@ -2284,77 +2501,33 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} + electron-to-chromium@1.5.361: + resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + emnapi@1.11.3: + resolution: {integrity: sha512-+/ZS90YK/rYfVOHtGLHkGffVsnmD/MAKaBHio+Y4XAtg75RLr4cveV/w0jTkUdLM1CcAlaRgG76mpIemWAlk0A==} + peerDependencies: + node-addon-api: '>= 6.1.0' + peerDependenciesMeta: + node-addon-api: + optional: true - electron-to-chromium@1.5.119: - resolution: {integrity: sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==} - - electron-to-chromium@1.5.45: - resolution: {integrity: sha512-vOzZS6uZwhhbkZbcRyiy99Wg+pYFV5hk+5YaECvx0+Z31NR3Tt5zS6dze2OepT6PCTzVzT0dIJItti+uAW5zmw==} - - 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.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} 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-iterator-helpers@1.1.0: - resolution: {integrity: sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==} - engines: {node: '>= 0.4'} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-object-atoms@1.0.0: - resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} - engines: {node: '>= 0.4'} + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} - 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'} - - 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.24.0: - resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.25.0: - resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true @@ -2362,118 +2535,18 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-config-next@15.2.4: - resolution: {integrity: sha512-v4gYjd4eYIme8qzaJItpR5MMBXJ0/YV07u7eb50kEnlEmX7yhOjdUdzz70v4fiINYRjLf8X8TbogF0k7wlz6sA==} - 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.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} - 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.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.22.0: - resolution: {integrity: sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} 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'} @@ -2481,74 +2554,68 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-content-type-parse@3.0.0: + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.1: - resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} - engines: {node: '>=8.6.0'} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fast-stringify@4.0.0: + resolution: {integrity: sha512-lE2DIivBaLysf6hK5WH/VfMgqRbvBVHcpGVVTmA5Zi8oWIjq9YxIt6lYGdUgP1HNSXxTIat7HEIDnrSvXSeKQw==} - fdir@6.4.3: - resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} + 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'} 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.0: - resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} 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==} - 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==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} @@ -2563,37 +2630,8 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - 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.8.1: - resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2603,419 +2641,184 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - 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@14.1.0: - resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} - engines: {node: '>=18'} - - gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + globby@16.2.2: + resolution: {integrity: sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==} + engines: {node: '>=20'} graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - h3@1.15.1: - resolution: {integrity: sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==} - - has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - 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==} + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} 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'} - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - ignore@7.0.3: - resolution: {integrity: sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==} - 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-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} 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.0: - resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} - 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-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} - 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==} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-with-bigint@3.5.8: + resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] - 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-darwin-arm64@1.29.2: - resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.29.2: - resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.29.2: - resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.29.2: - resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.29.2: - resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.29.2: - resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.29.2: - resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.29.2: - resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.29.2: - resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.29.2: - resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.29.2: - resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - listhen@1.9.0: - resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} + listhen@1.10.1: + resolution: {integrity: sha512-6nt/86SkqUQSLW1ofz8MxC6RhRMqOl3ONISe6qqvJ3xj09aJWQx6DhgSZpugs3PX4PXdOas/WD6A9jx6J2N19A==} 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'} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - lodash.castarray@4.4.0: - resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - 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.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + 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==} @@ -3024,75 +2827,101 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micro-memoize@4.1.3: - resolution: {integrity: sha512-DzRMi8smUZXT7rCGikRwldEh6eO6qzKiPPopcr1+2EY3AYKpy5fu159PKWwIS9A6IWnrvPKDMcuFtyrroZa8Bw==} - - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} - engines: {node: '>=8.6'} + micro-memoize@5.1.1: + resolution: {integrity: sha512-QDwluos8YeMijiKxZGwaV4f4tzj0soS6+xcsJhJ3+4wdEIHMyKbIKVUziebOgWX3e6yiijdoaHo+9tyhbnaWXA==} micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} mini-svg-data-uri@1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + 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@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.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} 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} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@15.2.4: - resolution: {integrity: sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.2.7: + resolution: {integrity: sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 + '@playwright/test': ^1.51.1 babel-plugin-react-compiler: '*' react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -3110,47 +2939,29 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-addon-api@8.2.2: - resolution: {integrity: sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==} + node-addon-api@8.7.0: + resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} engines: {node: ^18 || ^20 || >= 21} - 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==} + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} 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.0: - resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} - node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} 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'} @@ -3159,157 +2970,77 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - object-inspect@1.13.2: - resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} - engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - 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==} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} 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==} - - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + 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==} engines: {node: '>=12'} pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkg-types@1.3.0: - resolution: {integrity: sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - playwright-core@1.51.1: - resolution: {integrity: sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==} - engines: {node: '>=18'} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} hasBin: true - playwright@1.51.1: - resolution: {integrity: sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==} - engines: {node: '>=18'} + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} 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'} peerDependencies: postcss: ^8.0.0 - postcss-import@16.1.0: - resolution: {integrity: sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==} + postcss-import@16.1.1: + resolution: {integrity: sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==} engines: {node: '>=18.0.0'} peerDependencies: postcss: ^8.0.0 - postcss-js@4.0.1: - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} 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'} @@ -3342,8 +3073,8 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss-selector-parser@7.1.0: - resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} engines: {node: '>=4'} postcss-value-parser@4.2.0: @@ -3353,70 +3084,54 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.41: - resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} + prettier-plugin-embed@0.5.1: + resolution: {integrity: sha512-2Ege8gIlLNTvHElUeU5XcFsD7/dbDXkQA6H9TczHSJAGxB58nNjjifk/dlRMS5E29eqQx/z+ToA4ZVMWzjME/A==} - 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.0.0: - resolution: {integrity: sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==} + prettier-plugin-organize-imports@4.3.0: + resolution: {integrity: sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==} peerDependencies: - '@vue/language-plugin-pug': ^2.0.24 prettier: '>=2.0' typescript: '>=2.9' - vue-tsc: ^2.0.24 + vue-tsc: ^2.1.0 || 3 peerDependenciesMeta: - '@vue/language-plugin-pug': - optional: true vue-tsc: optional: true - prettier@3.5.0: - resolution: {integrity: sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==} + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} 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.0.0: - resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: - react: ^19.0.0 + react: ^19.2.6 - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 - react-refresh@0.14.2: - resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} - react@19.0.0: - resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -3426,93 +3141,59 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - readdirp@4.1.1: - resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.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'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} hasBin: true - 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==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.34.9: - resolution: {integrity: sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==} + rolldown@1.2.1: + resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} 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'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - safe-regex-test@1.0.3: - resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} - engines: {node: '>= 0.4'} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - scheduler@0.25.0: - resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + 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.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true - 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.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} 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==} @@ -3520,17 +3201,10 @@ 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'} - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3542,69 +3216,18 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - - 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'} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} @@ -3619,34 +3242,34 @@ packages: babel-plugin-macros: optional: true - sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - system-architecture@0.1.0: - resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} - engines: {node: '>=18'} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} - tailwindcss@3.4.14: - resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==} + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} hasBin: true - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - terser@5.31.6: - resolution: {integrity: sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==} + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} engines: {node: '>=10'} hasBin: true @@ -3663,32 +3286,33 @@ 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==} - tinyglobby@0.2.12: - resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} + 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==} engines: {node: '>=12.0.0'} - tinypool@1.0.0: - resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} - engines: {node: ^18.0.0 || >=20.0.0} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.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==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} 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 @@ -3709,26 +3333,17 @@ packages: tree-sitter: optional: true - 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' + tree-sitter@0.25.1: + resolution: {integrity: sha512-mrcEdkYtHfrK1A6fs3O6FxkBo0Qig5XUXqHhxUOQu0bmPo00QF4XaSx4edpazdHwxnSCjlGKGgIqWdaN4dvTLA==} 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==} - tslib@2.8.0: - resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==} - - tsup@8.4.0: - resolution: {integrity: sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -3746,175 +3361,90 @@ packages: typescript: optional: true - tsx@4.19.1: - resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} - engines: {node: '>=18.0.0'} + turbo@2.10.8: + resolution: {integrity: sha512-9+8YX5QOkGXzZxcIykTHgaooRHGMWO+jfdyRK0o+rN0U7hBIig2MrJ8r/aNzIPDPhdA73SGb0O+tIztaModTMg==} hasBin: true - turbo-darwin-64@2.4.4: - resolution: {integrity: sha512-5kPvRkLAfmWI0MH96D+/THnDMGXlFNmjeqNRj5grLKiry+M9pKj3pRuScddAXPdlxjO5Ptz06UNaOQrrYGTx1g==} - cpu: [x64] - os: [darwin] + typanion@3.14.0: + resolution: {integrity: sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==} - turbo-darwin-arm64@2.4.4: - resolution: {integrity: sha512-/gtHPqbGQXDFhrmy+Q/MFW2HUTUlThJ97WLLSe4bxkDrKHecDYhAjbZ4rN3MM93RV9STQb3Tqy4pZBtsd4DfCw==} - cpu: [arm64] - os: [darwin] + type-fest@5.6.0: + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + engines: {node: '>=20'} - turbo-linux-64@2.4.4: - resolution: {integrity: sha512-SR0gri4k0bda56hw5u9VgDXLKb1Q+jrw4lM7WAhnNdXvVoep4d6LmnzgMHQQR12Wxl3KyWPbkz9d1whL6NTm2Q==} - cpu: [x64] - os: [linux] - - turbo-linux-arm64@2.4.4: - resolution: {integrity: sha512-COXXwzRd3vslQIfJhXUklgEqlwq35uFUZ7hnN+AUyXx7hUOLIiD5NblL+ETrHnhY4TzWszrbwUMfe2BYWtaPQg==} - cpu: [arm64] - os: [linux] - - turbo-windows-64@2.4.4: - resolution: {integrity: sha512-PV9rYNouGz4Ff3fd6sIfQy5L7HT9a4fcZoEv8PKRavU9O75G7PoDtm8scpHU10QnK0QQNLbE9qNxOAeRvF0fJg==} - cpu: [x64] - os: [win32] - - turbo-windows-arm64@2.4.4: - resolution: {integrity: sha512-403sqp9t5sx6YGEC32IfZTVWkRAixOQomGYB8kEc6ZD+//LirSxzeCHCnM8EmSXw7l57U1G+Fb0kxgTcKPU/Lg==} - cpu: [arm64] - os: [win32] - - turbo@2.4.4: - resolution: {integrity: sha512-N9FDOVaY3yz0YCOhYIgOGYad7+m2ptvinXygw27WPLQvcZDl3+0Sa77KGVlLSiuPDChOUEnTKE9VJwLSi9BPGQ==} - hasBin: true - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - 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==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true - ufo@1.5.4: - resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} - - unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} 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==} - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - untun@0.1.3: - resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + + untun@0.2.2: + resolution: {integrity: sha512-+NnOJcSiEtYsVgJmXUzQbJeRAFXJC4yPJYuh6kF9B0Rm6zunXcs/3GZOTllyocSbUDIxD6Bj7e/4ATw7sph1Sw==} hasBin: true - update-browserslist-db@1.1.1: - resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - uqr@0.1.2: - resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uqr@0.1.3: + resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - 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} + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + engines: {node: ^20.19.0 || >=22.12.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@6.0.0: - resolution: {integrity: sha512-Q2+5yQV79EdnpbNxjD3/QHVMCBaQ3Kpd4/uL51UGuh38bIIM+s4o3FqyCzRvTRwFb+cWIUeZvaWwS9y2LD2qeQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@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: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 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: @@ -3930,23 +3460,39 @@ packages: yaml: optional: true - vitest@2.0.5: - resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} - engines: {node: ^18.0.0 || >=20.0.0} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.0.5 - '@vitest/ui': 2.0.5 + '@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 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': + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': optional: true '@vitest/ui': optional: true @@ -3955,667 +3501,704 @@ packages: jsdom: optional: true - webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} - whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} + 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'} + webpack@5.109.2: + resolution: {integrity: sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==} + engines: {node: '>=10.13.0'} 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 - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - 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'} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} hasBin: true - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - snapshots: '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.3.0': + '@emnapi/core@1.10.0': dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true - '@babel/code-frame@7.26.2': + '@emnapi/core@1.11.3': dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 + '@emnapi/wasi-threads': 1.2.3 + tslib: 2.8.1 - '@babel/compat-data@7.26.3': {} - - '@babel/core@7.26.0': + '@emnapi/core@2.0.0-alpha.3': dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.3 - '@babel/helper-compilation-targets': 7.25.9 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) - '@babel/helpers': 7.26.0 - '@babel/parser': 7.26.3 - '@babel/template': 7.25.9 - '@babel/traverse': 7.26.4 - '@babel/types': 7.26.3 - convert-source-map: 2.0.0 - debug: 4.4.0 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + '@emnapi/wasi-threads': 2.0.1 + tslib: 2.8.1 + optional: true - '@babel/generator@7.26.3': + '@emnapi/runtime@1.10.0': dependencies: - '@babel/parser': 7.26.3 - '@babel/types': 7.26.3 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 3.1.0 + tslib: 2.8.1 + optional: true - '@babel/helper-compilation-targets@7.25.9': + '@emnapi/runtime@1.11.3': dependencies: - '@babel/compat-data': 7.26.3 - '@babel/helper-validator-option': 7.25.9 - browserslist: 4.24.2 - lru-cache: 5.1.1 - semver: 6.3.1 + tslib: 2.8.1 - '@babel/helper-module-imports@7.25.9': + '@emnapi/runtime@2.0.0-alpha.3': dependencies: - '@babel/traverse': 7.26.4 - '@babel/types': 7.26.3 - transitivePeerDependencies: - - supports-color + tslib: 2.8.1 + optional: true - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + '@emnapi/wasi-threads@1.2.1': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.26.4 - transitivePeerDependencies: - - supports-color + tslib: 2.8.1 + optional: true - '@babel/helper-plugin-utils@7.25.9': {} - - '@babel/helper-string-parser@7.25.9': {} - - '@babel/helper-validator-identifier@7.25.9': {} - - '@babel/helper-validator-option@7.25.9': {} - - '@babel/helpers@7.26.0': + '@emnapi/wasi-threads@1.2.3': dependencies: - '@babel/template': 7.25.9 - '@babel/types': 7.26.3 + tslib: 2.8.1 - '@babel/parser@7.26.3': + '@emnapi/wasi-threads@2.0.1': dependencies: - '@babel/types': 7.26.3 - - '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.25.9 - - '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.25.9 - - '@babel/template@7.25.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.3 - '@babel/types': 7.26.3 - - '@babel/traverse@7.26.4': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.3 - '@babel/parser': 7.26.3 - '@babel/template': 7.25.9 - '@babel/types': 7.26.3 - debug: 4.4.0 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.26.3': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - - '@emnapi/runtime@1.3.1': - dependencies: - tslib: 2.8.0 - optional: true - - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/aix-ppc64@0.23.1': - optional: true - - '@esbuild/aix-ppc64@0.24.0': - optional: true - - '@esbuild/aix-ppc64@0.25.0': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.23.1': - optional: true - - '@esbuild/android-arm64@0.24.0': - optional: true - - '@esbuild/android-arm64@0.25.0': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-arm@0.23.1': - optional: true - - '@esbuild/android-arm@0.24.0': - optional: true - - '@esbuild/android-arm@0.25.0': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/android-x64@0.23.1': - optional: true - - '@esbuild/android-x64@0.24.0': - optional: true - - '@esbuild/android-x64@0.25.0': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.23.1': - optional: true - - '@esbuild/darwin-arm64@0.24.0': - optional: true - - '@esbuild/darwin-arm64@0.25.0': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.23.1': - optional: true - - '@esbuild/darwin-x64@0.24.0': - optional: true - - '@esbuild/darwin-x64@0.25.0': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.23.1': - optional: true - - '@esbuild/freebsd-arm64@0.24.0': - optional: true - - '@esbuild/freebsd-arm64@0.25.0': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.23.1': - optional: true - - '@esbuild/freebsd-x64@0.24.0': - optional: true - - '@esbuild/freebsd-x64@0.25.0': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.23.1': - optional: true - - '@esbuild/linux-arm64@0.24.0': - optional: true - - '@esbuild/linux-arm64@0.25.0': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-arm@0.23.1': - optional: true - - '@esbuild/linux-arm@0.24.0': - optional: true - - '@esbuild/linux-arm@0.25.0': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.23.1': - optional: true - - '@esbuild/linux-ia32@0.24.0': - optional: true - - '@esbuild/linux-ia32@0.25.0': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.23.1': - optional: true - - '@esbuild/linux-loong64@0.24.0': - optional: true - - '@esbuild/linux-loong64@0.25.0': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.23.1': - optional: true - - '@esbuild/linux-mips64el@0.24.0': - optional: true - - '@esbuild/linux-mips64el@0.25.0': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.23.1': + tslib: 2.8.1 optional: true - '@esbuild/linux-ppc64@0.24.0': + '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/linux-ppc64@0.25.0': + '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.21.5': + '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/linux-riscv64@0.23.1': + '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.24.0': + '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.25.0': + '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/linux-s390x@0.21.5': + '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/linux-s390x@0.23.1': + '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/linux-s390x@0.24.0': + '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-s390x@0.25.0': + '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-x64@0.21.5': + '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-x64@0.23.1': + '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-x64@0.24.0': + '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-x64@0.25.0': + '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.25.0': + '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.21.5': + '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/netbsd-x64@0.23.1': + '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.24.0': + '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.25.0': + '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.23.1': + '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.24.0': + '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.25.0': + '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.21.5': + '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.23.1': + '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.24.0': + '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/openbsd-x64@0.25.0': + '@esbuild/win32-x64@0.27.7': optional: true - '@esbuild/sunos-x64@0.21.5': + '@img/colour@1.1.0': optional: true - '@esbuild/sunos-x64@0.23.1': - optional: true - - '@esbuild/sunos-x64@0.24.0': - 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.24.0': - 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.24.0': - 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.24.0': - optional: true - - '@esbuild/win32-x64@0.25.0': - optional: true - - '@eslint-community/eslint-utils@4.4.0(eslint@9.22.0(jiti@2.4.2))': - dependencies: - eslint: 9.22.0(jiti@2.4.2) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.1': {} - - '@eslint/config-array@0.19.2': - dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.0 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.1.0': {} - - '@eslint/core@0.12.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.0': - dependencies: - ajv: 6.12.6 - debug: 4.4.0 - espree: 10.3.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.22.0': {} - - '@eslint/object-schema@2.1.6': {} - - '@eslint/plugin-kit@0.2.7': - dependencies: - '@eslint/core': 0.12.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.33.5': + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.33.5': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.0.4': + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.0.4': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.0.4': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.0.5': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.0.4': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.0.4': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.0.4': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.33.5': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.33.5': + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.33.5': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.33.5': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.33.5': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.33.5': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-wasm32@0.33.5': + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.3.1 + '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-win32-ia32@0.33.5': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-x64@0.33.5': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@isaacs/cliui@8.0.2': + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@inquirer/ansi@2.0.7': {} + + '@inquirer/checkbox@5.2.1(@types/node@25.9.1)': 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 + '@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) + optionalDependencies: + '@types/node': 25.9.1 - '@jridgewell/gen-mapping@0.3.5': + '@inquirer/confirm@6.1.1(@types/node@25.9.1)': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@inquirer/core': 11.2.1(@types/node@25.9.1) + '@inquirer/type': 4.0.7(@types/node@25.9.1) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/core@11.2.1(@types/node@25.9.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@25.9.1) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/editor@5.2.2(@types/node@25.9.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) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/expand@5.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) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/external-editor@3.0.3(@types/node@25.9.1)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/figures@2.0.7': {} + + '@inquirer/input@5.1.2(@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) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/number@4.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) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/password@5.1.1(@types/node@25.9.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) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/prompts@8.5.2(@types/node@25.9.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) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/rawlist@5.3.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) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/search@4.2.1(@types/node@25.9.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) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/select@5.2.1(@types/node@25.9.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) + optionalDependencies: + '@types/node': 25.9.1 + + '@inquirer/type@4.0.7(@types/node@25.9.1)': + optionalDependencies: + '@types/node': 25.9.1 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - optional: true + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/cli@2.18.4': {} - - '@next/env@15.2.4': {} - - '@next/eslint-plugin-next@15.2.4': + '@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)': dependencies: - fast-glob: 3.3.1 + '@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 + 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 + typanion: 3.14.0 + optionalDependencies: + '@emnapi/runtime': 1.11.3 + transitivePeerDependencies: + - '@emnapi/core' + - '@napi-rs/cross-toolchain-arm64-target-aarch64' + - '@napi-rs/cross-toolchain-arm64-target-armv7' + - '@napi-rs/cross-toolchain-arm64-target-ppc64le' + - '@napi-rs/cross-toolchain-arm64-target-s390x' + - '@napi-rs/cross-toolchain-arm64-target-x86_64' + - '@napi-rs/cross-toolchain-x64-target-aarch64' + - '@napi-rs/cross-toolchain-x64-target-armv7' + - '@napi-rs/cross-toolchain-x64-target-ppc64le' + - '@napi-rs/cross-toolchain-x64-target-s390x' + - '@napi-rs/cross-toolchain-x64-target-x86_64' + - '@types/node' + - node-addon-api + - supports-color - '@next/swc-darwin-arm64@15.2.4': + '@napi-rs/cross-toolchain@1.0.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.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) + debug: 4.4.3 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - supports-color + + '@napi-rs/lzma-android-arm-eabi@1.4.5': optional: true - '@next/swc-darwin-x64@15.2.4': + '@napi-rs/lzma-android-arm64@1.4.5': optional: true - '@next/swc-linux-arm64-gnu@15.2.4': + '@napi-rs/lzma-darwin-arm64@1.4.5': optional: true - '@next/swc-linux-arm64-musl@15.2.4': + '@napi-rs/lzma-darwin-x64@1.4.5': optional: true - '@next/swc-linux-x64-gnu@15.2.4': + '@napi-rs/lzma-freebsd-x64@1.4.5': optional: true - '@next/swc-linux-x64-musl@15.2.4': + '@napi-rs/lzma-linux-arm-gnueabihf@1.4.5': optional: true - '@next/swc-win32-arm64-msvc@15.2.4': + '@napi-rs/lzma-linux-arm64-gnu@1.4.5': optional: true - '@next/swc-win32-x64-msvc@15.2.4': + '@napi-rs/lzma-linux-arm64-musl@1.4.5': + optional: true + + '@napi-rs/lzma-linux-ppc64-gnu@1.4.5': + optional: true + + '@napi-rs/lzma-linux-riscv64-gnu@1.4.5': + optional: true + + '@napi-rs/lzma-linux-s390x-gnu@1.4.5': + optional: true + + '@napi-rs/lzma-linux-x64-gnu@1.4.5': + optional: true + + '@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)': + dependencies: + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@napi-rs/lzma-win32-arm64-msvc@1.4.5': + optional: true + + '@napi-rs/lzma-win32-ia32-msvc@1.4.5': + optional: true + + '@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)': + optionalDependencies: + '@napi-rs/lzma-android-arm-eabi': 1.4.5 + '@napi-rs/lzma-android-arm64': 1.4.5 + '@napi-rs/lzma-darwin-arm64': 1.4.5 + '@napi-rs/lzma-darwin-x64': 1.4.5 + '@napi-rs/lzma-freebsd-x64': 1.4.5 + '@napi-rs/lzma-linux-arm-gnueabihf': 1.4.5 + '@napi-rs/lzma-linux-arm64-gnu': 1.4.5 + '@napi-rs/lzma-linux-arm64-musl': 1.4.5 + '@napi-rs/lzma-linux-ppc64-gnu': 1.4.5 + '@napi-rs/lzma-linux-riscv64-gnu': 1.4.5 + '@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-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 + + '@napi-rs/tar-android-arm64@1.1.0': + optional: true + + '@napi-rs/tar-darwin-arm64@1.1.0': + optional: true + + '@napi-rs/tar-darwin-x64@1.1.0': + optional: true + + '@napi-rs/tar-freebsd-x64@1.1.0': + optional: true + + '@napi-rs/tar-linux-arm-gnueabihf@1.1.0': + optional: true + + '@napi-rs/tar-linux-arm64-gnu@1.1.0': + optional: true + + '@napi-rs/tar-linux-arm64-musl@1.1.0': + optional: true + + '@napi-rs/tar-linux-ppc64-gnu@1.1.0': + optional: true + + '@napi-rs/tar-linux-s390x-gnu@1.1.0': + optional: true + + '@napi-rs/tar-linux-x64-gnu@1.1.0': + optional: true + + '@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)': + dependencies: + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@napi-rs/tar-win32-arm64-msvc@1.1.0': + optional: true + + '@napi-rs/tar-win32-ia32-msvc@1.1.0': + optional: true + + '@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)': + optionalDependencies: + '@napi-rs/tar-android-arm-eabi': 1.1.0 + '@napi-rs/tar-android-arm64': 1.1.0 + '@napi-rs/tar-darwin-arm64': 1.1.0 + '@napi-rs/tar-darwin-x64': 1.1.0 + '@napi-rs/tar-freebsd-x64': 1.1.0 + '@napi-rs/tar-linux-arm-gnueabihf': 1.1.0 + '@napi-rs/tar-linux-arm64-gnu': 1.1.0 + '@napi-rs/tar-linux-arm64-musl': 1.1.0 + '@napi-rs/tar-linux-ppc64-gnu': 1.1.0 + '@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-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)': + 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 + + '@napi-rs/wasm-tools-android-arm-eabi@1.0.1': + optional: true + + '@napi-rs/wasm-tools-android-arm64@1.0.1': + optional: true + + '@napi-rs/wasm-tools-darwin-arm64@1.0.1': + optional: true + + '@napi-rs/wasm-tools-darwin-x64@1.0.1': + optional: true + + '@napi-rs/wasm-tools-freebsd-x64@1.0.1': + optional: true + + '@napi-rs/wasm-tools-linux-arm64-gnu@1.0.1': + optional: true + + '@napi-rs/wasm-tools-linux-arm64-musl@1.0.1': + optional: true + + '@napi-rs/wasm-tools-linux-x64-gnu@1.0.1': + optional: true + + '@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)': + dependencies: + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@napi-rs/wasm-tools-win32-arm64-msvc@1.0.1': + optional: true + + '@napi-rs/wasm-tools-win32-ia32-msvc@1.0.1': + optional: true + + '@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)': + optionalDependencies: + '@napi-rs/wasm-tools-android-arm-eabi': 1.0.1 + '@napi-rs/wasm-tools-android-arm64': 1.0.1 + '@napi-rs/wasm-tools-darwin-arm64': 1.0.1 + '@napi-rs/wasm-tools-darwin-x64': 1.0.1 + '@napi-rs/wasm-tools-freebsd-x64': 1.0.1 + '@napi-rs/wasm-tools-linux-arm64-gnu': 1.0.1 + '@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-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/swc-darwin-arm64@16.2.7': + optional: true + + '@next/swc-darwin-x64@16.2.7': + optional: true + + '@next/swc-linux-arm64-gnu@16.2.7': + optional: true + + '@next/swc-linux-arm64-musl@16.2.7': + optional: true + + '@next/swc-linux-x64-gnu@16.2.7': + optional: true + + '@next/swc-linux-x64-musl@16.2.7': + optional: true + + '@next/swc-win32-arm64-msvc@16.2.7': + optional: true + + '@next/swc-win32-x64-msvc@16.2.7': optional: true '@nodelib/fs.scandir@2.1.5': @@ -4628,716 +4211,665 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 + fastq: 1.20.1 - '@nolyfill/is-core-module@1.0.39': {} + '@octokit/auth-token@6.0.0': {} - '@oven/bun-darwin-aarch64@1.2.8': + '@octokit/core@7.0.6': + 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 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@11.0.3': + dependencies: + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@9.0.3': + dependencies: + '@octokit/request': 10.0.9 + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/openapi-types@27.0.0': {} + + '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/types': 16.0.0 + + '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + + '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/types': 16.0.0 + + '@octokit/request-error@7.1.0': + dependencies: + '@octokit/types': 16.0.0 + + '@octokit/request@10.0.9': + dependencies: + '@octokit/endpoint': 11.0.3 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + content-type: 2.0.0 + fast-content-type-parse: 3.0.0 + json-with-bigint: 3.5.8 + universal-user-agent: 7.0.3 + + '@octokit/rest@22.0.1': + 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/types@16.0.0': + dependencies: + '@octokit/openapi-types': 27.0.0 + + '@oven/bun-darwin-aarch64@1.3.14': optional: true - '@oven/bun-darwin-x64-baseline@1.2.8': + '@oven/bun-darwin-x64-baseline@1.3.14': optional: true - '@oven/bun-darwin-x64@1.2.8': + '@oven/bun-darwin-x64@1.3.14': optional: true - '@oven/bun-linux-aarch64-musl@1.2.8': + '@oven/bun-freebsd-aarch64@1.3.14': optional: true - '@oven/bun-linux-aarch64@1.2.8': + '@oven/bun-freebsd-x64@1.3.14': optional: true - '@oven/bun-linux-x64-baseline@1.2.8': + '@oven/bun-linux-aarch64-android@1.3.14': optional: true - '@oven/bun-linux-x64-musl-baseline@1.2.8': + '@oven/bun-linux-aarch64-musl@1.3.14': optional: true - '@oven/bun-linux-x64-musl@1.2.8': + '@oven/bun-linux-aarch64@1.3.14': optional: true - '@oven/bun-linux-x64@1.2.8': + '@oven/bun-linux-x64-android@1.3.14': optional: true - '@oven/bun-windows-x64-baseline@1.2.8': + '@oven/bun-linux-x64-baseline@1.3.14': optional: true - '@oven/bun-windows-x64@1.2.8': + '@oven/bun-linux-x64-musl-baseline@1.3.14': optional: true - '@parcel/watcher-android-arm64@2.5.0': + '@oven/bun-linux-x64-musl@1.3.14': optional: true - '@parcel/watcher-android-arm64@2.5.1': + '@oven/bun-linux-x64@1.3.14': optional: true - '@parcel/watcher-darwin-arm64@2.5.0': + '@oven/bun-windows-aarch64@1.3.14': optional: true - '@parcel/watcher-darwin-arm64@2.5.1': {} - - '@parcel/watcher-darwin-x64@2.5.0': + '@oven/bun-windows-x64-baseline@1.3.14': optional: true - '@parcel/watcher-darwin-x64@2.5.1': {} - - '@parcel/watcher-freebsd-x64@2.5.0': + '@oven/bun-windows-x64@1.3.14': optional: true - '@parcel/watcher-freebsd-x64@2.5.1': + '@oxc-project/types@0.142.0': {} + + '@parcel/watcher-android-arm64@2.6.0': optional: true - '@parcel/watcher-linux-arm-glibc@2.5.0': + '@parcel/watcher-darwin-arm64@2.6.0': {} + + '@parcel/watcher-darwin-x64@2.6.0': {} + + '@parcel/watcher-freebsd-x64@2.6.0': optional: true - '@parcel/watcher-linux-arm-glibc@2.5.1': + '@parcel/watcher-linux-arm-glibc@2.6.0': optional: true - '@parcel/watcher-linux-arm-musl@2.5.0': + '@parcel/watcher-linux-arm-musl@2.6.0': optional: true - '@parcel/watcher-linux-arm-musl@2.5.1': - optional: true + '@parcel/watcher-linux-arm64-glibc@2.6.0': {} - '@parcel/watcher-linux-arm64-glibc@2.5.0': - optional: true + '@parcel/watcher-linux-arm64-musl@2.6.0': {} - '@parcel/watcher-linux-arm64-glibc@2.5.1': {} + '@parcel/watcher-linux-x64-glibc@2.6.0': {} - '@parcel/watcher-linux-arm64-musl@2.5.0': - optional: true + '@parcel/watcher-linux-x64-musl@2.6.0': {} - '@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': + '@parcel/watcher-wasm@2.5.6': dependencies: is-glob: 4.0.3 - micromatch: 4.0.7 + picomatch: 4.0.5 - '@parcel/watcher-win32-arm64@2.5.0': + '@parcel/watcher-win32-arm64@2.6.0': optional: true - '@parcel/watcher-win32-arm64@2.5.1': - optional: true + '@parcel/watcher-win32-x64@2.6.0': {} - '@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': + '@parcel/watcher@2.6.0(patch_hash=705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da)': dependencies: - detect-libc: 1.0.3 + detect-libc: 2.1.2 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.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 + '@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@2.5.1(patch_hash=zs2vvlrje3h42xp5ed2v44fep4)': + '@playwright/test@1.62.1': dependencies: - detect-libc: 1.0.3 - is-glob: 4.0.3 - micromatch: 4.0.8 - node-addon-api: 7.1.1 + 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': optionalDependencies: - '@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/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 - '@pkgjs/parseargs@0.11.0': - optional: true - - '@playwright/test@1.51.1': + '@rspack/core@2.0.2(@swc/helpers@0.5.15)': dependencies: - playwright: 1.51.1 + '@rspack/binding': 2.0.2 + optionalDependencies: + '@swc/helpers': 0.5.15 - '@rollup/rollup-android-arm-eabi@4.34.9': - optional: true + '@sindresorhus/merge-streams@4.0.0': {} - '@rollup/rollup-android-arm64@4.34.9': - optional: true - - '@rollup/rollup-darwin-arm64@4.34.9': - optional: true - - '@rollup/rollup-darwin-x64@4.34.9': - optional: true - - '@rollup/rollup-freebsd-arm64@4.34.9': - optional: true - - '@rollup/rollup-freebsd-x64@4.34.9': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.34.9': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.34.9': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.34.9': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.34.9': - optional: true - - '@rollup/rollup-linux-loongarch64-gnu@4.34.9': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.34.9': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.34.9': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.34.9': - optional: true - - '@rollup/rollup-linux-x64-musl@4.34.9': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.34.9': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.34.9': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.34.9': - optional: true - - '@rtsao/scc@1.1.0': {} - - '@rushstack/eslint-patch@1.10.4': {} - - '@sindresorhus/merge-streams@2.3.0': {} - - '@swc/counter@0.1.3': {} + '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.15': dependencies: - tslib: 2.8.0 + tslib: 2.8.1 '@tailwindcss/aspect-ratio@0.4.2(tailwindcss@packages+tailwindcss)': dependencies: tailwindcss: link:packages/tailwindcss - '@tailwindcss/forms@0.5.10(tailwindcss@packages+tailwindcss)': + '@tailwindcss/forms@0.5.11(tailwindcss@packages+tailwindcss)': dependencies: mini-svg-data-uri: 1.4.4 tailwindcss: link:packages/tailwindcss - '@tailwindcss/typography@0.5.16(tailwindcss@packages+tailwindcss)': + '@tailwindcss/typography@0.5.20(tailwindcss@packages+tailwindcss)': dependencies: - lodash.castarray: 4.4.0 - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 tailwindcss: link:packages/tailwindcss - '@types/babel__core@7.20.5': + '@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': dependencies: - '@babel/parser': 7.26.3 - '@babel/types': 7.26.3 - '@types/babel__generator': 7.6.8 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 + tslib: 2.8.1 - '@types/babel__generator@7.6.8': + '@types/bun@1.3.14': dependencies: - '@babel/types': 7.26.3 + bun-types: 1.3.14 - '@types/babel__template@7.4.4': + '@types/chai@5.2.3': dependencies: - '@babel/parser': 7.26.3 - '@babel/types': 7.26.3 + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 - '@types/babel__traverse@7.20.6': - dependencies: - '@babel/types': 7.26.3 + '@types/deep-eql@4.0.2': {} - '@types/braces@3.0.5': {} + '@types/estree@1.0.8': {} - '@types/bun@1.2.8': - dependencies: - bun-types: 1.2.7 - - '@types/estree@1.0.6': {} + '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} - '@types/json5@0.0.29': {} - - '@types/node@20.14.13': + '@types/node@22.20.1': dependencies: - undici-types: 5.26.5 + 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.4.41 + postcss: 8.5.25 - '@types/react-dom@19.0.4(@types/react@19.0.12)': + '@types/react-dom@19.2.3(@types/react@19.2.15)': dependencies: - '@types/react': 19.0.12 + '@types/react': 19.2.15 - '@types/react@19.0.12': + '@types/react@19.2.15': dependencies: - csstype: 3.1.3 + csstype: 3.2.3 - '@types/ws@8.5.12': + '@types/semver@7.8.0': {} + + '@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))': dependencies: - '@types/node': 20.14.13 + '@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) - '@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4)': + '@vitest/expect@4.1.10': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.11.0 - '@typescript-eslint/type-utils': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4) - '@typescript-eslint/utils': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.11.0 - eslint: 9.22.0(jiti@2.4.2) - 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 + '@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 - '@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3)': + '@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))': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) - '@typescript-eslint/scope-manager': 8.11.0 - '@typescript-eslint/type-utils': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) - '@typescript-eslint/utils': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.11.0 - eslint: 9.22.0(jiti@2.4.2) - 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 - - '@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4)': - dependencies: - '@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.0 - eslint: 9.22.0(jiti@2.4.2) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(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.0 - eslint: 9.22.0(jiti@2.4.2) - 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.22.0(jiti@2.4.2))(typescript@5.5.4)': - dependencies: - '@typescript-eslint/typescript-estree': 8.11.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4) - debug: 4.4.0 - 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.22.0(jiti@2.4.2))(typescript@5.6.3)': - dependencies: - '@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3) - '@typescript-eslint/utils': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) - debug: 4.4.0 - 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.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.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.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.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.22.0(jiti@2.4.2))(typescript@5.5.4)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.22.0(jiti@2.4.2)) - '@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.22.0(jiti@2.4.2) - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/utils@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.22.0(jiti@2.4.2)) - '@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.22.0(jiti@2.4.2) - 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@4.3.4(vite@6.0.0(@types/node@20.14.13)(jiti@2.4.2)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0))': - dependencies: - '@babel/core': 7.26.0 - '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) - '@types/babel__core': 7.20.5 - react-refresh: 0.14.2 - vite: 6.0.0(@types/node@20.14.13)(jiti@2.4.2)(lightningcss@1.29.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.11 - 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 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 - loupe: 3.1.1 - tinyrainbow: 1.2.0 + 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) - acorn-jsx@5.3.2(acorn@8.14.0): + '@vitest/pretty-format@4.1.10': dependencies: - acorn: 8.14.0 + tinyrainbow: 3.1.0 - acorn@8.14.0: {} + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 - ajv@6.12.6: + '@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 + + '@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: dependencies: fast-deep-equal: 3.1.3 - 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: {} + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 any-promise@1.3.0: {} anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 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: {} - ast-types-flow@0.0.8: {} - - autoprefixer@10.4.21(postcss@8.4.47): + autoprefixer@10.5.0(postcss@8.5.25): dependencies: - browserslist: 4.24.4 - caniuse-lite: 1.0.30001705 - fraction.js: 4.3.7 - normalize-range: 0.1.2 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.4.47 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.0.0 + baseline-browser-mapping@2.10.31: {} - 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.24.2: + browserslist@4.28.2: dependencies: - caniuse-lite: 1.0.30001705 - electron-to-chromium: 1.5.45 - node-releases: 2.0.18 - update-browserslist-db: 1.1.1(browserslist@4.24.2) + 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) - browserslist@4.24.4: + buffer-from@1.1.2: {} + + bun-types@1.3.14: dependencies: - 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) + '@types/node': 25.9.1 - buffer-from@1.1.2: - optional: true - - bun-types@1.2.7: - dependencies: - '@types/node': 20.14.13 - '@types/ws': 8.5.12 - - bun@1.2.8: + bun@1.3.14: optionalDependencies: - '@oven/bun-darwin-aarch64': 1.2.8 - '@oven/bun-darwin-x64': 1.2.8 - '@oven/bun-darwin-x64-baseline': 1.2.8 - '@oven/bun-linux-aarch64': 1.2.8 - '@oven/bun-linux-aarch64-musl': 1.2.8 - '@oven/bun-linux-x64': 1.2.8 - '@oven/bun-linux-x64-baseline': 1.2.8 - '@oven/bun-linux-x64-musl': 1.2.8 - '@oven/bun-linux-x64-musl-baseline': 1.2.8 - '@oven/bun-windows-x64': 1.2.8 - '@oven/bun-windows-x64-baseline': 1.2.8 + '@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 - bundle-require@5.1.0(esbuild@0.25.0): + bundle-require@5.1.0(esbuild@0.27.7): dependencies: - esbuild: 0.25.0 + esbuild: 0.27.7 load-tsconfig: 0.2.5 - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - 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.30001705: {} + caniuse-lite@1.0.30001793: {} - 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 + chai@6.2.2: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - check-error@2.1.1: {} + chardet@2.1.1: {} chokidar@3.6.0: dependencies: @@ -5353,665 +4885,138 @@ snapshots: chokidar@4.0.3: dependencies: - readdirp: 4.1.1 + readdirp: 4.1.2 - citty@0.1.6: - dependencies: - consola: 3.2.3 + chrome-trace-event@1.0.4: {} + + citty@0.2.2: {} + + cli-width@4.1.0: {} client-only@0.0.1: {} - clipboardy@4.0.0: + clipanion@4.0.0-rc.4(typanion@3.14.0): dependencies: - execa: 8.0.1 - is-wsl: 3.1.0 - is64bit: 2.0.0 + typanion: 3.14.0 - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 + colorette@2.0.20: {} - 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 - - commander@2.20.3: - optional: true + commander@2.20.3: {} commander@4.1.1: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} - consola@3.2.3: {} + consola@3.4.2: {} - consola@3.4.0: {} + content-type@2.0.0: {} convert-source-map@2.0.0: {} - cookie-es@1.2.2: {} + cookie-es@1.2.3: {} - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - crossws@0.3.1: + crossws@0.3.5: dependencies: uncrypto: 0.1.3 - crossws@0.3.4: - dependencies: - uncrypto: 0.1.3 + crossws@0.4.10: {} cssesc@3.0.0: {} - csstype@3.1.3: {} + csstype@3.2.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: + debug@4.4.3: dependencies: ms: 2.1.3 - debug@4.3.6: - dependencies: - ms: 2.1.2 + dedent@1.7.2: {} - debug@4.4.0: - dependencies: - ms: 2.1.3 + defu@6.1.7: {} - dedent@1.5.3: {} - - 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.3: {} + destr@2.0.5: {} detect-libc@1.0.3: {} - detect-libc@2.0.3: {} + detect-libc@2.1.2: {} didyoumean@1.2.2: {} dlv@1.1.3: {} - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 + electron-to-chromium@1.5.361: {} - eastasianwidth@0.2.0: {} + emnapi@1.11.3(node-addon-api@8.7.0): + optionalDependencies: + node-addon-api: 8.7.0 - electron-to-chromium@1.5.119: {} - - electron-to-chromium@1.5.45: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - enhanced-resolve@5.18.1: + enhanced-resolve@5.24.5: dependencies: graceful-fs: 4.2.11 - 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 + tapable: 2.3.3 es-errors@1.3.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-module-lexer@2.1.0: {} - es-object-atoms@1.0.0: - dependencies: - es-errors: 1.3.0 + es-toolkit@1.50.0: {} - 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 - - esbuild@0.21.5: + esbuild@0.27.7: optionalDependencies: - '@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.24.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.24.0 - '@esbuild/android-arm': 0.24.0 - '@esbuild/android-arm64': 0.24.0 - '@esbuild/android-x64': 0.24.0 - '@esbuild/darwin-arm64': 0.24.0 - '@esbuild/darwin-x64': 0.24.0 - '@esbuild/freebsd-arm64': 0.24.0 - '@esbuild/freebsd-x64': 0.24.0 - '@esbuild/linux-arm': 0.24.0 - '@esbuild/linux-arm64': 0.24.0 - '@esbuild/linux-ia32': 0.24.0 - '@esbuild/linux-loong64': 0.24.0 - '@esbuild/linux-mips64el': 0.24.0 - '@esbuild/linux-ppc64': 0.24.0 - '@esbuild/linux-riscv64': 0.24.0 - '@esbuild/linux-s390x': 0.24.0 - '@esbuild/linux-x64': 0.24.0 - '@esbuild/netbsd-x64': 0.24.0 - '@esbuild/openbsd-arm64': 0.24.0 - '@esbuild/openbsd-x64': 0.24.0 - '@esbuild/sunos-x64': 0.24.0 - '@esbuild/win32-arm64': 0.24.0 - '@esbuild/win32-ia32': 0.24.0 - '@esbuild/win32-x64': 0.24.0 - - 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 + '@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 escalade@3.2.0: {} - escape-string-regexp@4.0.0: {} - - eslint-config-next@15.2.4(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4): - dependencies: - '@next/eslint-plugin-next': 15.2.4 - '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4) - '@typescript-eslint/parser': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4) - eslint: 9.22.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-jsx-a11y: 6.10.1(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-react: 7.37.2(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-react-hooks: 5.0.0(eslint@9.22.0(jiti@2.4.2)) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - - eslint-config-next@15.2.4(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3): - dependencies: - '@next/eslint-plugin-next': 15.2.4 - '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) - '@typescript-eslint/parser': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) - eslint: 9.22.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-jsx-a11y: 6.10.1(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-react: 7.37.2(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-react-hooks: 5.0.0(eslint@9.22.0(jiti@2.4.2)) - 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.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 - enhanced-resolve: 5.18.1 - eslint: 9.22.0(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.22.0(jiti@2.4.2)) - fast-glob: 3.3.3 - get-tsconfig: 4.8.1 - 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.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.22.0(jiti@2.4.2)) - 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.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 - enhanced-resolve: 5.18.1 - eslint: 9.22.0(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.22.0(jiti@2.4.2)) - fast-glob: 3.3.3 - get-tsconfig: 4.8.1 - 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.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.22.0(jiti@2.4.2)) - 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.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.22.0(jiti@2.4.2)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4) - eslint: 9.22.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.22.0(jiti@2.4.2)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) - eslint: 9.22.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.22.0(jiti@2.4.2)): - 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.22.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.22.0(jiti@2.4.2)) - 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.22.0(jiti@2.4.2))(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.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.22.0(jiti@2.4.2)): - 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.22.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.22.0(jiti@2.4.2)) - 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.22.0(jiti@2.4.2))(typescript@5.6.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-jsx-a11y@6.10.1(eslint@9.22.0(jiti@2.4.2)): - 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.22.0(jiti@2.4.2) - 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.22.0(jiti@2.4.2)): - dependencies: - eslint: 9.22.0(jiti@2.4.2) - - eslint-plugin-react@7.37.2(eslint@9.22.0(jiti@2.4.2)): - 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.22.0(jiti@2.4.2) - 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.3.0: + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.0: {} - - eslint@9.22.0(jiti@2.4.2): - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.22.0(jiti@2.4.2)) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.19.2 - '@eslint/config-helpers': 0.1.0 - '@eslint/core': 0.12.0 - '@eslint/eslintrc': 3.3.0 - '@eslint/js': 9.22.0 - '@eslint/plugin-kit': 0.2.7 - '@humanfs/node': 0.16.6 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.6 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.0 - escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.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.4.2 - transitivePeerDependencies: - - supports-color - - espree@10.3.0: - dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) - eslint-visitor-keys: 4.2.0 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 + estraverse: 4.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.6 + '@types/estree': 1.0.9 - esutils@2.0.3: {} + events@3.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 + expect-type@1.3.0: {} + + fast-content-type-parse@3.0.0: {} fast-deep-equal@3.1.3: {} - 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-equals@5.4.0: {} fast-glob@3.3.3: dependencies: @@ -6021,50 +5026,45 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} + fast-string-truncated-width@3.0.3: {} - fast-levenshtein@2.0.6: {} - - fastq@1.17.1: + fast-string-width@3.0.2: dependencies: - reusify: 1.0.4 + fast-string-truncated-width: 3.0.3 - fdir@6.4.3(picomatch@4.0.2): + 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): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.4 - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - find-up-simple@1.0.0: {} + find-up-simple@1.0.1: {} - find-up@5.0.0: + fix-dts-default-cjs-exports@1.0.1: dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.60.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: {} + fraction.js@5.3.4: {} fsevents@2.3.2: optional: true @@ -6074,40 +5074,7 @@ snapshots: function-bind@1.1.2: {} - 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.8.1: - dependencies: - resolve-pkg-maps: 1.0.0 + get-port-please@3.2.0: {} glob-parent@5.1.2: dependencies: @@ -6117,652 +5084,335 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.4.5: + globby@16.2.2: 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@11.12.0: {} - - globals@14.0.0: {} - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.0.1 - - globby@14.1.0: - dependencies: - '@sindresorhus/merge-streams': 2.3.0 + '@sindresorhus/merge-streams': 4.0.0 fast-glob: 3.3.3 - ignore: 7.0.3 - path-type: 6.0.0 + ignore: 7.0.5 + is-path-inside: 4.0.0 slash: 5.1.0 - unicorn-magic: 0.3.0 - - gopd@1.0.1: - dependencies: - get-intrinsic: 1.2.4 + unicorn-magic: 0.4.0 graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - - h3@1.15.1: + h3@1.15.11: dependencies: - cookie-es: 1.2.2 - crossws: 0.3.4 - defu: 6.1.4 - destr: 2.0.3 + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 iron-webcrypto: 1.2.1 - node-mock-http: 1.0.0 + node-mock-http: 1.0.4 radix3: 1.1.2 - ufo: 1.5.4 + ufo: 1.6.4 uncrypto: 0.1.3 - has-bigints@1.0.2: {} - has-flag@4.0.0: {} - 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: + hasown@2.0.3: dependencies: function-bind: 1.1.2 http-shutdown@1.2.2: {} - human-signals@5.0.0: {} - - ignore@5.3.2: {} - - ignore@7.0.3: {} - - import-fresh@3.3.0: + iconv-lite@0.7.2: dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 + safer-buffer: 2.1.2 - imurmurhash@0.1.4: {} - - internal-slot@1.0.7: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.0.6 + ignore@7.0.5: {} 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-boolean-object@1.1.2: + is-core-module@2.16.2: dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - - is-bun-module@1.2.1: - dependencies: - semver: 7.6.3 - - is-callable@1.2.7: {} - - is-core-module@2.15.0: - dependencies: - hasown: 2.0.2 - - 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: {} + hasown: 2.0.3 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-regex@1.1.4: + is-path-inside@4.0.0: {} + + jest-worker@27.5.1: dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 + '@types/node': 25.9.1 + merge-stream: 2.0.0 + supports-color: 8.1.1 - is-set@2.0.3: {} + jiti@1.21.7: {} - 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.7.0: {} joycon@3.1.1: {} - js-tokens@4.0.0: {} - - js-yaml@4.1.0: + js-yaml@4.3.1: dependencies: argparse: 2.0.1 - jsesc@3.1.0: {} + json-schema-traverse@1.0.0: {} - json-buffer@3.0.1: {} + json-with-bigint@3.5.8: {} - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@1.0.2: - dependencies: - minimist: 1.2.8 - - 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-darwin-arm64@1.29.2: {} - - lightningcss-darwin-x64@1.29.2: {} - - lightningcss-freebsd-x64@1.29.2: + lightningcss-android-arm64@1.33.0: optional: true - lightningcss-linux-arm-gnueabihf@1.29.2: + lightningcss-darwin-arm64@1.33.0: {} + + lightningcss-darwin-x64@1.33.0: {} + + lightningcss-freebsd-x64@1.33.0: optional: true - lightningcss-linux-arm64-gnu@1.29.2: {} - - lightningcss-linux-arm64-musl@1.29.2: {} - - lightningcss-linux-x64-gnu@1.29.2: {} - - lightningcss-linux-x64-musl@1.29.2: {} - - lightningcss-win32-arm64-msvc@1.29.2: + lightningcss-linux-arm-gnueabihf@1.33.0: optional: true - lightningcss-win32-x64-msvc@1.29.2: {} + lightningcss-linux-arm64-gnu@1.33.0: {} - lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm): + 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): dependencies: - detect-libc: 2.0.3 + detect-libc: 2.1.2 optionalDependencies: - lightningcss-darwin-arm64: 1.29.2 - lightningcss-darwin-x64: 1.29.2 - lightningcss-freebsd-x64: 1.29.2 - lightningcss-linux-arm-gnueabihf: 1.29.2 - lightningcss-linux-arm64-gnu: 1.29.2 - lightningcss-linux-arm64-musl: 1.29.2 - lightningcss-linux-x64-gnu: 1.29.2 - lightningcss-linux-x64-musl: 1.29.2 - lightningcss-win32-arm64-msvc: 1.29.2 - lightningcss-win32-x64-msvc: 1.29.2 + 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 - lilconfig@2.1.0: {} - - lilconfig@3.1.2: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} - listhen@1.9.0: + listhen@1.10.1(@parcel/watcher@2.6.0(patch_hash=705ce75ccea54337110c4d7fe0a8b421658ea0c23e1a77c1fa890a86041179da)): dependencies: - '@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.1 + '@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 http-shutdown: 1.2.2 - 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 + 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 load-tsconfig@0.2.5: {} - locate-path@6.0.0: + magic-string@0.30.21: dependencies: - p-locate: 5.0.0 + '@jridgewell/sourcemap-codec': 1.5.5 - lodash.castarray@4.4.0: {} - - lodash.isplainobject@4.0.6: {} - - lodash.merge@4.6.2: {} - - lodash.sortby@4.7.0: {} - - loose-envify@1.4.0: + magic-string@1.1.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.11: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 merge-stream@2.0.0: {} merge2@1.4.1: {} - micro-memoize@4.1.3: {} - - micromatch@4.0.7: + micro-memoize@5.1.1: dependencies: - braces: 3.0.3 - picomatch: 2.3.1 + fast-equals: 5.4.0 + fast-stringify: 4.0.0 micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 - mimic-fn@4.0.0: {} + mime-db@1.54.0: {} mini-svg-data-uri@1.4.4: {} - minimatch@3.1.2: + 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)): dependencies: - brace-expansion: 1.1.11 + '@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 - minimatch@9.0.5: + mlly@1.8.2: dependencies: - 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.5.4 + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 mri@1.2.0: {} - ms@2.1.2: {} - ms@2.1.3: {} + mute-stream@3.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.7: {} + nanoid@3.3.12: {} - nanoid@3.3.8: {} + nanoid@3.3.16: {} - natural-compare@1.4.0: {} + neo-async@2.6.2: {} - next@15.2.4(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@16.2.7(@playwright/test@1.62.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@next/env': 15.2.4 - '@swc/counter': 0.1.3 + '@next/env': 16.2.7 '@swc/helpers': 0.5.15 - busboy: 1.6.0 - caniuse-lite: 1.0.30001705 + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 postcss: 8.4.31 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - styled-jsx: 5.1.6(react@19.0.0) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + styled-jsx: 5.1.6(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 15.2.4 - '@next/swc-darwin-x64': 15.2.4 - '@next/swc-linux-arm64-gnu': 15.2.4 - '@next/swc-linux-arm64-musl': 15.2.4 - '@next/swc-linux-x64-gnu': 15.2.4 - '@next/swc-linux-x64-musl': 15.2.4 - '@next/swc-win32-arm64-msvc': 15.2.4 - '@next/swc-win32-x64-msvc': 15.2.4 - '@playwright/test': 1.51.1 - sharp: 0.33.5 + '@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 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros node-addon-api@7.1.1: {} - node-addon-api@8.2.2: {} + node-addon-api@8.7.0: {} - node-addon-api@8.3.0: {} - - node-forge@1.3.1: {} - - node-gyp-build@4.8.2: {} + node-forge@1.4.0: {} node-gyp-build@4.8.4: {} - node-mock-http@1.0.0: {} + node-mock-http@1.0.4: {} - node-releases@2.0.18: {} - - node-releases@2.0.19: {} + node-releases@2.0.46: {} 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: {} - object-inspect@1.13.2: {} + obug@2.1.1: {} - 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: {} + obug@2.1.4: {} package-up@5.0.0: dependencies: - 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: {} + find-up-simple: 1.0.1 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: {} - - pathval@2.0.0: {} + pathe@2.0.3: {} picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.2: {} + picomatch@4.0.4: {} + + picomatch@4.0.5: {} pify@2.3.0: {} - pirates@4.0.6: {} + pirates@4.0.7: {} - pkg-types@1.3.0: + pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.7.3 - pathe: 1.1.2 + mlly: 1.8.2 + pathe: 2.0.3 - playwright-core@1.51.1: {} + playwright-core@1.62.1: {} - playwright@1.51.1: + playwright@1.62.1: dependencies: - playwright-core: 1.51.1 + playwright-core: 1.62.1 optionalDependencies: fsevents: 2.3.2 - possible-typed-array-names@1.0.0: {} - - postcss-import@15.1.0(postcss@8.4.47): + postcss-import@15.1.0(postcss@8.5.16): dependencies: - postcss: 8.4.47 + postcss: 8.5.16 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.8 + resolve: 1.22.12 - postcss-import@16.1.0(postcss@8.4.41): + postcss-import@16.1.1(postcss@8.5.25): dependencies: - postcss: 8.4.41 + postcss: 8.5.25 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.8 + resolve: 1.22.12 - postcss-import@16.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.8 - - postcss-js@4.0.1(postcss@8.4.47): + postcss-js@4.1.0(postcss@8.5.16): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.47 + postcss: 8.5.16 - postcss-load-config@4.0.2(postcss@8.4.47): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.16)(yaml@2.9.0): dependencies: - lilconfig: 3.1.2 - yaml: 2.6.0 + lilconfig: 3.1.3 optionalDependencies: - postcss: 8.4.47 + jiti: 1.21.7 + postcss: 8.5.16 + yaml: 2.9.0 - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.1)(yaml@2.6.0): + postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.25)(yaml@2.9.0): dependencies: - lilconfig: 3.1.2 + lilconfig: 3.1.3 optionalDependencies: - jiti: 2.4.2 - postcss: 8.5.3 - tsx: 4.19.1 - yaml: 2.6.0 + jiti: 2.7.0 + postcss: 8.5.25 + yaml: 2.9.0 - postcss-nested@6.2.0(postcss@8.4.47): + postcss-nested@6.2.0(postcss@8.5.16): dependencies: - postcss: 8.4.47 + postcss: 8.5.16 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.0.10: @@ -6775,7 +5425,7 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.0: + postcss-selector-parser@7.1.4: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -6784,70 +5434,57 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.4.41: + postcss@8.5.16: dependencies: - nanoid: 3.3.7 - picocolors: 1.1.1 - source-map-js: 1.2.0 - - postcss@8.4.47: - dependencies: - nanoid: 3.3.7 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.3: + postcss@8.5.25: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 - prelude-ls@1.2.1: {} - - prettier-plugin-embed@0.5.0: + prettier-plugin-embed@0.5.1: dependencies: - '@types/estree': 1.0.6 - dedent: 1.5.3 - micro-memoize: 4.1.3 + '@types/estree': 1.0.9 + dedent: 1.7.2 + micro-memoize: 5.1.1 package-up: 5.0.0 tiny-jsonc: 1.0.2 - type-fest: 4.36.0 + type-fest: 5.6.0 transitivePeerDependencies: - babel-plugin-macros - prettier-plugin-organize-imports@4.0.0(prettier@3.5.0)(typescript@5.5.4): + prettier-plugin-organize-imports@4.3.0(prettier@3.9.6)(typescript@5.9.3): dependencies: - prettier: 3.5.0 - typescript: 5.5.4 + prettier: 3.9.6 + typescript: 5.9.3 - prettier@3.5.0: {} - - 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: {} + prettier@3.9.6: {} queue-microtask@1.2.3: {} radix3@1.1.2: {} - react-dom@19.0.0(react@19.0.0): + react-dom@19.2.6(react@19.2.6): dependencies: - react: 19.0.0 - scheduler: 0.25.0 + react: 19.2.6 + scheduler: 0.27.0 - react-is@16.13.1: {} + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 - react-refresh@0.14.2: {} + react@19.2.6: {} - react@19.0.0: {} + react@19.2.7: {} read-cache@1.0.0: dependencies: @@ -6855,280 +5492,171 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 2.3.1 + picomatch: 2.3.2 - readdirp@4.1.1: {} + readdirp@4.1.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: {} + require-from-string@2.0.2: {} resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - - resolve@1.22.8: + resolve@1.22.12: dependencies: - is-core-module: 2.15.0 + es-errors: 1.3.0 + is-core-module: 2.16.2 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.5: - dependencies: - is-core-module: 2.15.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} - reusify@1.0.4: {} - - rollup@4.34.9: + rolldown@1.2.1: dependencies: - '@types/estree': 1.0.6 + '@oxc-project/types': 0.142.0 + '@rolldown/pluginutils': 1.0.1 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 + '@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 + + rollup@4.60.4: + 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 fsevents: 2.3.3 run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - safe-array-concat@1.1.2: + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + schema-utils@4.3.3: dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 - isarray: 2.0.5 + '@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) - safe-regex-test@1.0.3: + semver@7.8.5: {} + + sharp@0.34.5: dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-regex: 1.1.4 - - scheduler@0.25.0: {} - - semver@6.3.1: {} - - semver@7.6.3: {} - - set-function-length@1.2.2: - dependencies: - 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.33.5: - dependencies: - color: 4.2.3 - detect-libc: 2.0.3 - semver: 7.6.3 + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 + '@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 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.0: {} - source-map-js@1.2.1: {} source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - optional: true - source-map@0.6.1: - optional: true + source-map@0.6.1: {} - source-map@0.8.0-beta.0: - dependencies: - whatwg-url: 7.1.0 + source-map@0.7.6: {} stackback@0.0.2: {} - std-env@3.7.0: {} + std-env@4.1.0: {} - streamsearch@1.1.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 - - 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.0.0): + styled-jsx@5.1.6(react@19.2.7): dependencies: client-only: 0.0.1 - react: 19.0.0 + react: 19.2.7 - sucrase@3.35.0: + sucrase@3.35.1: dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 - glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 - pirates: 4.0.6 + pirates: 4.0.7 + tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 - supports-color@7.2.0: + supports-color@8.1.1: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} - system-architecture@0.1.0: {} + tagged-tag@1.0.0: {} - tailwindcss@3.4.14: + tailwindcss@3.4.19(yaml@2.9.0): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -7138,32 +5666,32 @@ snapshots: fast-glob: 3.3.3 glob-parent: 6.0.2 is-glob: 4.0.3 - jiti: 1.21.6 - lilconfig: 2.1.0 - micromatch: 4.0.7 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - 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: 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-selector-parser: 6.1.2 - resolve: 1.22.8 - sucrase: 3.35.0 + resolve: 1.22.12 + sucrase: 3.35.1 transitivePeerDependencies: - - ts-node + - tsx + - yaml - tapable@2.2.1: {} + tapable@2.3.3: {} - terser@5.31.6: + terser@5.48.0: dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.14.0 + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 - optional: true thenify-all@1.6.0: dependencies: @@ -7177,360 +5705,227 @@ snapshots: tinybench@2.9.0: {} + tinyclip@0.1.15: {} + tinyexec@0.3.2: {} - tinyglobby@0.2.12: + tinyexec@1.1.2: {} + + tinyglobby@0.2.16: dependencies: - fdir: 6.4.3(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - tinypool@1.0.0: {} + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 - tinyrainbow@1.2.0: {} - - tinyspy@3.0.0: {} + tinyrainbow@3.1.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.22.4): + tree-sitter-javascript@0.23.1(tree-sitter@0.25.1): dependencies: - node-addon-api: 8.2.2 - node-gyp-build: 4.8.2 - optionalDependencies: - tree-sitter: 0.22.4 - - tree-sitter-typescript@0.23.2(tree-sitter@0.22.4): - dependencies: - 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.22.4 - - tree-sitter@0.22.4: - dependencies: - node-addon-api: 8.3.0 + node-addon-api: 8.7.0 node-gyp-build: 4.8.4 + optionalDependencies: + tree-sitter: 0.25.1 - ts-api-utils@1.3.0(typescript@5.5.4): + tree-sitter-typescript@0.23.2(tree-sitter@0.25.1): dependencies: - typescript: 5.5.4 + node-addon-api: 8.7.0 + node-gyp-build: 4.8.4 + tree-sitter-javascript: 0.23.1(tree-sitter@0.25.1) + optionalDependencies: + tree-sitter: 0.25.1 - ts-api-utils@1.3.0(typescript@5.6.3): + tree-sitter@0.25.1: dependencies: - typescript: 5.6.3 + node-addon-api: 8.7.0 + node-gyp-build: 4.8.4 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: {} - tslib@2.8.0: {} - - tsup@8.4.0(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.1)(typescript@5.5.4)(yaml@2.6.0): + tsup@8.5.1(jiti@2.7.0)(postcss@8.5.25)(typescript@5.9.3)(yaml@2.9.0): dependencies: - bundle-require: 5.1.0(esbuild@0.25.0) + bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 chokidar: 4.0.3 - consola: 3.4.0 - debug: 4.4.0 - esbuild: 0.25.0 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.1)(yaml@2.6.0) + postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.25)(yaml@2.9.0) resolve-from: 5.0.0 - rollup: 4.34.9 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 + rollup: 4.60.4 + source-map: 0.7.6 + sucrase: 3.35.1 tinyexec: 0.3.2 - tinyglobby: 0.2.12 + tinyglobby: 0.2.16 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.3 - typescript: 5.5.4 + postcss: 8.5.25 + typescript: 5.9.3 transitivePeerDependencies: - jiti - supports-color - tsx - yaml - tsx@4.19.1: - dependencies: - esbuild: 0.23.1 - get-tsconfig: 4.8.1 + turbo@2.10.8: optionalDependencies: - fsevents: 2.3.3 - optional: true + '@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 - turbo-darwin-64@2.4.4: - optional: true + typanion@3.14.0: {} - turbo-darwin-arm64@2.4.4: - optional: true - - turbo-linux-64@2.4.4: - optional: true - - turbo-linux-arm64@2.4.4: - optional: true - - turbo-windows-64@2.4.4: - optional: true - - turbo-windows-arm64@2.4.4: - optional: true - - turbo@2.4.4: - optionalDependencies: - turbo-darwin-64: 2.4.4 - turbo-darwin-arm64: 2.4.4 - turbo-linux-64: 2.4.4 - turbo-linux-arm64: 2.4.4 - turbo-windows-64: 2.4.4 - turbo-windows-arm64: 2.4.4 - - type-check@0.4.0: + type-fest@5.6.0: dependencies: - prelude-ls: 1.2.1 + tagged-tag: 1.0.0 - type-fest@4.36.0: {} + typescript@5.9.3: {} - typed-array-buffer@1.0.2: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-typed-array: 1.1.13 + typescript@6.0.3: {} - 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: {} - - 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 + ufo@1.6.4: {} uncrypto@0.1.3: {} - undici-types@5.26.5: {} + undici-types@6.21.0: {} - unicorn-magic@0.3.0: {} + undici-types@7.24.6: {} - untun@0.1.3: + unicorn-magic@0.4.0: {} + + universal-user-agent@7.0.3: {} + + untun@0.2.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - citty: 0.1.6 - consola: 3.2.3 - pathe: 1.1.2 - - update-browserslist-db@1.1.1(browserslist@4.24.2): - dependencies: - browserslist: 4.24.2 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 - 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.2: {} - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 + uqr@0.1.3: {} util-deprecate@1.0.2: {} - vite-node@2.0.5(@types/node@20.14.13)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6): + 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): dependencies: - cac: 6.7.14 - debug: 4.4.0 - pathe: 1.1.2 - tinyrainbow: 1.2.0 - vite: 5.4.0(@types/node@20.14.13)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite@5.4.0(@types/node@20.14.13)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.3 - rollup: 4.34.9 + 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': 20.14.13 + '@types/node': 22.20.1 + esbuild: 0.27.7 fsevents: 2.3.3 - lightningcss: 1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) - terser: 5.31.6 + jiti: 2.7.0 + terser: 5.48.0 + yaml: 2.9.0 - vite@6.0.0(@types/node@20.14.13)(jiti@2.4.2)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0): + 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): dependencies: - esbuild: 0.24.0 - postcss: 8.5.3 - rollup: 4.34.9 + 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': 20.14.13 + '@types/node': 25.9.1 + esbuild: 0.27.7 fsevents: 2.3.3 - jiti: 2.4.2 - lightningcss: 1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) - terser: 5.31.6 - tsx: 4.19.1 - yaml: 2.6.0 + jiti: 2.7.0 + terser: 5.48.0 + yaml: 2.9.0 - vitest@2.0.5(@types/node@20.14.13)(lightningcss@1.29.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)): 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.11 - pathe: 1.1.2 - std-env: 3.7.0 + '@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 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinypool: 1.0.0 - tinyrainbow: 1.2.0 - vite: 5.4.0(@types/node@20.14.13)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6) - vite-node: 2.0.5(@types/node@20.14.13)(lightningcss@1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm))(terser@5.31.6) + 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) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.14.13 + '@types/node': 22.20.1 transitivePeerDependencies: - - less + - 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 - lightningcss - - 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 + - postcss + - uglify-js why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - word-wrap@1.2.5: {} - - 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: {} + yaml@2.9.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 98329b69e..bcddef126 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,15 +5,50 @@ 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': ^20.14.8 - prettier: 3.5.0 - vite: ^6.0.0 - lightningcss: 1.29.2 - lightningcss-darwin-arm64: 1.29.2 - lightningcss-darwin-x64: 1.29.2 - lightningcss-linux-arm64-gnu: 1.29.2 - lightningcss-linux-arm64-musl: 1.29.2 - lightningcss-linux-x64-gnu: 1.29.2 - lightningcss-linux-x64-musl: 1.29.2 - lightningcss-win32-x64-msvc: 1.29.2 + '@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 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index aaba1588e..f92020c65 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.85.0" +channel = "1.95.0" profile = "default" diff --git a/scripts/pack-packages.mjs b/scripts/pack-packages.mjs index 3bcf23bec..2b0b9f15d 100644 --- a/scripts/pack-packages.mjs +++ b/scripts/pack-packages.mjs @@ -21,30 +21,25 @@ 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) }) -} - -// Move napi artifacts into sub packages -const tailwindcssOxideRoot = path.join(root, 'crates', 'node') -for (let file of await fs.readdir(tailwindcssOxideRoot)) { - if (file.startsWith('tailwindcss-oxide.') && file.endsWith('.node')) { - let target = file.split('.')[1] - await fs.cp( - path.join(tailwindcssOxideRoot, file), - path.join(tailwindcssOxideRoot, 'npm', target, file), - ) - } + workspaces.set(pkg.name, { + version: pkg.version ?? '', + dir: dirname(path), + hasBundledDependencies: pkg.bundledDependencies?.length > 0 ?? false, + }) } // Clean dist folder await fs.rm(path.join(root, 'dist'), { recursive: true, force: true }) Promise.all( - [...workspaces.entries()].map(async ([name, { version, dir }]) => { + [...workspaces.entries()].map(async ([name, { dir, hasBundledDependencies }]) => { 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, '\\\\')}"`, + `pnpm pack --pack-gzip-level=0 --pack-destination="${path.join(root, 'dist').replace(/\\/g, '\\\\')}"${hasBundledDependencies ? ' --config.node-linker=hoisted' : ''}`, { cwd: dir }, (err, stdout, stderr) => { if (err) { diff --git a/scripts/version-packages.mjs b/scripts/version-packages.mjs index 36f412cc3..1641d5404 100644 --- a/scripts/version-packages.mjs +++ b/scripts/version-packages.mjs @@ -28,8 +28,9 @@ const syncedWorkspaces = new Map([ 'crates/node/npm/linux-arm64-musl', 'crates/node/npm/linux-x64-gnu', 'crates/node/npm/linux-x64-musl', - 'crates/node/npm/win32-x64-msvc', + 'crates/node/npm/wasm32-wasi', 'crates/node/npm/win32-arm64-msvc', + 'crates/node/npm/win32-x64-msvc', ], ], ['@tailwindcss/cli', ['packages/@tailwindcss-standalone']], diff --git a/turbo.json b/turbo.json index 87eba2732..c48410b29 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://turbo.build/schema.json", + "$schema": "https://v2-10-8.turborepo.dev/schema.json", "ui": "tui", "tasks": { "@tailwindcss/oxide#build": { diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 000000000..3ef999be2 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,8 @@ +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 deleted file mode 100644 index 4ccde0667..000000000 --- a/vitest.workspace.ts +++ /dev/null @@ -1 +0,0 @@ -export default ['packages/*']