diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..a19ade077 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 4bf4af1ed..000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @tailwindlabs/engineering diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 63f79fa10..75ca2dfb9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,48 +1,12 @@ # 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 before starting work on it. +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. **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. @@ -50,7 +14,7 @@ If you open a pull request for a new feature, we're likely to close it not becau ## Coding standards -Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindlabs/tailwindcss/blob/main/package.json). You can check your code against these standards by running: +Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindcss/tailwindcss/blob/next/package.json). You can check your code against these standards by running: ```sh pnpm run lint @@ -64,40 +28,10 @@ pnpm run format ## Running tests -You can run the TypeScript and Rust test suites using the following command: +You can run the test suite using the following commands: ```sh -pnpm test +pnpm build && pnpm test ``` -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 +Please ensure that the tests are passing when submitting a pull request. If you're adding new features to Tailwind, please include tests. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 848c851b1..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -custom: ['https://tailwindcss.com/sponsor'] diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 1b8a310c6..000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: Bug report -about: If you've already asked for help with a problem and confirmed something is broken with Tailwind CSS itself, create a bug report. -title: '' -labels: '' -assignees: '' ---- - - - -**What version of Tailwind CSS are you using?** - -For example: v4.0.6 - -**What build tool (or framework if it abstracts the build tool) are you using?** - -For example: postcss-cli 11.0.0, Next.js 15.1.7, Vite 6.1.0 - -**What version of Node.js are you using?** - -For example: v20.0.0 - -**What browser are you using?** - -For example: Chrome, Safari, or N/A - -**What operating system are you using?** - -For example: macOS, Windows - -**Reproduction URL** - -A Tailwind Play link or public GitHub repo that includes a minimal reproduction of the bug. **Please do not link to your actual project**, what we need instead is a _minimal_ reproduction in a fresh project without any unnecessary code. This means it doesn't matter if your real project is private/confidential, since we want a link to a separate, isolated reproduction anyways. - -A reproduction is **required** when filing an issue — any issue opened without a reproduction will be closed and you'll be asked to create a new issue that includes a reproduction. We're a small team and we can't keep up with the volume of issues we receive if we need to reproduce each issue from scratch ourselves. - -**Describe your issue** - -Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 10ee9a6b8..58166a010 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,6 +6,9 @@ contact_links: - name: Feature Request url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas about: 'Suggest any ideas you have using our discussion forums.' + - name: Bug Report + url: https://github.com/tailwindlabs/tailwindcss/issues/new?body=%3C%21--%20Please%20provide%20all%20of%20the%20information%20requested%20below.%20We%27re%20a%20small%20team%20and%20without%20all%20of%20this%20information%20it%27s%20not%20possible%20for%20us%20to%20help%20and%20your%20bug%20report%20will%20be%20closed.%20--%3E%0A%0A%2A%2AWhat%20version%20of%20Tailwind%20CSS%20are%20you%20using%3F%2A%2A%0A%0AFor%20example%3A%20v2.0.4%0A%0A%2A%2AWhat%20build%20tool%20%28or%20framework%20if%20it%20abstracts%20the%20build%20tool%29%20are%20you%20using%3F%2A%2A%0A%0AFor%20example%3A%20postcss-cli%208.3.1%2C%20Next.js%2010.0.9%2C%20webpack%205.28.0%0A%0A%2A%2AWhat%20version%20of%20Node.js%20are%20you%20using%3F%2A%2A%0A%0AFor%20example%3A%20v12.0.0%0A%0A%2A%2AWhat%20browser%20are%20you%20using%3F%2A%2A%0A%0AFor%20example%3A%20Chrome%2C%20Safari%2C%20or%20N%2FA%0A%0A%2A%2AWhat%20operating%20system%20are%20you%20using%3F%2A%2A%0A%0AFor%20example%3A%20macOS%2C%20Windows%0A%0A%2A%2AReproduction%20URL%2A%2A%0A%0AA%20Tailwind%20Play%20link%20or%20public%20GitHub%20repo%20that%20includes%20a%20minimal%20reproduction%20of%20the%20bug.%20%2A%2APlease%20do%20not%20link%20to%20your%20actual%20project%2A%2A%2C%20what%20we%20need%20instead%20is%20a%20_minimal_%20reproduction%20in%20a%20fresh%20project%20without%20any%20unnecessary%20code.%20This%20means%20it%20doesn%27t%20matter%20if%20your%20real%20project%20is%20private%2Fconfidential%2C%20since%20we%20want%20a%20link%20to%20a%20separate%2C%20isolated%20reproduction%20anyways.%0A%0AA%20reproduction%20is%20%2A%2Arequired%2A%2A%20when%20filing%20an%20issue%20%E2%80%94%20any%20issue%20opened%20without%20a%20reproduction%20will%20be%20closed%20and%20you%27ll%20be%20asked%20to%20create%20a%20new%20issue%20that%20includes%20a%20reproduction.%20We%27re%20a%20small%20team%20and%20we%20can%27t%20keep%20up%20with%20the%20volume%20of%20issues%20we%20receive%20if%20we%20need%20to%20reproduce%20each%20issue%20from%20scratch%20ourselves.%0A%0A%2A%2ADescribe%20your%20issue%2A%2A%0A%0ADescribe%20the%20problem%20you%27re%20seeing%2C%20any%20important%20steps%20to%20reproduce%20and%20what%20behavior%20you%20expect%20instead. + about: If you've already asked for help with a problem and confirmed something is broken with Tailwind CSS itself, create a bug report. - name: Documentation Issue url: https://github.com/tailwindlabs/tailwindcss.com about: 'For documentation issues, suggest changes on our documentation repository.' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index aa0793aa3..ce7fafb97 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,26 +4,8 @@ **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 a discussion to first discuss 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. -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 5ae59e80f..d28a1e1e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,68 +2,47 @@ name: CI on: push: - branches: [main] + branches: [next] pull_request: permissions: contents: read -env: - NODE_VERSION: 24 - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: tests: strategy: fail-fast: false matrix: - runner: - - name: Windows - os: windows-latest - - - name: Linux - os: namespace-profile-default - - # Playwright 1.62+ dropped WebKit support for macOS 14, and hangs - # instead of failing when launching WebKit on a macos-14 runner. - - name: macOS - os: macos-15 - + node-version: [20] + runner: [namespace-profile-default, windows-latest, macos-14] # 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.event.pull_request.user.login == 'depfu[bot]' }} + on-next-branch: + - ${{ github.ref == 'refs/heads/next' }} exclude: - - run-all: false - runner: - name: Windows - - run-all: false - runner: - name: macOS + - on-next-branch: false + runner: windows-latest + - on-next-branch: false + runner: macos-14 - runs-on: ${{ matrix.runner.os }} + runs-on: ${{ matrix.runner }} timeout-minutes: 30 - name: ${{ matrix.runner.name }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} + node-version: ${{ matrix.node-version }} + cache: 'pnpm' # Cargo already skips downloading dependencies if they already exist - name: Cache cargo - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + uses: actions/cache@v4 with: path: | + ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ @@ -72,25 +51,17 @@ jobs: # Cache the `oxide` Rust build - name: Cache oxide build - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + uses: actions/cache@v4 with: path: | + ./target/ ./crates/node/*.node - ./crates/node/*.wasm - ./crates/node/index.d.ts ./crates/node/index.js - ./crates/node/browser.js - ./crates/node/tailwindcss-oxide.wasi-browser.js - ./crates/node/tailwindcss-oxide.wasi.cjs - ./crates/node/wasi-worker-browser.mjs - ./crates/node/wasi-worker.mjs + ./crates/node/index.d.ts key: ${{ runner.os }}-oxide-${{ hashFiles('./crates/**/*') }} - - name: Setup WASM target - run: rustup target add wasm32-wasip1-threads - - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install - name: Build run: pnpm run build @@ -101,24 +72,25 @@ jobs: - name: Lint run: pnpm run lint # Only lint on linux to avoid \r\n line ending errors - if: matrix.runner.os == 'ubuntu-latest' + if: matrix.runner == 'ubuntu-latest' - name: Test run: pnpm run test + - name: Integration Tests + run: pnpm run test:integrations + env: + GITHUB_WORKSPACE: ${{ github.workspace }} + - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Run Playwright tests run: npm run test:ui - notify: - if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }} - needs: tests - runs-on: ubuntu-latest - steps: - name: Notify Discord - uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2 + if: failure() && github.ref == 'refs/heads/next' + uses: discord-actions/message@v2 with: webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }} - message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' + message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `next` branch has failed.' diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml deleted file mode 100644 index 457c38424..000000000 --- a/.github/workflows/integration-tests.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: Integration Tests - -on: - push: - branches: [main] - pull_request: - -permissions: - contents: read - -env: - NODE_VERSION: 24 - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - tests: - strategy: - fail-fast: false - matrix: - runner: - - name: Windows - os: windows-latest - - - name: Linux - os: namespace-profile-default - - - name: macOS - os: macos-14 - - integration: - - upgrade - - vite - - cli - - postcss - - oxide - - webpack - - # 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]') }} - exclude: - - run-all: false - runner: - name: Windows - - run-all: false - runner: - name: macOS - - runs-on: ${{ matrix.runner.os }} - timeout-minutes: 30 - - name: ${{ matrix.runner.name }} / ${{ matrix.integration }} - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - 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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 - with: - path: | - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - # Cache the `oxide` Rust build - - name: Cache oxide build - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 - with: - path: | - ./crates/node/*.node - ./crates/node/*.wasm - ./crates/node/index.d.ts - ./crates/node/index.js - ./crates/node/browser.js - ./crates/node/tailwindcss-oxide.wasi-browser.js - ./crates/node/tailwindcss-oxide.wasi.cjs - ./crates/node/wasi-worker-browser.mjs - ./crates/node/wasi-worker.mjs - key: ${{ runner.os }}-oxide-${{ hashFiles('./crates/**/*') }} - - - name: Setup WASM target - run: rustup target add wasm32-wasip1-threads - - - name: Install dependencies - run: pnpm install - - - name: Build - run: pnpm run build - env: - CARGO_PROFILE_RELEASE_LTO: 'off' - CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link' - - - name: Test ${{ matrix.integration }} - run: pnpm run test:integrations ./integrations/${{ matrix.integration }} - env: - GITHUB_WORKSPACE: ${{ github.workspace }} - - notify: - if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }} - needs: tests - runs-on: ubuntu-latest - steps: - - name: Notify Discord - uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2 - with: - webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }} - message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.' diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 24e829afd..0c4979b2b 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -2,306 +2,21 @@ name: Prepare Release on: workflow_dispatch: - inputs: - dry_run: - description: Skip creating the draft GitHub release - required: false - default: true - type: boolean - push: - tags: - - 'v*' env: - APP_NAME: tailwindcss-oxide - NODE_VERSION: 24 - PNPM_VERSION: '11.9.0' - OXIDE_LOCATION: ./crates/node + CI: true permissions: contents: read -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: build: + runs-on: macos-12 + timeout-minutes: 15 + 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 - build-flags: --use-napi-cross - - os: ubuntu-latest - target: aarch64-unknown-linux-gnu - strip: aarch64-linux-gnu-strip - build-flags: --use-napi-cross - - os: ubuntu-latest - target: armv7-unknown-linux-gnueabihf - strip: arm-linux-gnueabihf-strip - build-flags: --use-napi-cross - - os: ubuntu-latest - target: aarch64-unknown-linux-musl - strip-zig: true - build-flags: -x - - os: ubuntu-latest - target: x86_64-unknown-linux-musl - strip: strip - build-flags: -x - - name: Build ${{ matrix.target }} (oxide) - runs-on: ${{ matrix.os }} - timeout-minutes: 15 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: ${{ env.PNPM_VERSION }} - - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: ${{ env.NODE_VERSION }} - package-manager-cache: false - - - name: Install gcc-arm-linux-gnueabihf - if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }} - run: | - sudo apt-get update - sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y - - - 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 --frozen-lockfile --filter=!./playgrounds/* - - - name: Build release - 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 || 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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: bindings-${{ matrix.target }} - path: ${{ env.OXIDE_LOCATION }}/*.node - - build-freebsd: - name: Build x86_64-unknown-freebsd (OXIDE) - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - name: Build FreeBSD - uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # 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@${{ 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 --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:platform - strip -x ${{ env.OXIDE_LOCATION }}/*.node - ls -la ${{ env.OXIDE_LOCATION }} - - name: Upload artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: bindings-x86_64-unknown-freebsd - path: ${{ env.OXIDE_LOCATION }}/*.node - - prepare: - runs-on: macos-14 - timeout-minutes: 15 - name: Build and release Tailwind CSS - - permissions: - contents: write # Required for creating releases - - needs: - - build - - build-freebsd + node-version: [16] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 20 - persist-credentials: false - - - run: git fetch --tags -f - - - name: Resolve version - id: vars - run: | - echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: ${{ env.PNPM_VERSION }} - - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: ${{ env.NODE_VERSION }} - registry-url: 'https://registry.npmjs.org' - package-manager-cache: false - - - name: Setup WASM target - run: rustup target add wasm32-wasip1-threads - - - name: Install dependencies - run: pnpm --filter=!./playgrounds/* install --frozen-lockfile - - - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 - with: - path: ${{ env.OXIDE_LOCATION }} - - - name: Move artifacts - run: | - cd ${{ env.OXIDE_LOCATION }} - cp bindings-x86_64-pc-windows-msvc/* ./npm/win32-x64-msvc/ - cp bindings-aarch64-pc-windows-msvc/* ./npm/win32-arm64-msvc/ - cp bindings-x86_64-apple-darwin/* ./npm/darwin-x64/ - cp bindings-aarch64-apple-darwin/* ./npm/darwin-arm64/ - cp bindings-aarch64-linux-android/* ./npm/android-arm64/ - cp bindings-armv7-linux-androideabi/* ./npm/android-arm-eabi/ - cp bindings-aarch64-unknown-linux-gnu/* ./npm/linux-arm64-gnu/ - cp bindings-aarch64-unknown-linux-musl/* ./npm/linux-arm64-musl/ - cp bindings-armv7-unknown-linux-gnueabihf/* ./npm/linux-arm-gnueabihf/ - cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/ - cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/ - cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/ - - - name: Build Tailwind CSS - run: pnpm run build - env: - FEATURES_ENV: stable - - - 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: Get release notes - run: | - RELEASE_NOTES=$(node ./scripts/release-notes.mjs) - echo "RELEASE_NOTES<> $GITHUB_ENV - echo "$RELEASE_NOTES" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - - - name: Upload standalone artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: tailwindcss-standalone - path: packages/@tailwindcss-standalone/dist/ - - - name: Upload npm package tarballs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: npm-package-tarballs - path: dist/*.tgz - - - name: Prepare GitHub Release - if: ${{ !inputs.dry_run }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create "${TAG_NAME}" \ - --draft \ - --title "${TAG_NAME}" \ - --notes "${RELEASE_NOTES}" \ - packages/@tailwindcss-standalone/dist/sha256sums.txt \ - packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64 \ - packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl \ - packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64 \ - packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl \ - packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64 \ - packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64 \ - packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe + - run: echo "stub" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c957d3af..2fc468f67 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,34 +1,22 @@ 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 + release_channel: + description: 'Release channel' + required: false + default: 'next' + type: string permissions: contents: read env: APP_NAME: tailwindcss-oxide - NODE_VERSION: 24 - PNPM_VERSION: '11.9.0' + NODE_VERSION: 20 OXIDE_LOCATION: ./crates/node -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true - jobs: build: strategy: @@ -37,8 +25,6 @@ jobs: # 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 @@ -58,207 +44,163 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu strip: strip - build-flags: --use-napi-cross + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian - os: ubuntu-latest target: aarch64-unknown-linux-gnu - strip: aarch64-linux-gnu-strip - build-flags: --use-napi-cross + strip: llvm-strip + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - os: ubuntu-latest target: armv7-unknown-linux-gnueabihf - strip: arm-linux-gnueabihf-strip - build-flags: --use-napi-cross + strip: llvm-strip + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig - os: ubuntu-latest target: aarch64-unknown-linux-musl - strip-zig: true - build-flags: -x + strip: aarch64-linux-musl-strip + download: true + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine - os: ubuntu-latest target: x86_64-unknown-linux-musl strip: strip - build-flags: -x + download: true + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine - name: Build ${{ matrix.target }} (oxide) + name: Build ${{ matrix.target }} (OXIDE) runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} timeout-minutes: 15 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: ${{ env.PNPM_VERSION }} + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - package-manager-cache: false + cache: 'pnpm' - - name: Install gcc-arm-linux-gnueabihf - if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }} - run: | - 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') }} + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v4 with: - version: 0.14.1 - use-cache: false + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install cargo-zigbuild - uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2 - if: ${{ contains(matrix.target, 'musl') }} - env: - GITHUB_TOKEN: ${{ github.token }} + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v4 with: - tool: cargo-zigbuild + 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 --frozen-lockfile --filter=!./playgrounds/* + run: pnpm install --ignore-scripts --filter=!./playgrounds/* - name: Build release - run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }} + run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build env: RUST_TARGET: ${{ matrix.target }} JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }} - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 - if: ${{ matrix.strip || matrix.strip-zig }} - env: - STRIP_COMMAND: ${{ matrix.strip }} - STRIP_ZIG: ${{ matrix.strip-zig }} - run: | - if [ "$STRIP_ZIG" = "true" ]; then - for file in ${{ env.OXIDE_LOCATION }}/*.node; do - zig objcopy --strip-all "$file" "$file.stripped" - mv "$file.stripped" "$file" - done - exit 0 - fi - - eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node" + if: ${{ matrix.strip }} + run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node - name: Upload artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: ${{ env.OXIDE_LOCATION }}/*.node - build-freebsd: - name: Build x86_64-unknown-freebsd (OXIDE) - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - name: Build FreeBSD - uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # 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@${{ 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" - echo "~~~~ rustc --version ~~~~" - rustc --version - echo "~~~~ node -v ~~~~" - node -v - echo "~~~~ pnpm --version ~~~~" - pnpm --version - 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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: bindings-x86_64-unknown-freebsd - path: ${{ env.OXIDE_LOCATION }}/*.node - release: runs-on: macos-14 timeout-minutes: 15 - name: Build and publish Tailwind CSS + name: Build and release Tailwind CSS permissions: - contents: read + contents: write # for softprops/action-gh-release to create GitHub release # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions id-token: write needs: - build - - build-freebsd steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@v4 with: + fetch-tags: true fetch-depth: 20 - persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: ${{ env.PNPM_VERSION }} + - run: git fetch --tags -f + + - name: Resolve version + id: vars + run: | + echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV + + - uses: pnpm/action-setup@v4 - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' registry-url: 'https://registry.npmjs.org' - package-manager-cache: false - # npm trusted publishing validates the caller workflow filename, so all npm publishes live here. - # This workflow rebuilds the publish artifacts instead of depending on prepare-release.yml. - - name: Resolve release metadata - env: - INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }} - run: | - if [[ "${{ github.event_name }}" == "release" || "$INPUT_CHANNEL" == "release" ]]; then - release_channel=$(node ./scripts/release-channel.js) + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} - echo "RELEASE_KIND=release" >> $GITHUB_ENV - echo "RELEASE_CHANNEL=$release_channel" >> $GITHUB_ENV - echo "FEATURES_ENV=stable" >> $GITHUB_ENV - else - sha_short=$(git rev-parse --short HEAD) - - echo "RELEASE_KIND=insiders" >> $GITHUB_ENV - echo "RELEASE_CHANNEL=insiders" >> $GITHUB_ENV - echo "SHA_SHORT=$sha_short" >> $GITHUB_ENV - echo "INSIDERS_VERSION=0.0.0-insiders.$sha_short" >> $GITHUB_ENV - fi - - - name: Setup WASM target - run: rustup target add wasm32-wasip1-threads + # 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 --frozen-lockfile + run: pnpm --filter=!./playgrounds/* install - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@v4 with: path: ${{ env.OXIDE_LOCATION }} @@ -266,7 +208,6 @@ jobs: 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/ @@ -276,83 +217,45 @@ jobs: 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: ${{ env.INSIDERS_VERSION }}' - if: env.RELEASE_KIND == 'insiders' - run: pnpm run version-packages ${INSIDERS_VERSION} - name: Build Tailwind CSS - if: env.RELEASE_KIND == 'insiders' run: pnpm run build - - name: Build Tailwind CSS - if: env.RELEASE_KIND == 'release' - run: pnpm run build - env: - FEATURES_ENV: ${{ env.FEATURES_ENV }} - - name: Run pre-publish optimizations scripts run: node ./scripts/pre-publish-optimizations.mjs - name: Lock pre-release versions run: node ./scripts/lock-pre-release-versions.mjs - - name: Upload npm package tarballs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + - name: Get release notes + run: | + RELEASE_NOTES=$(node ./scripts/release-notes.mjs) + echo "RELEASE_NOTES<> $GITHUB_ENV + echo "$RELEASE_NOTES" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Upload Standalone Artifacts + uses: actions/upload-artifact@v4 with: - name: npm-package-tarballs - path: dist/*.tgz + name: tailwindcss-standalone + path: packages/@tailwindcss-standalone/dist/ - name: Publish - 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 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }} - script: | - await github.rest.actions.createWorkflowDispatch({ - owner: 'tailwindlabs', - repo: 'upgrades', - ref: 'main', - workflow_id: 'upgrade-tailwindcss.yml' - }) - - 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 + run: pnpm --recursive publish --tag ${{ inputs.release_channel }} --no-git-checks 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 + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Notify Discord - uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2 + - name: Release + uses: softprops/action-gh-release@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.' + 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-x64 + 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/.gitignore b/.gitignore index bbac7d227..9d75bba27 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ playwright-report/ blob-report/ playwright/.cache/ target/ -.debug/ +.debug diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..ded82e2f6 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers = true diff --git a/.prettierignore b/.prettierignore index 4f50b932f..3de4530dd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,5 @@ pnpm-lock.yaml target/ crates/node/index.d.ts crates/node/index.js -crates/ignore/ .next .fingerprint diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e6f6f89..5af6d0fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,1255 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `@tailwindcss/turbopack` package to run Tailwind CSS with Next.js ([20367](https://github.com/tailwindlabs/tailwindcss/pull/20367)) - -### Fixed - -- Ensure watch mode detects changes to symlinked `@source` files whose real paths aren't otherwise scanned ([#20356](https://github.com/tailwindlabs/tailwindcss/pull/20356)) -- Ensure custom variants using `@scope` wrap the generated utilities instead of nesting inside them ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369)) -- Fix flattening of `@scope` at-rules ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369)) -- Fix standalone declarations in `@scope`, wrap them in `:where(:scope)` ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369)) -- Always emit a space for empty fallback values in CSS variables (e.g. `var(--tw-blur,)` → `var(--tw-blur, )`) ([#20373](https://github.com/tailwindlabs/tailwindcss/pull/20373)) -- Canonicalization: convert arbitrary breakpoint and container query variants to named equivalents (e.g. `max-[64rem]` → `max-lg`) ([#20380](https://github.com/tailwindlabs/tailwindcss/pull/20380)) -- Prevent `@tailwindcss/vite` from crashing on every edit under Vite's experimental `bundledDev` mode ([#20379](https://github.com/tailwindlabs/tailwindcss/pull/20379)) -- Ensure `@tailwindcss/oxide` falls back to WASM on platforms without native bindings ([#20383](https://github.com/tailwindlabs/tailwindcss/pull/20383)) -- Detect classes in Ruby percent literals using angle brackets or custom delimiters (e.g. `%w`, `%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 cab0d3d62..87f95ccd0 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -1,15 +1,13 @@ -import path from 'node:path' -import { describe } from 'vitest' -import { css, html, json, test, ts, txt } from '../utils' +import { expect } from 'vitest' +import { css, json, test, ts } from '../utils' test( - `upgrade JS config files with flat theme values, darkMode, and content fields`, + `upgrades a simple JS config file to CSS`, { fs: { 'package.json': json` { "dependencies": { - "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } @@ -20,7 +18,7 @@ test( module.exports = { darkMode: 'selector', - content: ['./src/**/*.{html,js}', './node_modules/my-external-lib/**/*.{html}'], + content: ['./src/**/*.{html,js}', './my-app/**/*.{html,js}'], theme: { boxShadow: { sm: '0 2px 6px rgb(15 23 42 / 0.08)', @@ -30,74 +28,11 @@ 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', - auto: 'auto', - 1: '0.25rem', - 1.5: '0.375rem', - 2: '0.5rem', - 2.5: '0.625rem', - 3: '0.75rem', - 3.5: '0.875rem', - 4: '1rem', - 5: '1.25rem', - 6: '1.5rem', - 8: '2rem', - 10: '2.5rem', - 11: '2.75rem', - 12: '3rem', - 16: '4rem', - 24: '6rem', - 32: '8rem', - 40: '10rem', - 48: '12rem', - 64: '16rem', - 80: '20rem', - 96: '24rem', - 128: '32rem', - - full: '100%', - 0: '0%', - '1/2': '50%', - '1/3': 'calc(100% / 3)', - '2/3': 'calc(100% / 3 * 2)', - '1/4': '25%', - '3/4': '75%', - '1/5': '20%', - '2/5': '40%', - '3/5': '60%', - '4/5': '80%', - '1/6': 'calc(100% / 6)', - '5/6': 'calc(100% / 6 * 5)', - '1/7': 'calc(100% / 7)', - '1/10': 'calc(100% / 10)', - '3/10': 'calc(100% / 10 * 3)', - '7/10': 'calc(100% / 10 * 7)', - '9/10': 'calc(100% / 10 * 9)', - screen: '100vw', - - 'full-minus-80': 'calc(100% - 20rem)', - 'full-minus-96': 'calc(100% - 24rem)', - - '225px': '225px', }, extend: { colors: { @@ -113,26 +48,6 @@ test( borderRadius: { '4xl': '2rem', }, - keyframes: { - 'spin-clockwise': { - '0%': { transform: 'rotate(0deg)' }, - '100%': { transform: 'rotate(360deg)' }, - }, - 'spin-counterclockwise': { - '0%': { transform: 'rotate(0deg)' }, - '100%': { transform: 'rotate(-360deg)' }, - }, - }, - animation: { - 'spin-clockwise': 'spin-clockwise 1s linear infinite', - 'spin-counterclockwise': 'spin-counterclockwise 1s linear infinite', - }, - letterSpacing: { - superWide: '0.25em', - }, - lineHeight: { - superLoose: '3', - }, }, }, plugins: [], @@ -143,43 +58,20 @@ test( @tailwind components; @tailwind utilities; `, - // prettier-ignore - 'src/test.js': ts` - export default { - 'shouldNotMigrate': !border.test + '', - 'filter': 'drop-shadow(0 0 0.5rem #000)', - } - `, - 'src/index.html': html` -
-
- `, - 'node_modules/my-external-lib/src/template.html': html` -
- Hello world! -
- `, }, }, - async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + async ({ exec, fs }) => { + await exec('npx @tailwindcss/upgrade') - expect(await fs.dumpFiles('src/**/*.{css,js,html}')).toMatchInlineSnapshot(` + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " - --- src/index.html --- -
-
- --- src/input.css --- @import 'tailwindcss'; - @source '../node_modules/my-external-lib/**/*.{html}'; + @source './**/*.{html,js}'; + @source '../my-app/**/*.{html,js}'; - @custom-variant dark (&:where(.dark, .dark *)); + @variant dark (&:where(.dark, .dark *)); @theme { --shadow-*: initial; @@ -190,130 +82,18 @@ 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)); + --font-size-*: initial; + --font-size-xs: 0.75rem; + --font-size-xs--line-height: 1rem; + --font-size-sm: 0.875rem; + --font-size-sm--line-height: 1.5rem; + --font-size-base: 1rem; + --font-size-base--line-height: 2rem; - --default-ring-color: #c0ffee; - - --opacity-*: initial; - --opacity-superOpaque: 95%; - - --text-*: initial; - --text-xs: 0.75rem; - --text-xs--line-height: 1rem; - --text-sm: 0.875rem; - --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%; - --width-1: 0.25rem; - --width-2: 0.5rem; - --width-3: 0.75rem; - --width-4: 1rem; - --width-5: 1.25rem; - --width-6: 1.5rem; - --width-8: 2rem; - --width-10: 2.5rem; - --width-11: 2.75rem; - --width-12: 3rem; - --width-16: 4rem; - --width-24: 6rem; - --width-32: 8rem; - --width-40: 10rem; - --width-48: 12rem; - --width-64: 16rem; - --width-80: 20rem; - --width-96: 24rem; - --width-128: 32rem; - --width-px: 1px; - --width-auto: auto; - --width-1_5: 0.375rem; - --width-2_5: 0.625rem; - --width-3_5: 0.875rem; - --width-full: 100%; - --width-1\\/2: 50%; - --width-1\\/3: calc(100% / 3); - --width-2\\/3: calc(100% / 3 * 2); - --width-1\\/4: 25%; - --width-3\\/4: 75%; - --width-1\\/5: 20%; - --width-2\\/5: 40%; - --width-3\\/5: 60%; - --width-4\\/5: 80%; - --width-1\\/6: calc(100% / 6); - --width-5\\/6: calc(100% / 6 * 5); - --width-1\\/7: calc(100% / 7); - --width-1\\/10: calc(100% / 10); - --width-3\\/10: calc(100% / 10 * 3); - --width-7\\/10: calc(100% / 10 * 7); - --width-9\\/10: calc(100% / 10 * 9); - --width-screen: 100vw; - --width-full-minus-80: calc(100% - 20rem); - --width-full-minus-96: calc(100% - 24rem); - --width-225px: 225px; - - --font-sans: Inter, system-ui, sans-serif; - --font-display: - Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', - 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-family-sans: Inter, system-ui, sans-serif; + --font-family-display: Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --radius-4xl: 2rem; - - --animate-spin-clockwise: spin-clockwise 1s linear infinite; - --animate-spin-counterclockwise: spin-counterclockwise 1s linear infinite; - - --tracking-superWide: 0.25em; - - --leading-superLoose: 3; - - @keyframes spin-clockwise { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } - @keyframes spin-counterclockwise { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(-360deg); - } - } - } - - /* - 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/test.js --- - export default { - 'shouldNotMigrate': !border.test + '', - 'filter': 'drop-shadow(0 0 0.5rem #000)', } " `) @@ -323,414 +103,12 @@ test( ) test( - 'skips gitignored template files even when they are explicitly referenced in `content`', + `does not upgrade a complex JS config file to CSS`, { 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', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/typography": "^0.5.15", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - import typography from '@tailwindcss/typography' - import customPlugin from './custom-plugin' - - export default { - darkMode: [ - 'variant', - [ - '@media not print { .dark & }', - '@media not eink { .dark & }', - '&:where(.dark, .dark *)', - ], - ], - plugins: [ - typography, - customPlugin({ - 'is-null': null, - 'is-true': true, - 'is-false': false, - 'is-int': 1234567, - 'is-float': 1.35, - 'is-sci': 1.35e-5, - 'is-str-null': 'null', - 'is-str-true': 'true', - 'is-str-false': 'false', - 'is-str-int': '1234567', - 'is-str-float': '1.35', - 'is-str-sci': '1.35e-5', - 'is-arr': ['foo', 'bar'], - 'is-arr-mixed': [null, true, false, 1234567, 1.35, 'foo', 'bar', 'true'], - }), - ], - } satisfies Config - `, - 'custom-plugin.js': ts` - import plugin from 'tailwindcss/plugin' - export default plugin.withOptions((_options) => ({ addVariant }) => { - addVariant('inverted', '@media (inverted-colors: inverted)') - addVariant('hocus', ['&:focus', '&:hover']) - }) - `, - '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'; - - @plugin '@tailwindcss/typography'; - @plugin '../custom-plugin' { - is-null: null; - is-true: true; - is-false: false; - is-int: 1234567; - is-float: 1.35; - is-sci: 0.0000135; - is-str-null: 'null'; - is-str-true: 'true'; - is-str-false: 'false'; - is-str-int: '1234567'; - is-str-float: '1.35'; - is-str-sci: '1.35e-5'; - is-arr: 'foo', 'bar'; - is-arr-mixed: null, true, false, 1234567, 1.35, 'foo', 'bar', 'true'; - } - - @custom-variant dark { - @media not print { - .dark & { - @slot; - } - } - @media not eink { - .dark & { - @slot; - } - } - &:where(.dark, .dark *) { - @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); - } - } - " - `) - - expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` - " - - " - `) - }, -) - -test( - 'upgrades JS config files with functions in the theme config', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - theme: { - extend: { - colors: ({ colors }) => ({ - gray: colors.neutral, - }), - }, - }, - } 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,ts}')).toMatchInlineSnapshot(` - " - --- src/input.css --- - @import 'tailwindcss'; - - @theme { - --color-gray-50: oklch(98.5% 0 none); - --color-gray-100: oklch(97% 0 none); - --color-gray-200: oklch(92.2% 0 none); - --color-gray-300: oklch(87% 0 none); - --color-gray-400: oklch(70.8% 0 none); - --color-gray-500: oklch(55.6% 0 none); - --color-gray-600: oklch(43.9% 0 none); - --color-gray-700: oklch(37.1% 0 none); - --color-gray-800: oklch(26.9% 0 none); - --color-gray-900: oklch(20.5% 0 none); - --color-gray-950: oklch(14.5% 0 none); - } - - /* - 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')).toMatchInlineSnapshot(` - " - - " - `) - }, -) - -test( - 'does not upgrade JS config files with theme keys contributed to by plugins in the theme config', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - theme: { - typography: { - DEFAULT: { - css: { - '--tw-prose-body': 'red', - color: 'var(--tw-prose-body)', - }, - }, - }, - }, - } satisfies Config - `, - 'src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - @config '../tailwind.config.ts'; - `, - }, - }, - 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')).toMatchInlineSnapshot(` - " - --- tailwind.config.ts --- - import { type Config } from 'tailwindcss' - - export default { - theme: { - typography: { - DEFAULT: { - css: { - '--tw-prose-body': 'red', - color: 'var(--tw-prose-body)', - }, - }, - }, - }, - } satisfies Config - " - `) - }, -) - -test( - `does not upgrade JS config files with inline plugins`, - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", "@tailwindcss/upgrade": "workspace:^" } } @@ -749,33 +127,14 @@ test( `, }, }, - async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') + async ({ exec, fs }) => { + await exec('npx @tailwindcss/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); - } - } " `) @@ -791,1397 +150,3 @@ test( `) }, ) - -test( - `does not upgrade JS config files with non-simple screens object`, - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - theme: { - screens: { - xl: { min: '1024px', max: '1279px' }, - tall: { raw: '(min-height: 800px)' }, - }, - }, - } 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')).toMatchInlineSnapshot(` - " - --- tailwind.config.ts --- - import { type Config } from 'tailwindcss' - - export default { - theme: { - screens: { - xl: { min: '1024px', max: '1279px' }, - tall: { raw: '(min-height: 800px)' }, - }, - }, - } satisfies Config - " - `) - }, -) - -test( - 'multi-root project', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - - // Project A - 'project-a/tailwind.config.ts': ts` - export default { - content: { - relative: true, - files: ['./src/**/*.html'], - }, - theme: { - extend: { - colors: { - primary: 'red', - }, - }, - }, - } - `, - 'project-a/src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - @config "../tailwind.config.ts"; - `, - 'project-a/src/index.html': html`
`, - - // Project B - 'project-b/tailwind.config.ts': ts` - export default { - content: { - relative: true, - files: ['./src/**/*.html'], - }, - theme: { - extend: { - colors: { - primary: 'blue', - }, - }, - }, - } - `, - 'project-b/src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - @config "../tailwind.config.ts"; - `, - 'project-b/src/index.html': html`
`, - }, - }, - async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') - - expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(` - " - --- project-a/src/input.css --- - @import 'tailwindcss'; - - @theme { - --color-primary: 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); - } - } - - --- project-b/src/input.css --- - @import 'tailwindcss'; - - @theme { - --color-primary: blue; - } - - /* - 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 sources when pointing to folders outside the project root', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - - 'frontend/tailwind.config.ts': ts` - export default { - content: { - relative: true, - files: ['./src/**/*.html', '../backend/mails/**/*.blade.php'], - }, - theme: { - extend: { - colors: { - primary: 'red', - }, - }, - }, - } - `, - 'frontend/src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - @config "../tailwind.config.ts"; - `, - 'frontend/src/index.html': html`
`, - - 'backend/mails/welcome.blade.php': html`
`, - }, - }, - async ({ root, exec, fs, expect }) => { - await exec('pnpm exec upgrade', { - cwd: path.join(root, 'frontend'), - }) - - expect(await fs.dumpFiles('frontend/**/*.css')).toMatchInlineSnapshot(` - " - --- frontend/src/input.css --- - @import 'tailwindcss'; - - @source '../../backend/mails/**/*.blade.php'; - - @theme { - --color-primary: 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); - } - } - " - `) - }, -) - -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', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - // Empty / default config - export default { - theme: { - extend: {}, - }, - } 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'; - - /* - 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 border compatibility if a custom border color is used', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - theme: { - extend: { - borderColor: ({ colors }) => ({ - DEFAULT: colors.blue[500], - }), - }, - }, - } 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'; - - /* - 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: oklch(62.3% 0.214 259.815); - } - } - " - `) - }, - ) - - test( - 'migrate border compatibility if a custom border color is used, that matches the default v4 border color', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - theme: { - extend: { - borderColor: ({ colors }) => ({ - DEFAULT: 'currentcolor', - }), - }, - }, - } 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'; - " - `) - }, - ) - - test( - 'migrate border compatibility in the file that uses the `@import "tailwindcss"` import', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - theme: {}, - } satisfies Config - `, - 'src/input.css': css`@import './tailwind.css';`, - 'src/tailwind.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 './tailwind.css'; - - --- src/tailwind.css --- - @import 'tailwindcss'; - - /* - 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 border compatibility in the file that uses the `@import "tailwindcss/preflight"` import', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - theme: {}, - } satisfies Config - `, - 'src/input.css': css` - @import './base.css'; - @import './my-base.css'; - @import './utilities.css'; - `, - 'src/base.css': css`@tailwind base;`, - 'src/utilities.css': css` - @tailwind components; - @tailwind utilities; - `, - 'src/my-base.css': css` - @layer base { - html { - color: black; - } - } - `, - }, - }, - async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') - - expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` - " - --- src/base.css --- - @import 'tailwindcss/theme' layer(theme); - @import 'tailwindcss/preflight' layer(base); - - /* - 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/input.css --- - @import './base.css'; - @import './my-base.css'; - @import './utilities.css'; - - --- src/my-base.css --- - @layer base { - html { - color: black; - } - } - - --- src/utilities.css --- - @import 'tailwindcss/utilities' layer(utilities); - " - `) - }, - ) - - test( - 'migrates extended spacing keys', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - content: ['./src/**/*.html'], - theme: { - extend: { - spacing: { - 2: '0.5rem', - 4.5: '1.125rem', - 5.5: '1.375em', // Units are different from --spacing scale - 13: '3.25rem', - 100: '100px', - miami: '1337px', - }, - }, - }, - } satisfies Config - `, - 'src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - - .container { - width: theme(spacing.2); - width: theme(spacing[4.5]); - width: theme(spacing[5.5]); - width: theme(spacing[13]); - width: theme(spacing[100]); - width: theme(spacing.miami); - } - `, - 'src/index.html': html` -
- `, - }, - }, - async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') - - expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(` - " - --- src/index.html --- -
- - --- src/input.css --- - @import 'tailwindcss'; - - @theme { - --spacing-100: 100px; - --spacing-5_5: 1.375em; - --spacing-miami: 1337px; - } - - /* - 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); - } - } - - .container { - width: --spacing(2); - width: --spacing(4.5); - width: var(--spacing-5_5); - width: --spacing(13); - width: var(--spacing-100); - width: var(--spacing-miami); - } - " - `) - }, - ) - - test( - 'retains overwriting spacing scale', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - content: ['./src/**/*.html'], - theme: { - spacing: { - 2: '0.5rem', - 4.5: '1.125rem', - 5.5: '1.375em', - 13: '3.25rem', - 100: '100px', - miami: '1337px', - }, - }, - } satisfies Config - `, - 'src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - - .container { - width: theme(spacing.2); - width: theme(spacing[4.5]); - width: theme(spacing[5.5]); - width: theme(spacing[13]); - width: theme(spacing[100]); - width: theme(spacing.miami); - } - `, - 'src/index.html': html` -
- `, - }, - }, - async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') - - expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(` - " - --- src/index.html --- -
- - --- src/input.css --- - @import 'tailwindcss'; - - @theme { - --spacing-*: initial; - --spacing-2: 0.5rem; - --spacing-13: 3.25rem; - --spacing-100: 100px; - --spacing-4_5: 1.125rem; - --spacing-5_5: 1.375em; - --spacing-miami: 1337px; - } - - /* - 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); - } - } - - .container { - width: var(--spacing-2); - width: var(--spacing-4_5); - width: var(--spacing-5_5); - width: var(--spacing-13); - width: var(--spacing-100); - width: var(--spacing-miami); - } - " - `) - }, - ) - - test( - 'migrates `container` component configurations', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "^3", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - import { type Config } from 'tailwindcss' - - export default { - content: ['./src/**/*.html'], - theme: { - container: { - center: true, - padding: { - DEFAULT: '2rem', - '2xl': '4rem', - }, - screens: { - md: '48rem', // Matches a default --breakpoint - xl: '1280px', - '2xl': '1536px', - }, - }, - }, - } satisfies Config - `, - 'src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - `, - 'src/index.html': html` -
- `, - }, - }, - async ({ exec, fs, expect }) => { - await exec('pnpm exec upgrade') - - expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(` - " - --- src/index.html --- -
- - --- src/input.css --- - @import 'tailwindcss'; - - @utility container { - margin-inline: auto; - padding-inline: 2rem; - @media (width >= --theme(--breakpoint-sm)) { - max-width: none; - } - @media (width >= 48rem) { - max-width: 48rem; - } - @media (width >= 1280px) { - max-width: 1280px; - } - @media (width >= 1536px) { - max-width: 1536px; - padding-inline: 4rem; - } - } - - /* - 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( - `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 deleted file mode 100644 index fdc60828e..000000000 --- a/integrations/upgrade/upgrade-errors.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -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 9863918d1..d9242ab04 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -1,85 +1,63 @@ import dedent from 'dedent' import fastGlob from 'fast-glob' -import { exec, execFile, spawn, type ChildProcess } from 'node:child_process' +import killPort from 'kill-port' +import { exec, spawn } from 'node:child_process' import fs from 'node:fs/promises' -import { createServer } from 'node:net' +import net from 'node:net' import { platform, tmpdir } from 'node:os' import path from 'node:path' -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' +import { test as defaultTest, expect } from 'vitest' const REPO_ROOT = path.join(__dirname, '..') -const ROOT_PNPM_WORKSPACE = Yaml.parse( - await fs.readFile(path.join(REPO_ROOT, 'pnpm-workspace.yaml'), 'utf8'), -) const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((name) => name.replace('tailwindcss-', '@tailwindcss/').replace('.tgz', ''), ) interface SpawnedProcess { - dispose: () => Promise - flush: () => void + dispose: () => void onStdout: (predicate: (message: string) => boolean) => Promise onStderr: (predicate: (message: string) => boolean) => Promise } interface ChildProcessOptions { cwd?: string - env?: Record } interface ExecOptions { ignoreStdErr?: boolean - stdin?: string } interface TestConfig { fs: { - [filePath: string]: string | Uint8Array + [filePath: string]: string } - - 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 + getFreePort(): Promise fs: { - write(filePath: string, content: string, encoding?: BufferEncoding): Promise - symlink(dst: string, src: string): Promise - create(filePaths: string[]): Promise + write(filePath: string, content: 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)[], + contents: string | string[] | RegExp | RegExp[], ): Promise - expectFileNotToContain(filePath: string, contents: string[]): Promise + expectFileNotToContain(filePath: string, contents: string | string[]): Promise } } type TestCallback = (context: TestContext) => Promise | void interface TestFlags { only?: boolean - skip?: boolean debug?: boolean - concurrent?: boolean } type SpawnActor = { predicate: (message: string) => boolean; resolve: () => void } -export const IS_WINDOWS = platform() === 'win32' - -const execFileAsync = promisify(execFile) +const IS_WINDOWS = platform() === 'win32' const TEST_TIMEOUT = IS_WINDOWS ? 120000 : 60000 const ASSERTION_TIMEOUT = IS_WINDOWS ? 10000 : 5000 @@ -93,17 +71,11 @@ export function test( name: string, config: TestConfig, testCallback: TestCallback, - { only = false, skip = false, debug = false, concurrent = false }: TestFlags = {}, + { only = false, debug = false }: TestFlags = {}, ) { - return defaultTest( + return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)( name, - { - timeout: config.timeout ?? TEST_TIMEOUT, - retry: config.retry ?? (process.env.CI ? 2 : 0), - only: only || (!process.env.CI && debug), - skip, - concurrent, - }, + { timeout: TEST_TIMEOUT, retry: process.env.CI ? 2 : 0 }, async (options) => { let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT await fs.mkdir(rootDir, { recursive: true }) @@ -118,47 +90,12 @@ 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}` @@ -166,40 +103,21 @@ export function test( } if (debug) console.log(`> ${command}`) return new Promise((resolve, reject) => { - let child = exec( + exec( command, { cwd, ...childProcessOptions, - 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) - } reject(error) } else { - if (only || debug) { - console.log(stdout.toString() + '\n\n' + stderr.toString()) - } resolve(stdout.toString() + '\n\n' + stderr.toString()) } }, ) - if (execOptions.stdin) { - child.stdin?.write(execOptions.stdin) - child.stdin?.end() - } }) }, async spawn(command: string, childProcessOptions: ChildProcessOptions = {}) { @@ -219,35 +137,27 @@ export function test( if (debug) console.log(`>& ${command}`) let child = spawn(command, { cwd, - detached: !IS_WINDOWS, shell: true, - ...childProcessOptions, env: { ...process.env, - ...childProcessOptions.env, }, + ...childProcessOptions, }) - let disposed = false + function dispose() { + child.kill() - async function dispose() { - if (disposed) return disposePromise - disposed = true - - await killProcessTree(child) - - let timer = setTimeout(() => { - forceKillProcessTree(child) - rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)) - }, ASSERTION_TIMEOUT) - disposePromise.then( - () => clearTimeout(timer), - () => clearTimeout(timer), + let timer = setTimeout( + () => + rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)), + ASSERTION_TIMEOUT, ) + disposePromise.finally(() => { + clearTimeout(timer) + }) return disposePromise } disposables.push(dispose) - function onExit() { resolveDisposal?.() } @@ -277,20 +187,16 @@ export function test( child.stdout.on('data', (result) => { let content = result.toString() - if (debug || only) console.log(content) + if (debug) console.log(content) combined.push(['stdout', content]) - for (let line of content.split('\n')) { - stdoutMessages.push(stripVTControlCharacters(line)) - } + stdoutMessages.push(content) notifyNext(stdoutActors, stdoutMessages) }) child.stderr.on('data', (result) => { let content = result.toString() - if (debug || only) console.error(content) + if (debug) console.error(content) combined.push(['stderr', content]) - for (let line of content.split('\n')) { - stderrMessages.push(stripVTControlCharacters(line)) - } + stderrMessages.push(content) notifyNext(stderrActors, stderrMessages) }) child.on('exit', onExit) @@ -301,9 +207,8 @@ export function test( }) options.onTestFailed(() => { - // In only or debug mode, messages are logged to the console - // immediately. - if (only || debug) return + // In debug mode, messages are logged to the console immediately + if (debug) return for (let [type, message] of combined) { if (type === 'stdout') { @@ -316,13 +221,6 @@ export function test( return { dispose, - flush() { - stdoutActors.splice(0) - stderrActors.splice(0) - - stdoutMessages.splice(0) - stderrMessages.splice(0) - }, onStdout(predicate: (message: string) => boolean) { return new Promise((resolve) => { stdoutActors.push({ predicate, resolve }) @@ -337,20 +235,48 @@ export function test( }, } }, - fs: { - async write(filename, content, encoding = 'utf8') { - let full = path.join(root, filename) - let dir = path.dirname(full) - await fs.mkdir(dir, { recursive: true }) + async getFreePort(): Promise { + return new Promise((resolve, reject) => { + let server = net.createServer() + server.listen(0, () => { + let address = server.address() + let port = address === null || typeof address === 'string' ? null : address.port - if (typeof content !== 'string') { - return await fs.writeFile(full, content) - } + server.close(() => { + if (port === null) { + reject(new Error(`Failed to get a free port: address is ${address}`)) + } else { + disposables.push(async () => { + // Wait for 10ms in case the process was just killed + await new Promise((resolve) => setTimeout(resolve, 10)) + + // kill-port uses `lsof` on macOS which is expensive and can + // block for multiple seconds. In order to avoid that for a + // server that is no longer running, we check if the port is + // still in use first. + let isPortTaken = await testIfPortTaken(port) + if (!isPortTaken) { + return + } + + try { + await killPort(port) + } catch { + // If the process can not be killed, we can't do anything + } + }) + resolve(port) + } + }) + }) + }) + }, + fs: { + async write(filename: string, content: string): Promise { + let full = path.join(root, filename) 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 @@ -358,36 +284,11 @@ export function test( content = content.replace(/\n/g, '\r\n') } - await fs.writeFile(full, content, encoding) + let dir = path.dirname(full) + await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(full, content) }, - - 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) - - let dir = path.dirname(full) - await fs.mkdir(dir, { recursive: true }) - await fs.writeFile(full, '') - } - }, - - async delete(filename) { - await fs.unlink(path.join(root, filename)) - }, - - async read(filePath) { + async read(filePath: string) { let content = await fs.readFile(path.resolve(root, filePath), 'utf8') // Ensure that files read on Windows have \r\n line endings removed @@ -397,20 +298,16 @@ export function test( return content }, - async glob(pattern) { + async glob(pattern: string) { let files = await fastGlob(pattern, { cwd: root }) return Promise.all( files.map(async (file) => { let content = await fs.readFile(path.join(root, file), 'utf8') - return [ - file, - // Drop license comment - content.replace(/[\s\n]*\/\*![\s\S]*?\*\/[\s\n]*/g, ''), - ] + return [file, content] }), ) }, - async dumpFiles(pattern) { + async dumpFiles(pattern: string) { let files = await context.fs.glob(pattern) return `\n${files .slice() @@ -419,21 +316,15 @@ export function test( let zParts = z[0].split('/') let aFile = aParts.at(-1) - let zFile = zParts.at(-1) + let zFile = aParts.at(-1) // Sort by depth, shallow first if (aParts.length < zParts.length) return -1 if (aParts.length > zParts.length) return 1 - // Sort by folder names, alphabetically - for (let i = 0; i < aParts.length - 1; i++) { - let diff = aParts[i].localeCompare(zParts[i]) - if (diff !== 0) return diff - } - // Sort by filename, sort files named `index` before others - if (aFile?.startsWith('index') && !zFile?.startsWith('index')) return -1 - if (zFile?.startsWith('index') && !aFile?.startsWith('index')) return 1 + if (aFile?.startsWith('index')) return -1 + if (zFile?.startsWith('index')) return 1 // Sort by filename, alphabetically return a[0].localeCompare(z[0]) @@ -447,9 +338,9 @@ export function test( let fileContent = await this.read(filePath) for (let content of Array.isArray(contents) ? contents : [contents]) { if (content instanceof RegExp) { - options.expect(fileContent).toMatch(content) + expect(fileContent).toMatch(content) } else { - options.expect(fileContent).toContain(content) + expect(fileContent).toContain(content) } } }) @@ -458,7 +349,7 @@ export function test( return retryAssertion(async () => { let fileContent = await this.read(filePath) for (let content of contents) { - options.expect(fileContent).not.toContain(content) + expect(fileContent).not.toContain(content) } }) }, @@ -469,45 +360,18 @@ export function test( node_modules/ ` - // Disable Git's automatic line ending conversion in these throwaway test - // repositories. Tools like pnpm write files (e.g. `pnpm-lock.yaml`) with - // LF endings, which on Windows (where `core.autocrlf=true` is common) - // makes Git emit noisy "LF will be replaced by CRLF" warnings when it - // touches those files. - config.fs['.gitattributes'] ??= txt` - * -text - ` - - // Ensure there is always a root `pnpm-workspace.yaml` so that transitive - // dependency overrides (and the build allow list) are injected into it. - // As of pnpm v10, these can no longer live in the `pnpm.overrides` field - // of `package.json`. - config.fs['pnpm-workspace.yaml'] ??= '' - for (let [filename, content] of Object.entries(config.fs)) { - if (content.toString().startsWith('symlink:')) { - // The symlink path is relative to the target destination's path - let target = path.join( - filename, - content.toString().slice('symlink:'.length), // Relative path - ) - - await context.fs.symlink(target, filename) - } else { - await context.fs.write(filename, content) - } + await context.fs.write(filename, content) } - let shouldInstallDependencies = config.installDependencies ?? true - try { - // Every test project gets its own root `pnpm-workspace.yaml` (see - // above). This makes the test directory its own workspace root, so even - // in debug mode — where the directory lives inside this repository's - // pnpm workspace — `pnpm install` won't attach to the parent workspace. - if (shouldInstallDependencies) { - await context.exec(`pnpm install`) - } + // 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. + let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml'] + await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`) } catch (error: any) { console.error(error) console.error(error.stdout?.toString()) @@ -518,36 +382,15 @@ export function test( let disposables: (() => Promise)[] = [] async function dispose() { - let results = await Promise.allSettled(disposables.map((dispose) => dispose())) + await Promise.all(disposables.map((dispose) => dispose())) if (!debug) { await gracefullyRemove(root) } - - let errors = results.flatMap((result) => - result.status === 'rejected' ? [result.reason] : [], - ) - if (errors.length > 0) { - throw new AggregateError(errors, 'Failed to clean up spawned processes') - } } options.onTestFinished(dispose) - // Make it a git repository, and commit all files - if (only || debug) { - try { - await context.exec('git init', { cwd: root }) - await context.exec('git add --all', { cwd: root }) - await context.exec('git commit -m "before migration" --allow-empty', { cwd: root }) - } catch (error: any) { - console.error(error) - console.error(error.stdout?.toString()) - console.error(error.stderr?.toString()) - throw error - } - } - return await testCallback(context) }, ) @@ -555,12 +398,6 @@ export function test( test.only = (name: string, config: TestConfig, testCallback: TestCallback) => { return test(name, config, testCallback, { only: true }) } -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 }) } @@ -575,52 +412,28 @@ async function overwriteVersionsInPackageJson(content: string): Promise let json = JSON.parse(content) // Resolve all workspace:^ versions to local tarballs - for (let key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) { - let dependencies = json[key] || {} - for (let dependency in dependencies) { - if (dependencies[dependency] === 'workspace:^') { - dependencies[dependency] = resolveVersion(dependency) + ;['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].forEach( + (key) => { + let dependencies = json[key] || {} + for (let dependency in dependencies) { + if (dependencies[dependency] === 'workspace:^') { + dependencies[dependency] = resolveVersion(dependency) + } } - } - } - - return JSON.stringify(json, null, 2) -} - -function overwriteVersionsInPnpmWorkspace(content: string): string { - let workspace = content.trim() === '' ? {} : Yaml.parse(content) - - // Inherit information from the root workspace - workspace.allowBuilds = { - ...ROOT_PNPM_WORKSPACE.allowBuilds, - ...workspace.allowBuilds, - } + }, + ) // Inject transitive dependency overwrite. This is necessary because // @tailwindcss/vite internally depends on a specific version of // @tailwindcss/oxide and we instead want to resolve it to the locally built // version. - // - // As of pnpm v10, the `pnpm.overrides` field in `package.json` is no longer - // read. Overrides have to be defined in `pnpm-workspace.yaml` instead. - workspace.overrides ||= {} + json.pnpm ||= {} + json.pnpm.overrides ||= {} for (let pkg of PUBLIC_PACKAGES) { - if (pkg === 'tailwindcss') { - // We want to be explicit about the `tailwindcss` package so our tests can - // also import v3 without conflicting v4 tarballs. - workspace.overrides['@tailwindcss/node>tailwindcss'] = resolveVersion(pkg) - workspace.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg) - workspace.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg) - workspace.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg) - workspace.overrides['@tailwindcss/turbopack>tailwindcss'] = resolveVersion(pkg) - workspace.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg) - workspace.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg) - } else { - workspace.overrides[pkg] = resolveVersion(pkg) - } + json.pnpm.overrides[pkg] = resolveVersion(pkg) } - return Yaml.stringify(workspace) + return JSON.stringify(json, null, 2) } function resolveVersion(dependency: string) { @@ -632,22 +445,33 @@ export function stripTailwindComment(content: string) { return content.replace(/\/\*! tailwindcss .*? \*\//g, '').trim() } -export let svg = dedent +function testIfPortTaken(port: number): Promise { + return new Promise((resolve) => { + let client = new net.Socket() + client.once('connect', () => { + resolve(true) + client.end() + }) + client.once('error', (error: any) => { + if (error.code !== 'ECONNREFUSED') { + resolve(true) + } else { + resolve(false) + } + client.end() + }) + client.connect({ port: port, host: 'localhost' }) + }) +} + export let css = dedent export let html = dedent export let ts = dedent export let js = dedent -export let jsx = dedent export let json = dedent export let yaml = dedent export let txt = dedent -export function binary(str: string | TemplateStringsArray, ...values: unknown[]): Uint8Array { - let base64 = typeof str === 'string' ? str : String.raw(str, ...values) - - return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)) -} - export function candidate(strings: TemplateStringsArray, ...values: any[]) { let output: string[] = [] for (let i = 0; i < strings.length; i++) { @@ -660,6 +484,80 @@ export function candidate(strings: TemplateStringsArray, ...values: any[]) { return `.${escape(output.join('').trim())}` } +// https://drafts.csswg.org/cssom/#serialize-an-identifier +export function escape(value: string) { + if (arguments.length == 0) { + throw new TypeError('`CSS.escape` requires an argument.') + } + var string = String(value) + var length = string.length + var index = -1 + var codeUnit + var result = '' + var firstCodeUnit = string.charCodeAt(0) + + if ( + // If the character is the first character and is a `-` (U+002D), and + // there is no second character, […] + length == 1 && + firstCodeUnit == 0x002d + ) { + return '\\' + string + } + + while (++index < length) { + codeUnit = string.charCodeAt(index) + // Note: there’s no need to special-case astral symbols, surrogate + // pairs, or lone surrogates. + + // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER + // (U+FFFD). + if (codeUnit == 0x0000) { + result += '\uFFFD' + continue + } + + if ( + // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is + // U+007F, […] + (codeUnit >= 0x0001 && codeUnit <= 0x001f) || + codeUnit == 0x007f || + // If the character is the first character and is in the range [0-9] + // (U+0030 to U+0039), […] + (index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || + // If the character is the second character and is in the range [0-9] + // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] + (index == 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && firstCodeUnit == 0x002d) + ) { + // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point + result += '\\' + codeUnit.toString(16) + ' ' + continue + } + + // If the character is not handled by one of the above rules and is + // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or + // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to + // U+005A), or [a-z] (U+0061 to U+007A), […] + if ( + codeUnit >= 0x0080 || + codeUnit == 0x002d || + codeUnit == 0x005f || + (codeUnit >= 0x0030 && codeUnit <= 0x0039) || + (codeUnit >= 0x0041 && codeUnit <= 0x005a) || + (codeUnit >= 0x0061 && codeUnit <= 0x007a) + ) { + // the character itself + result += string.charAt(index) + continue + } + + // Otherwise, the escaped character. + // https://drafts.csswg.org/cssom/#escape-a-character + result += '\\' + string.charAt(index) + } + return result +} + export async function retryAssertion( fn: () => Promise, { timeout = ASSERTION_TIMEOUT, delay = 5 }: { timeout?: number; delay?: number } = {}, @@ -670,7 +568,6 @@ export async function retryAssertion( try { return await fn() } catch (err) { - Error.captureStackTrace(err, retryAssertion) error = err await new Promise((resolve) => setTimeout(resolve, delay)) } @@ -678,15 +575,11 @@ export async function retryAssertion( throw error } -export async function fetchStyles(base: string, path = '/'): Promise { - while (base.endsWith('/')) { - base = base.slice(0, -1) - } - - let index = await fetch(`${base}${path}`) +export async function fetchStyles(port: number, path = '/'): Promise { + let index = await fetch(`http://localhost:${port}${path}`) let html = await index.text() - let linkRegex = /]*>([\s\S]*?)<\/style>/gi let stylesheets: string[] = [] @@ -702,7 +595,7 @@ export async function fetchStyles(base: string, path = '/'): Promise { stylesheets.push( ...(await Promise.all( paths.map(async (path) => { - let css = await fetch(`${base}${path}`, { + let css = await fetch(`http://localhost:${port}${path}`, { headers: { Accept: 'text/css', }, @@ -717,155 +610,21 @@ export async function fetchStyles(base: string, path = '/'): Promise { } return stylesheets.reduce((acc, css) => { - if (acc.length > 0) acc += '\n' - acc += css - return acc + return acc + '\n' + css }, '') } -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) { - await fs.rm(dir, { recursive: true, force: true }).catch((error) => { - console.log(`Failed to remove ${dir}`, error) - }) + await fs.rm(dir, { recursive: true, force: true }) } } -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() + '...', - } - }, +async function dirExists(dir: string): Promise { + try { + return await fs.stat(dir).then((stat) => stat.isDirectory()) + } catch { + return false } } diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts index f828b5071..188d6e9f4 100644 --- a/integrations/vite/astro.test.ts +++ b/integrations/vite/astro.test.ts @@ -1,4 +1,5 @@ -import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils' +import { expect } from 'vitest' +import { candidate, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' test( 'dev mode', @@ -20,8 +21,9 @@ test( // https://astro.build/config export default defineConfig({ - vite: { plugins: [tailwindcss()] }, - build: { inlineStylesheets: 'never' }, + vite: { + plugins: [tailwindcss()], + }, }) `, 'src/pages/index.astro': html` @@ -33,21 +35,12 @@ test( `, }, }, - async ({ fs, spawn, expect }) => { - let process = await spawn('pnpm astro 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 process.onStdout((m) => m.includes('watching for file changes')) + async ({ fs, spawn, getFreePort }) => { + let port = await getFreePort() + await spawn(`pnpm astro dev --port ${port}`) await retryAssertion(async () => { - let css = await fetchStyles(url) + let css = await fetchStyles(port) expect(css).toContain(candidate`underline`) }) @@ -63,139 +56,9 @@ test( `, ) - let css = await fetchStyles(url) + let css = await fetchStyles(port) expect(css).toContain(candidate`underline`) expect(css).toContain(candidate`font-bold`) }) }, ) - -test( - 'build mode', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "astro": "^4.15.2", - "react": "^19", - "react-dom": "^19", - "@astrojs/react": "^4", - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - } - } - `, - 'astro.config.mjs': ts` - import tailwindcss from '@tailwindcss/vite' - import react from '@astrojs/react' - import { defineConfig } from 'astro/config' - - // https://astro.build/config - export default defineConfig({ - vite: { plugins: [tailwindcss()] }, - integrations: [react()], - build: { inlineStylesheets: 'never' }, - }) - `, - // prettier-ignore - 'src/pages/index.astro': html` - --- - import ClientOnly from './client-only'; - --- - -
Hello, world!
- - - - - `, - 'src/pages/client-only.jsx': js` - export default function ClientOnly() { - return
Hello, world!
- } - `, - }, - }, - 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`, 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 deleted file mode 100644 index 8ba1b6b73..000000000 --- a/integrations/vite/bundled-dev.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import path from 'node:path' -import { candidate, css, html, json, retryAssertion, test, ts, txt, yaml } from '../utils' - -// Vite's experimental `bundledDev` mode invokes the `hotUpdate` hook without a -// `server`, which used to crash the plugin on every file edit. -// -// - https://github.com/tailwindlabs/tailwindcss/issues/20378 -// - https://vite.dev/blog/announcing-vite8-1#experimental-bundled-dev-mode -test( - 'dev mode (experimental `bundledDev`)', - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - `, - 'project-a/package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^8.1" - } - } - `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - experimental: { - bundledDev: true, - }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` - - - - -
Hello, world!
- - `, - 'project-a/src/index.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - }, - }, - async ({ root, spawn, fs, expect }) => { - let process = await spawn('pnpm vite dev', { - cwd: path.join(root, 'project-a'), - env: { - // Some CI machines have an upstream rolldown bug where its file watcher - // never delivers a single event. The debug output is the only way to - // tell that apart from a real regression (see below). - DEBUG: 'vite:full-bundle-mode', - }, - }) - - // Any debug output beyond the "INITIAL:" startup lines proves rolldown's - // watcher delivered at least one file event. - let watcherAlive = false - - // `hotUpdate` errors don't kill the dev server, they are only printed to - // stderr. Track them explicitly so a crash fails the test even if the - // rebuild happens to succeed anyway. - let pluginErrors: string[] = [] - process.onStderr((message) => { - if (message.includes('vite:full-bundle-mode')) { - if (!message.includes('INITIAL:')) watcherAlive = true - } else if (message.includes('@tailwindcss/vite')) { - pluginErrors.push(message) - } - return false - }) - - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - // In `bundledDev` mode the stylesheet is not served separately. Instead the - // generated CSS is embedded in the bundled JS and injected at runtime, so - // extract the bundle from the served HTML. While the bundle is being built, - // Vite serves a temporary fallback page instead. - // - // The bundle can be split into multiple chunks (e.g. the HMR client runtime - // and the app itself), and the chunk containing the CSS is not always the - // first one, so fetch every referenced script and stylesheet. - async function fetchBundledStyles(): Promise { - let index = await fetch(`${url}/`) - let html = await index.text() - if (html.includes('__vite_is_fallback_page__')) { - throw new Error('Bundling still in progress') - } - - let sources = [ - ...html.matchAll(/]*\ssrc="([^"]+)"/g), - ...html.matchAll(/]*\srel="stylesheet"[^>]*\shref="([^"]+)"/g), - ].map((match) => match[1]) - if (sources.length === 0) throw new Error(`No scripts or stylesheets found in:\n\n${html}`) - - let contents = await Promise.all( - sources.map(async (src) => { - let response = await fetch(new URL(src, `${url}/`)) - return await response.text() - }), - ) - return contents.join('\n') - } - - await retryAssertion( - async () => { - let styles = await fetchBundledStyles() - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - }, - { timeout: 10_000, delay: 100 }, - ) - - // Edit a module file and a manually `@source`d file. Without a connected - // HMR websocket client (this test only uses HTTP fetches), a file change - // does not trigger an eager rebuild — it only marks the bundle as stale, - // and the next fetch kicks off the rebuild. So poll with fetches without - // touching the files again: rewriting would re-mark the bundle stale right - // before each fetch and the rebuild could never be observed. - await fs.write( - 'project-a/index.html', - html` - - - - -
Hello, world!
- - `, - ) - - await fs.write('project-b/src/index.html', html` -
- `) - - try { - await retryAssertion( - async () => { - let styles = await fetchBundledStyles() - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`font-bold`) - }, - { timeout: 15_000, delay: 100 }, - ) - } catch (error) { - // If rolldown's watcher delivered any file event, the updates were - // genuinely dropped somewhere along the way — a real failure. - if (watcherAlive) throw error - - // Otherwise the watcher never delivered a single event, so the updates - // can never be observed no matter how long we wait — an upstream - // rolldown bug on some CI machines, not a plugin regression. The crash - // regression this test guards is still covered: `hotUpdate` is driven - // by Vite's own (working) chokidar watcher and asserted via - // `pluginErrors` below. - // - // Skipping is only sound while the server still healthily serves the - // original bundle. If it stopped serving (crashed, or wedged on the - // fallback page), that's real breakage a silent watcher must not mask. - console.log(error) - let styles = await fetchBundledStyles() - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - console.warn( - 'Skipping update assertions: rolldown’s file watcher delivered no events on this machine.', - ) - } - - expect(pluginErrors).toEqual([]) - }, -) diff --git a/integrations/vite/config.test.ts b/integrations/vite/config.test.ts index 0ac68f5e9..196d96440 100644 --- a/integrations/vite/config.test.ts +++ b/integrations/vite/config.test.ts @@ -1,3 +1,4 @@ +import { expect } from 'vitest' import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils' test( @@ -12,7 +13,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^7" + "vite": "^5.3.5" } } `, @@ -50,7 +51,7 @@ test( `, }, }, - async ({ fs, exec, expect }) => { + async ({ fs, exec }) => { await exec('pnpm vite build') let files = await fs.glob('dist/**/*.css') @@ -76,7 +77,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^7" + "vite": "^5.3.5" } } `, @@ -114,7 +115,7 @@ test( `, }, }, - async ({ fs, exec, expect }) => { + async ({ fs, exec }) => { await exec('pnpm vite build') let files = await fs.glob('dist/**/*.css') @@ -140,7 +141,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^7" + "vite": "^5.3.5" } } `, @@ -180,19 +181,12 @@ test( `, }, }, - 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) - }) + async ({ fs, getFreePort, spawn }) => { + let port = await getFreePort() + await spawn(`pnpm vite dev --port ${port}`) await retryAssertion(async () => { - let css = await fetchStyles(url, '/index.html') + let css = await fetchStyles(port, '/index.html') expect(css).toContain(candidate`text-primary`) expect(css).toContain('color: blue') }) @@ -200,7 +194,7 @@ test( await retryAssertion(async () => { await fs.write('my-color.cjs', js`module.exports = 'red'`) - let css = await fetchStyles(url, '/index.html') + let css = await fetchStyles(port, '/index.html') expect(css).toContain(candidate`text-primary`) expect(css).toContain('color: red') }) @@ -219,7 +213,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^7" + "vite": "^5.3.5" } } `, @@ -259,19 +253,12 @@ test( `, }, }, - 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) - }) + async ({ fs, getFreePort, spawn }) => { + let port = await getFreePort() + await spawn(`pnpm vite dev --port ${port}`) await retryAssertion(async () => { - let css = await fetchStyles(url, '/index.html') + let css = await fetchStyles(port, '/index.html') expect(css).toContain(candidate`text-primary`) expect(css).toContain('color: blue') }) @@ -279,7 +266,7 @@ test( await retryAssertion(async () => { await fs.write('my-color.mjs', js`export default 'red'`) - let css = await fetchStyles(url, '/index.html') + let css = await fetchStyles(port, '/index.html') expect(css).toContain(candidate`text-primary`) expect(css).toContain('color: red') }) diff --git a/integrations/vite/css-modules.test.ts b/integrations/vite/css-modules.test.ts index a680ab9b7..8e73a9340 100644 --- a/integrations/vite/css-modules.test.ts +++ b/integrations/vite/css-modules.test.ts @@ -1,65 +1,67 @@ -import { describe } from 'vitest' +import { describe, expect } from 'vitest' import { css, html, test, ts, txt } from '../utils' -describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { - test( - `dev mode`, - { - fs: { - 'package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "^7" +for (let transformer of ['postcss', 'lightningcss']) { + describe(transformer, () => { + test( + `dev mode`, + { + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^5.3.5" + } } - } - `, - '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({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - - - - -
- - `, - 'src/component.ts': ts` - import { foo } from './component.module.css' - let root = document.getElementById('root') - root.className = foo - root.innerText = 'Hello, world!' - `, - 'src/component.module.css': css` - @import 'tailwindcss/utilities'; + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
+ + `, + 'src/component.ts': ts` + import { foo } from './component.module.css' + let root = document.getElementById('root') + root.className = foo + root.innerText = 'Hello, world!' + `, + 'src/component.module.css': css` + @import 'tailwindcss/utilities'; - .foo { - @apply underline; - } - `, + .foo { + @apply underline; + } + `, + }, }, - }, - async ({ exec, fs, expect }) => { - await exec(`pnpm vite build`) + async ({ exec, fs }) => { + await exec(`pnpm vite build`) - 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, [/text-decoration-line: underline;/gi]) - }, - ) -}) + await fs.expectFileToContain(filename, [/text-decoration-line: underline;/gi]) + }, + ) + }) +} diff --git a/integrations/vite/html-style-blocks.test.ts b/integrations/vite/html-style-blocks.test.ts deleted file mode 100644 index 22bb91d78..000000000 --- a/integrations/vite/html-style-blocks.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { html, json, test, ts } from '../utils' - -test( - 'transforms html style blocks', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "@tailwindcss/vite": "workspace:^", - "vite": "^7" - } - } - `, - 'vite.config.ts': ts` - import { defineConfig } from 'vite' - import tailwindcss from '@tailwindcss/vite' - - export default defineConfig({ - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - - - -
- - - - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm vite build') - - expect(await fs.dumpFiles('dist/*.html')).toMatchInlineSnapshot(` - " - --- dist/index.html --- - - - -
- - - - " - `) - }, -) diff --git a/integrations/vite/ignored-packages.test.ts b/integrations/vite/ignored-packages.test.ts deleted file mode 100644 index 1efa9dc61..000000000 --- a/integrations/vite/ignored-packages.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils' - -const WORKSPACE = { - fs: { - 'package.json': txt` - { - "type": "module", - "dependencies": { - "tailwind-merge": "^2", - "@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()], - }) - `, - 'index.html': html` - - - - - `, - 'src/index.js': js` - import { twMerge } from 'tailwind-merge' - - twMerge('underline') - - console.log('underline') - `, - 'src/index.css': css`@import 'tailwindcss/utilities' layer(utilities);`, - }, -} - -test( - 'does not scan tailwind-merge in production builds', - WORKSPACE, - async ({ fs, exec, expect }) => { - await exec('pnpm vite build') - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [, content] = files[0] - - expect(content).toMatchInlineSnapshot(` - "@layer utilities { - .underline { - text-decoration-line: underline; - } - } - " - `) - }, -) - -test('does not scan tailwind-merge in dev builds', WORKSPACE, 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).not.toContain(candidate`flex`) - }) -}) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index c346f8e37..f56575c1a 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -1,5 +1,5 @@ import path from 'node:path' -import { describe } from 'vitest' +import { describe, expect } from 'vitest' import { candidate, css, @@ -7,7 +7,6 @@ import { html, js, json, - jsx, retryAssertion, test, ts, @@ -15,585 +14,19 @@ import { yaml, } from '../utils' -describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { - test( - `production build`, - { - 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": { - ${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!
- - `, - 'project-a/tailwind.config.js': js` - export default { - content: ['../project-b/src/**/*.js'], - } - `, - 'project-a/src/index.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - @config '../tailwind.config.js'; - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, - }, - }, - async ({ root, fs, exec, expect }) => { - await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] - - await fs.expectFileToContain(filename, [ - candidate`underline`, - candidate`m-2`, - candidate`flex`, - candidate`content-['project-b/src/index.js']`, - ]) - }, - ) - - test( - 'dev mode', - { - 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": { - ${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!
- - `, - 'project-a/about.html': html` - - - - -
Tailwind Labs
- - `, - 'project-a/tailwind.config.js': js` - export default { - content: ['../project-b/src/**/*.js'], - } - `, - 'project-a/src/index.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - @import './imported.css'; - @config '../tailwind.config.js'; - @source '../../project-b/src/**/*.html'; - `, - 'project-a/src/imported.css': css` - .imported { - color: red; - } - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, - }, - }, - 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`flex`) - expect(styles).toContain(candidate`font-bold`) - expect(styles).toContain(candidate`imported`) - }) - - await retryAssertion(async () => { - // Updates are additive and cause new candidates to be added. - await fs.write( - 'project-a/index.html', - html` - - - - -
Hello, world!
- +for (let transformer of ['postcss', 'lightningcss']) { + describe(transformer, () => { + test( + `production build`, + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a `, - ) - - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`font-bold`) - expect(styles).toContain(candidate`imported`) - expect(styles).toContain(candidate`m-2`) - }) - - await retryAssertion(async () => { - // Manually added `@source`s are watched and trigger a rebuild - await fs.write( - 'project-b/src/index.js', - js` - const className = "[.changed_&]:content-['project-b/src/index.js']" - module.exports = { className } - `, - ) - - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`font-bold`) - expect(styles).toContain(candidate`imported`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) - }) - - await retryAssertion(async () => { - // After updates to the CSS file, all previous candidates should still be in - // the generated CSS - await fs.write( - 'project-a/src/index.css', - css` - ${await fs.read('project-a/src/index.css')} - - .red { - color: red; - } - `, - ) - - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`red`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`imported`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) - expect(styles).toContain(candidate`font-bold`) - }) - - await retryAssertion(async () => { - // Trigger a partial rebuild for the next test - await fs.write( - 'project-a/index.html', - html` - - - - -
Hello, world!
- - `, - ) - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`m-4`) - }) - - await retryAssertion(async () => { - // Changing an `@imported` CSS file after a partial rebuild also triggers the correct update - await fs.write( - 'project-a/src/imported.css', - css` - .imported-updated { - color: red; - } - `, - ) - - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`red`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) - expect(styles).toContain(candidate`font-bold`) - expect(styles).toContain(candidate`imported-updated`) - }) - }, - ) - - // 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', - { - 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": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "^7" - } - } - `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` - - - - -
Hello, world!
- - `, - 'project-a/tailwind.config.js': js` - export default { - content: ['../project-b/src/**/*.js'], - } - `, - 'project-a/src/index.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - @import './custom-theme.css'; - @config '../tailwind.config.js'; - @source '../../project-b/src/**/*.html'; - `, - 'project-a/src/custom-theme.css': css` - /* Will be overwritten later */ - @theme { - --color-primary: black; - } - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, - }, - }, - async ({ root, spawn, fs, expect }) => { - let process = await spawn('pnpm vite build --watch', { - cwd: path.join(root, 'project-a'), - }) - await process.onStdout((m) => m.includes('built in')) - - let filename = '' - await retryAssertion(async () => { - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - filename = files[0][0] - }) - - await fs.expectFileToContain(filename, [ - candidate`underline`, - candidate`flex`, - css` - .text-primary { - color: var(--color-primary); - } - `, - ]) - - await retryAssertion(async () => { - await fs.write( - 'project-a/src/custom-theme.css', - css` - /* Overriding the primary color */ - @theme { - --color-primary: red; - } - `, - ) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - - expect(styles).toContain(css` - .text-primary { - color: var(--color-primary); - } - `) - }) - - await retryAssertion(async () => { - // Updates are additive and cause new candidates to be added. - await fs.write( - 'project-a/index.html', - html` - - - - -
Hello, world!
- - `, - ) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`m-2`) - }) - - await retryAssertion(async () => { - // Manually added `@source`s are watched and trigger a rebuild - await fs.write( - 'project-b/src/index.js', - js` - const className = "[.changed_&]:content-['project-b/src/index.js']" - module.exports = { className } - `, - ) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) - }) - - await retryAssertion(async () => { - // After updates to the CSS file, all previous candidates should still be in - // the generated CSS - await fs.write( - 'project-a/src/index.css', - css` - ${await fs.read('project-a/src/index.css')} - - .red { - color: red; - } - `, - ) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) - expect(styles).toContain(candidate`red`) - }) - }, - ) - - 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` + 'project-a/package.json': txt` { "type": "module", "dependencies": { @@ -601,673 +34,404 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { "tailwindcss": "workspace:^" }, "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "${version}" + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^5.3.5" } } `, - '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' + '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()], - 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` - + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` -
- +
Hello, world!
- - `, - '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
+ `, + 'project-a/tailwind.config.js': js` + export default { + content: ['../project-b/src/**/*.js'], } `, - ) + 'project-a/src/index.css': css` + @import 'tailwindcss/theme' theme(reference); + @import 'tailwindcss/utilities'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec }) => { + await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`m-2`, + candidate`flex`, + candidate`content-['project-b/src/index.js']`, + ]) + }, + ) + + test( + `dev mode`, + { + 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": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^5.3.5" + } + } + `, + '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!
+ + `, + 'project-a/about.html': html` + + + + +
Tailwind Labs
+ + `, + 'project-a/tailwind.config.js': js` + export default { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss/theme' theme(reference); + @import 'tailwindcss/utilities'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, spawn, getFreePort, fs }) => { + let port = await getFreePort() + await spawn(`pnpm vite dev --port ${port}`, { + cwd: path.join(root, 'project-a'), + }) + + // Candidates are resolved lazily, so the first visit of index.html + // will only have candidates from this file. + await retryAssertion(async () => { + let styles = await fetchStyles(port, '/index.html') + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).not.toContain(candidate`font-bold`) + }) + + // Going to about.html will extend the candidate list to include + // candidates from about.html. + await retryAssertion(async () => { + let styles = await fetchStyles(port, '/about.html') + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`font-bold`) + }) await retryAssertion(async () => { - let styles = await fetchStyles(url, '/index.html') - expect(styles).toContain(candidate`content-['updated:src/lazy.tsx']`) - }) - } + // Updates are additive and cause new candidates to be added. + await fs.write( + 'project-a/index.html', + html` + + + + +
Hello, world!
+ + `, + ) - // 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`
`, - ) + let styles = await fetchStyles(port) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`font-bold`) + expect(styles).toContain(candidate`m-2`) + }) await retryAssertion(async () => { - let styles = await fetchStyles(url, '/index.html') - expect(styles).toContain(candidate`content-['updated:src/comp-b.custom']`) + // Manually added `@source`s are watched and trigger a rebuild + await fs.write( + 'project-b/src/index.js', + js` + const className = "[.changed_&]:content-['project-b/src/index.js']" + module.exports = { className } + `, + ) + + let styles = await fetchStyles(port) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`font-bold`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) }) - } - // 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 + await retryAssertion(async () => { + // After updates to the CSS file, all previous candidates should still be in + // the generated CSS + await fs.write( + 'project-a/src/index.css', + css` + ${await fs.read('project-a/src/index.css')} + + .red { + color: red; + } + `, + ) + + let styles = await fetchStyles(port) + expect(styles).toContain(candidate`red`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + expect(styles).toContain(candidate`font-bold`) + }) + }, + ) + + test( + 'watch mode', { - let updates = (await fs.read('project-a/hmr.log')).split('"type":"update"').length + 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": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^5.3.5" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' - await fs.write( - 'project-a/src/unimported.css', + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + `, + 'project-a/tailwind.config.js': js` + export default { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss/theme' theme(reference); + @import 'tailwindcss/utilities'; + @import './custom-theme.css'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + `, + 'project-a/src/custom-theme.css': css` + /* Will be overwritten later */ + @theme { + --color-primary: black; + } + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, spawn, fs }) => { + await spawn(`pnpm vite build --watch`, { + cwd: path.join(root, 'project-a'), + }) + + let filename = '' + await retryAssertion(async () => { + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + filename = files[0][0] + }) + + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`flex`, css` - .unimported { - color: blue; + .text-primary { + color: var(--color-primary, black); } `, - ) - - // 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) + await fs.write( + 'project-a/src/custom-theme.css', + css` + /* Overriding the primary color */ + @theme { + --color-primary: red; + } + `, + ) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + + expect(styles).toContain(css` + .text-primary { + color: var(--color-primary, red); + } + `) }) - let styles = await fetchStyles(url, '/index.html') - expect(styles).toContain(candidate`content-['updated:project-b/src/index.php']`) - } - }, - ) + await retryAssertion(async () => { + // Updates are additive and cause new candidates to be added. + await fs.write( + 'project-a/index.html', + html` + + + + +
Hello, world!
+ + `, + ) - test( - `source(none) disables looking at the module graph`, - { - 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": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "^7" - } - } - `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + }) - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` - - - - -
Hello, world!
- - `, - 'project-a/src/index.css': css` - @import 'tailwindcss' source(none); - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, + await retryAssertion(async () => { + // Manually added `@source`s are watched and trigger a rebuild + await fs.write( + 'project-b/src/index.js', + js` + const className = "[.changed_&]:content-['project-b/src/index.js']" + module.exports = { className } + `, + ) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + }) + + await retryAssertion(async () => { + // After updates to the CSS file, all previous candidates should still be in + // the generated CSS + await fs.write( + 'project-a/src/index.css', + css` + ${await fs.read('project-a/src/index.css')} + + .red { + color: red; + } + `, + ) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + expect(styles).toContain(candidate`red`) + }) }, - }, - async ({ root, fs, exec, expect }) => { - await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] - - // `underline` and `m-2` are only present from files in the module graph - // which we've explicitly disabled with source(none) so they should not - // be present - await fs.expectFileNotToContain(filename, [ - // - candidate`underline`, - candidate`m-2`, - ]) - - // The files from `project-b` should be included because there is an - // explicit `@source` directive for it - await fs.expectFileToContain(filename, [ - // - candidate`flex`, - ]) - - // The explicit source directive only covers HTML files, so the JS file - // should not be included - await fs.expectFileNotToContain(filename, [ - // - candidate`content-['project-b/src/index.js']`, - ]) - }, - ) - - test( - `source("…") filters the module graph`, - { - 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": { - ${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!
- - - `, - 'project-a/app/index.js': js` - const className = "content-['project-a/app/index.js']" - export default { className } - `, - 'project-a/src/index.css': css` - @import 'tailwindcss' source('../app'); - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, - }, - }, - async ({ root, fs, exec, expect }) => { - await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] - - // `underline` and `m-2` are present in files in the module graph but - // we've filtered the module graph such that we only look in - // `./app/**/*` so they should not be present - await fs.expectFileNotToContain(filename, [ - // - candidate`underline`, - candidate`m-2`, - candidate`content-['project-a/index.html']`, - ]) - - // We've filtered the module graph to only look in ./app/**/* so the - // candidates from that project should be present - await fs.expectFileToContain(filename, [ - // - candidate`content-['project-a/app/index.js']`, - ]) - - // Even through we're filtering the module graph explicit sources are - // additive and as such files from `project-b` should be included - // because there is an explicit `@source` directive for it - await fs.expectFileToContain(filename, [ - // - candidate`content-['project-b/src/index.html']`, - ]) - - // The explicit source directive only covers HTML files, so the JS file - // should not be included - await fs.expectFileNotToContain(filename, [ - // - candidate`content-['project-b/src/index.js']`, - ]) - }, - ) - - test( - `source("…") must be a directory`, - { - 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": { - ${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!
- - - `, - 'project-a/app/index.js': js` - const className = "content-['project-a/app/index.js']" - export default { className } - `, - 'project-a/src/index.css': css` - @import 'tailwindcss' source('../i-do-not-exist'); - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, - }, - }, - async ({ root, fs, exec, expect }) => { - await expect(() => - exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }), - ).rejects.toThrow('The `source(../i-do-not-exist)` does not exist') - - 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( - `demote Tailwind roots to regular CSS files and back to Tailwind roots`, + `demote Tailwind roots to regular CSS files and back to Tailwind roots while restoring all candidates`, { fs: { 'package.json': json` @@ -1278,7 +442,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^7" + "vite": "^5.3.5" } } `, @@ -1310,19 +474,22 @@ test( 'src/index.css': css`@import 'tailwindcss';`, }, }, - async ({ spawn, fs, expect }) => { - let process = await spawn('pnpm vite dev') - await process.onStdout((m) => m.includes('ready in')) + async ({ spawn, getFreePort, fs }) => { + let port = await getFreePort() + await spawn(`pnpm vite dev --port ${port}`) - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) + // Candidates are resolved lazily, so the first visit of index.html + // will only have candidates from this file. + await retryAssertion(async () => { + let styles = await fetchStyles(port, '/index.html') + expect(styles).toContain(candidate`underline`) + expect(styles).not.toContain(candidate`font-bold`) }) + // Going to about.html will extend the candidate list to include + // candidates from about.html. await retryAssertion(async () => { - let styles = await fetchStyles(url, '/index.html') + let styles = await fetchStyles(port, '/about.html') expect(styles).toContain(candidate`underline`) expect(styles).toContain(candidate`font-bold`) }) @@ -1331,396 +498,9 @@ test( // We change the CSS file so it is no longer a valid Tailwind root. await fs.write('src/index.css', css`@import 'tailwindcss';`) - let styles = await fetchStyles(url) + let styles = await fetchStyles(port) expect(styles).toContain(candidate`underline`) expect(styles).toContain(candidate`font-bold`) }) }, ) - -test( - `does not interfere with ?raw and ?url static asset handling`, - { - 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()], - }) - `, - 'index.html': html` - - - - `, - 'src/index.js': js` - import url from './index.css?url' - import raw from './index.css?raw' - `, - 'src/index.css': css`@import 'tailwindcss';`, - }, - }, - async ({ spawn, expect }) => { - let process = await spawn('pnpm vite dev') - await process.onStdout((m) => m.includes('ready in')) - - let baseUrl = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) baseUrl = match[1] - return Boolean(baseUrl) - }) - - await retryAssertion(async () => { - // We have to load the .js file first so that the static assets are - // resolved - await fetch(`${baseUrl}/src/index.js`).then((r) => r.text()) - - let [raw, url] = await Promise.all([ - fetch(`${baseUrl}/src/index.css?raw`).then((r) => r.text()), - fetch(`${baseUrl}/src/index.css?url`).then((r) => r.text()), - ]) - - expect(firstLine(raw)).toBe(`export default "@import 'tailwindcss';"`) - expect(firstLine(url)).toBe(`export default "/src/index.css"`) - }) - }, -) - -test( - `does not interfere with ?commonjs-proxy modules`, - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^", - "plotly.js": "^3", - "vite": "^7" - } - } - `, - '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.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 }) => { - 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`maplibregl-map`]) - }, -) - -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 4020aff0f..76b189d9c 100644 --- a/integrations/vite/multi-root.test.ts +++ b/integrations/vite/multi-root.test.ts @@ -1,3 +1,4 @@ +import { expect } from 'vitest' import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' test( @@ -12,7 +13,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^7" + "vite": "^5.3.5" } } `, @@ -43,12 +44,12 @@ test( `, 'src/shared.css': css` - @reference 'tailwindcss/theme'; + @import 'tailwindcss/theme' theme(reference); @import 'tailwindcss/utilities'; `, 'src/root1.css': css` @import './shared.css'; - @custom-variant one (&:is([data-root='1'])); + @variant one (&:is([data-root='1'])); `, 'root2.html': html` @@ -60,11 +61,11 @@ test( `, 'src/root2.css': css` @import './shared.css'; - @custom-variant two (&:is([data-root='2'])); + @variant two (&:is([data-root='2'])); `, }, }, - async ({ fs, exec, expect }) => { + async ({ fs, exec }) => { await exec('pnpm vite build') let files = await fs.glob('dist/**/*.css') @@ -85,7 +86,7 @@ test( ) test( - 'dev mode', + `dev mode`, { fs: { 'package.json': json` @@ -96,7 +97,7 @@ test( "tailwindcss": "workspace:^" }, "devDependencies": { - "vite": "^7" + "vite": "^5.3.5" } } `, @@ -119,12 +120,12 @@ test( `, 'src/shared.css': css` - @reference 'tailwindcss/theme'; + @import 'tailwindcss/theme' theme(reference); @import 'tailwindcss/utilities'; `, 'src/root1.css': css` @import './shared.css'; - @custom-variant one (&:is([data-root='1'])); + @variant one (&:is([data-root='1'])); `, 'root2.html': html` @@ -136,25 +137,18 @@ test( `, 'src/root2.css': css` @import './shared.css'; - @custom-variant two (&:is([data-root='2'])); + @variant two (&:is([data-root='2'])); `, }, }, - 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) - }) + async ({ root, spawn, getFreePort, fs }) => { + let port = await getFreePort() + await spawn(`pnpm vite dev --port ${port}`) // Candidates are resolved lazily, so the first visit of index.html // will only have candidates from this file. await retryAssertion(async () => { - let styles = await fetchStyles(url, '/root1.html') + let styles = await fetchStyles(port, '/root1.html') expect(styles).toContain(candidate`one:underline`) expect(styles).not.toContain(candidate`two:underline`) }) @@ -162,7 +156,7 @@ test( // Going to about.html will extend the candidate list to include // candidates from about.html. await retryAssertion(async () => { - let styles = await fetchStyles(url, '/root2.html') + let styles = await fetchStyles(port, '/root2.html') expect(styles).not.toContain(candidate`one:underline`) expect(styles).toContain(candidate`two:underline`) }) diff --git a/integrations/vite/nuxt.test.ts b/integrations/vite/nuxt.test.ts index 20f367020..21876d2c7 100644 --- a/integrations/vite/nuxt.test.ts +++ b/integrations/vite/nuxt.test.ts @@ -1,117 +1,65 @@ -import { - candidate, - css, - fetchStyles, - getRandomPort, - html, - json, - retryAssertion, - test, - ts, - yaml, -} from '../utils' +import { expect } from 'vitest' +import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' -const SETUP = { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "nuxt": "3.16.0", - "nitropack": "2.11.0", - "tailwindcss": "workspace:^", - "vue": "latest" +test( + 'dev mode', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "nuxt": "^3.13.1", + "tailwindcss": "workspace:^", + "vue": "latest" + } } - } - `, - 'pnpm-workspace.yaml': yaml` - # - overrides: - nuxi: 3.28.0 - `, - 'nuxt.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - - // https://nuxt.com/docs/api/configuration/nuxt-config - export default defineNuxtConfig({ - vite: { - plugins: [tailwindcss()], - }, - - css: ['~/assets/css/main.css'], - devtools: { enabled: true }, - compatibilityDate: '2024-08-30', - }) - `, - 'app.vue': html` - `, - 'assets/css/main.css': css`@import 'tailwindcss';`, - }, -} + 'nuxt.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' -test('dev mode', SETUP, async ({ fs, spawn, expect }) => { - let process = await spawn('pnpm nuxt dev', { - env: { - TEST: 'false', // VERY IMPORTANT OTHERWISE YOU WON'T GET OUTPUT - NODE_ENV: 'development', - }, - }) + // https://nuxt.com/docs/api/configuration/nuxt-config + export default defineNuxtConfig({ + vite: { + plugins: [tailwindcss()], + }, - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - await process.onStdout((m) => m.includes('server warmed up in')) - - await retryAssertion(async () => { - let css = await fetchStyles(url) - expect(css).toContain(candidate`underline`) - }) - - await retryAssertion(async () => { - await fs.write( - 'app.vue', - html` + css: ['~/assets/css/main.css'], + devtools: { enabled: true }, + compatibilityDate: '2024-08-30', + }) + `, + 'app.vue': html` `, - ) - - let css = await fetchStyles(url) - expect(css).toContain(candidate`underline`) - expect(css).toContain(candidate`font-bold`) - }) -}) - -test('build', SETUP, async ({ spawn, exec, expect }) => { - await exec('pnpm nuxt build') - // The Nuxt preview server does not automatically assign a free port if 3000 - // is taken, so we use a random port instead. - let port = await getRandomPort() - let process = await spawn(`pnpm nuxt preview --port ${port}`, { - env: { - TEST: 'false', - NODE_ENV: 'development', + 'assets/css/main.css': css`@import 'tailwindcss';`, }, - }) + }, + async ({ fs, spawn, getFreePort }) => { + let port = await getFreePort() + await spawn(`pnpm nuxt dev --port ${port}`) - let url = '' - await process.onStdout((m) => { - let match = /Listening on\s*(http.*)\/?/.exec(m) - if (match) url = match[1].replace('http://[::]', 'http://127.0.0.1') - return m.includes('Listening on') - }) + await retryAssertion(async () => { + let css = await fetchStyles(port) + expect(css).toContain(candidate`underline`) + }) - await retryAssertion(async () => { - let css = await fetchStyles(url) - expect(css).toContain(candidate`underline`) - }) -}) + await retryAssertion(async () => { + await fs.write( + 'app.vue', + html` + + `, + ) + + let css = await fetchStyles(port) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + }) + }, +) diff --git a/integrations/vite/other-transforms.test.ts b/integrations/vite/other-transforms.test.ts deleted file mode 100644 index d56396065..000000000 --- a/integrations/vite/other-transforms.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import dedent from 'dedent' -import { describe } from 'vitest' -import { css, fetchStyles, html, retryAssertion, test, ts, txt } from '../utils' - -function createSetup(transformer: 'postcss' | 'lightningcss') { - return { - fs: { - 'package.json': txt` - { - "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(), - { - name: 'recolor', - transform(code, id) { - if (id.includes('.css')) { - return code.replace(/red;/g, 'blue;') - } - }, - }, - ], - }) - `, - 'index.html': html` - - - - -
Hello, world!
- - `, - 'src/index.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - - .foo { - color: red; - } - `, - }, - } -} - -describe.each(['postcss', 'lightningcss'] as const)('%s', (transformer) => { - test(`production build`, createSetup(transformer), 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, [ - css` - .foo { - color: blue; - } - `, - // Running the transforms on utilities generated by Tailwind might change in the future - dedent` - .\[background-color\:red\] { - background-color: blue; - } - `, - ]) - }) - - test('dev mode', createSetup(transformer), 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(css` - .foo { - color: blue; - } - `) - // Running the transforms on utilities generated by Tailwind might change in the future - expect(styles).toContain(dedent` - .\[background-color\:red\] { - background-color: blue; - } - `) - }) - - await retryAssertion(async () => { - await fs.write( - 'src/index.css', - css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - - .foo { - background-color: red; - } - `, - ) - - let styles = await fetchStyles(url) - expect(styles).toContain(css` - .foo { - background-color: blue; - } - `) - }) - }) - - test('watch mode', createSetup(transformer), async ({ spawn, fs, expect }) => { - let process = await spawn('pnpm vite build --watch') - await process.onStdout((m) => m.includes('built in')) - - await retryAssertion(async () => { - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - - expect(styles).toContain(css` - .foo { - color: blue; - } - `) - // Running the transforms on utilities generated by Tailwind might change in the future - expect(styles).toContain(dedent` - .\[background-color\:red\] { - background-color: blue; - } - `) - }) - - await retryAssertion(async () => { - await fs.write( - 'src/index.css', - css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - - .foo { - background-color: red; - } - `, - ) - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - - expect(styles).toContain(css` - .foo { - background-color: blue; - } - `) - }) - }) -}) diff --git a/integrations/vite/preact.test.ts b/integrations/vite/preact.test.ts deleted file mode 100644 index 8e4deec2b..000000000 --- a/integrations/vite/preact.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts, txt } from '../utils' - -test( - 'dev mode', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "preact": "^10" - }, - "devDependencies": { - "@preact/preset-vite": "^2", - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^", - "vite": "^8" - } - } - `, - 'vite.config.ts': ts` - import fs from 'node:fs' - import path from 'node:path' - import preact from '@preact/preset-vite' - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - plugins: [ - tailwindcss(), - preact(), - { - // Log all HMR payloads to a file so the test can assert on them - name: 'hmr-wiretap', - configureServer(server) { - let logFile = path.resolve('hmr.log') - fs.writeFileSync(logFile, '') - for (let environment of Object.values(server.environments)) { - let send = environment.hot.send.bind(environment.hot) - environment.hot.send = (payload) => { - fs.appendFileSync(logFile, JSON.stringify(payload) + '\\n') - return send(payload) - } - } - }, - }, - ], - }) - `, - 'index.html': html` - - - - - -
- - - - `, - 'src/main.tsx': ts` - import { render } from 'preact' - import { App } from './app' - - render(, document.getElementById('app')!) - `, - 'src/app.tsx': ts` - import { useState } from 'preact/hooks' - - export function App() { - const [count, setCount] = useState(0) - return ( - - ) - } - `, - 'src/index.css': css`@import 'tailwindcss';`, - }, - }, - async ({ fs, spawn, expect }) => { - let process = await spawn('pnpm vite dev') - await process.onStdout((m) => m.includes('ready in')) - - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - await retryAssertion(async () => { - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`underline`) - }) - - // Load the component modules, like a browser visiting the page would - await fetch(`${url}/src/main.tsx`) - await fetch(`${url}/src/app.tsx`) - - // Editing a component keeps HMR intact: new classes are delivered through - // a regular update, not a full page reload (which would lose all state) - { - await fs.write( - 'src/app.tsx', - ts` - import { useState } from 'preact/hooks' - - export function App() { - const [count, setCount] = useState(0) - return ( - - ) - } - `, - ) - - await retryAssertion(async () => { - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - }) - expect(await fs.read('hmr.log')).toContain('"type":"update"') - expect(await fs.read('hmr.log')).not.toContain('full-reload') - } - - // Changing a scanned file that is not part of the module graph (e.g. - // `package.json`, which package managers and other tooling write to while - // the dev server is running) should not trigger a full reload either — - // that would destroy client state. New candidates should still be picked - // up because the file is a watch dependency of the CSS root, so the CSS - // hot-updates through Vite's regular pipeline. - // - // https://github.com/tailwindlabs/tailwindcss/issues/20411 - { - await fs.write( - 'package.json', - txt` - { - "type": "module", - "description": "content-['package.json']", - "dependencies": { - "preact": "^10" - }, - "devDependencies": { - "@preact/preset-vite": "^2", - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^", - "vite": "^8" - } - } - `, - ) - - await retryAssertion(async () => { - let styles = await fetchStyles(url) - expect(styles).toContain(candidate`content-['package.json']`) - }) - expect(await fs.read('hmr.log')).not.toContain('full-reload') - } - }, -) diff --git a/integrations/vite/qwik.test.ts b/integrations/vite/qwik.test.ts deleted file mode 100644 index 42db6563a..000000000 --- a/integrations/vite/qwik.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { candidate, css, fetchStyles, json, retryAssertion, test, ts } from '../utils' - -test( - 'dev mode', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@builder.io/qwik": "^1", - "@builder.io/qwik-city": "^1", - "vite": "^5", - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - } - } - `, - 'vite.config.ts': ts` - import { defineConfig } from 'vite' - import { qwikVite } from '@builder.io/qwik/optimizer' - import { qwikCity } from '@builder.io/qwik-city/vite' - import tailwindcss from '@tailwindcss/vite' - - export default defineConfig(() => { - return { - plugins: [tailwindcss(), qwikCity(), qwikVite()], - } - }) - `, - 'src/root.tsx': ts` - import { component$ } from '@builder.io/qwik' - import { QwikCityProvider, RouterOutlet } from '@builder.io/qwik-city' - - import './global.css' - - export default component$(() => { - return ( - - - - - - - ) - }) - `, - 'src/global.css': css`@import 'tailwindcss/utilities.css';`, - 'src/entry.ssr.tsx': ts` - import { renderToStream, type RenderToStreamOptions } from '@builder.io/qwik/server' - import Root from './root' - - export default function (opts: RenderToStreamOptions) { - return renderToStream(, opts) - } - `, - 'src/routes/index.tsx': ts` - import { component$ } from '@builder.io/qwik' - - export default component$(() => { - return

Hello World!

- }) - `, - }, - }, - async ({ fs, spawn, expect }) => { - let process = await spawn('pnpm vite --mode ssr') - await process.onStdout((m) => m.includes('ready in')) - - let url = '' - await process.onStdout((m) => { - console.log(m) - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - await retryAssertion(async () => { - let css = await fetchStyles(url) - expect(css).toContain(candidate`underline`) - }) - - await retryAssertion(async () => { - await fs.write( - 'src/routes/index.tsx', - ts` - import { component$ } from '@builder.io/qwik' - - export default component$(() => { - return

Hello World!

- }) - `, - ) - - let css = await fetchStyles(url) - expect(css).toContain(candidate`underline`) - expect(css).toContain(candidate`flex`) - }) - }, -) diff --git a/integrations/vite/react-router.test.ts b/integrations/vite/react-router.test.ts deleted file mode 100644 index 98b0c928d..000000000 --- a/integrations/vite/react-router.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { candidate, css, fetchStyles, json, retryAssertion, test, ts, txt } from '../utils' - -const 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": "^5" - } - } - `, - 'react-router.config.ts': ts` - import type { Config } from '@react-router/dev/config' - export default { ssr: true } satisfies Config - `, - 'vite.config.ts': ts` - import { defineConfig } from 'vite' - import { reactRouter } from '@react-router/dev/vite' - import tailwindcss from '@tailwindcss/vite' - - export default defineConfig({ - plugins: [tailwindcss(), reactRouter()], - }) - `, - 'app/routes/home.tsx': ts` - export default function Home() { - return

Welcome to React Router

- } - `, - 'app/app.css': css`@import 'tailwindcss';`, - 'app/routes.ts': ts` - import { type RouteConfig, index } from '@react-router/dev/routes' - export default [index('routes/home.tsx')] satisfies RouteConfig - `, - 'app/root.tsx': ts` - import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router' - import './app.css' - export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ) - } - - export default function App() { - return - } - `, -} - -test('dev mode', { fs: WORKSPACE }, 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) - }) - - await retryAssertion(async () => { - let css = await fetchStyles(url) - expect(css).toContain(candidate`font-bold`) - }) - - await retryAssertion(async () => { - await fs.write( - 'app/routes/home.tsx', - ts` - export default function Home() { - return

Welcome to React Router

- } - `, - ) - - let css = await fetchStyles(url) - expect(css).toContain(candidate`underline`) - expect(css).toContain(candidate`font-bold`) - }) -}) - -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) - if (match) url = match[1] - return url != '' - }) - - await retryAssertion(async () => { - let css = await fetchStyles(url) - expect(css).toContain(candidate`font-bold`) - }) -}) - -test( - 'build mode using ?url stylesheet imports should only build one stylesheet (requires `file-system` scanner)', - { - fs: { - ...WORKSPACE, - 'app/root.tsx': ts` - import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router' - import styles from './app.css?url' - export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: styles }] - export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ) - } - - export default function App() { - return - } - `, - 'vite.config.ts': ts` - import { defineConfig } from 'vite' - import { reactRouter } from '@react-router/dev/vite' - import tailwindcss from '@tailwindcss/vite' - - export default defineConfig({ - plugins: [tailwindcss(), reactRouter()], - }) - `, - '.gitignore': txt` - node_modules/ - build/ - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm react-router build') - - let files = await fs.glob('build/client/assets/**/*.css') - - expect(files).toHaveLength(1) - let [filename] = files[0] - - await fs.expectFileToContain(filename, [candidate`font-bold`]) - }, -) diff --git a/integrations/vite/resolvers.test.ts b/integrations/vite/resolvers.test.ts deleted file mode 100644 index fc9aca5c7..000000000 --- a/integrations/vite/resolvers.test.ts +++ /dev/null @@ -1,804 +0,0 @@ -import { describe } from 'vitest' -import { - candidate, - css, - fetchStyles, - html, - js, - json, - retryAssertion, - test, - ts, - txt, - yaml, -} from '../utils' - -test( - 'resolves tsconfig paths in production build', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^8" - } - } - `, - 'tsconfig.json': json` - { - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - resolve: { - tsconfigPaths: true, - }, - }) - `, - 'index.html': html` - - - - -
Hello, world!
- - `, - 'src/index.css': css` - @import '@/styles/base.css'; - @plugin '@/plugin.js'; - `, - 'src/styles/base.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - 'src/plugin.js': js` - export default function ({ addUtilities }) { - addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } }) - } - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm vite build') - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] - - await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`]) - }, -) - -test( - 'resolves tsconfig paths in dev mode', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^8" - } - } - `, - 'tsconfig.json': json` - { - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - resolve: { - tsconfigPaths: true, - }, - }) - `, - 'index.html': html` - - - - -
Hello, world!
- - `, - 'src/index.css': css` - @import '@/styles/base.css'; - @plugin '@/plugin.js'; - `, - 'src/styles/base.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - 'src/plugin.js': js` - export default function ({ addUtilities }) { - addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } }) - } - `, - }, - }, - async ({ spawn, expect }) => { - let process = await spawn('pnpm vite dev') - await process.onStdout((m) => m.includes('ready in')) - - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - await retryAssertion(async () => { - let styles = await fetchStyles(url, '/index.html') - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`custom-underline`) - }) - }, -) - -test( - 'resolves at-sign aliases in production build', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^8" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - import { fileURLToPath } from 'node:url' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - resolve: { - alias: [{ find: '@', replacement: fileURLToPath(new URL('.', import.meta.url)) }], - }, - }) - `, - 'index.html': html` - - - - -
Hello, world!
- - `, - 'src/index.css': css` - @import '@/src/styles/base.css'; - @plugin '@/src/plugin.js'; - `, - 'src/styles/base.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - 'src/plugin.js': js` - export default function ({ addUtilities }) { - addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } }) - } - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm vite build') - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] - - await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`]) - }, -) - -test( - 'resolves package plugins in production build with at-sign aliases', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "tailwind-scrollbar": "^4.0.2", - "tailwindcss-motion": "^1.1.1", - "vite": "^8" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - import { fileURLToPath } from 'node:url' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - resolve: { - alias: [{ find: '@', replacement: fileURLToPath(new URL('.', import.meta.url)) }], - }, - }) - `, - 'index.html': html` - - - - -
Hello, world!
- - `, - 'src/index.css': css` - @import 'tailwindcss'; - @plugin 'tailwind-scrollbar'; - @plugin 'tailwindcss-motion'; - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm vite build') - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] - - await fs.expectFileToContain(filename, [ - candidate`scrollbar`, - candidate`scrollbar-thumb-red-500`, - candidate`motion-preset-fade`, - ]) - }, -) - -test( - 'resolves package plugins to JS entries in production build when browser points to CSS', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "plugin-browser-css": "workspace:*", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^8" - } - } - `, - 'pnpm-workspace.yaml': yaml` - # - packages: - - packages/* - `, - 'packages/plugin-browser-css/package.json': json` - { - "name": "plugin-browser-css", - "version": "1.0.0", - "type": "module", - "main": "./index.js", - "module": "./index.js", - "browser": "./browser.css" - } - `, - 'packages/plugin-browser-css/index.js': js` - export default function ({ addUtilities }) { - addUtilities({ '.browser-css-plugin': { 'border-bottom': '1px solid green' } }) - } - `, - 'packages/plugin-browser-css/browser.css': css` - .should-not-be-loaded-as-a-plugin { - display: none; - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - - - - - -
Hello, world!
- - - `, - 'src/index.css': css` - @import 'tailwindcss'; - @plugin 'plugin-browser-css'; - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm vite build') - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] - - await fs.expectFileToContain(filename, [candidate`browser-css-plugin`]) - }, -) - -test( - 'resolves package plugins to JS entries in dev mode when browser points to CSS', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "plugin-browser-css": "workspace:*", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^8" - } - } - `, - 'pnpm-workspace.yaml': yaml` - # - packages: - - packages/* - `, - 'packages/plugin-browser-css/package.json': json` - { - "name": "plugin-browser-css", - "version": "1.0.0", - "type": "module", - "main": "./index.js", - "module": "./index.js", - "browser": "./browser.css" - } - `, - 'packages/plugin-browser-css/index.js': js` - export default function ({ addUtilities }) { - addUtilities({ '.browser-css-plugin': { 'border-bottom': '1px solid green' } }) - } - `, - 'packages/plugin-browser-css/browser.css': css` - .should-not-be-loaded-as-a-plugin { - display: none; - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - - - - - -
Hello, world!
- - - `, - 'src/index.css': css` - @import 'tailwindcss'; - @plugin 'plugin-browser-css'; - `, - }, - }, - async ({ spawn, expect }) => { - let process = await spawn('pnpm vite dev') - await process.onStdout((m) => m.includes('ready in')) - - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - await retryAssertion(async () => { - let styles = await fetchStyles(url, '/index.html') - expect(styles).toContain(candidate`browser-css-plugin`) - }) - }, -) - -test( - 'resolves package plugins in dev mode when package exports CSS files', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "daisyui": "^5", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^8" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - - - - - - - - - `, - 'src/index.css': css` - @import 'tailwindcss'; - @plugin 'daisyui'; - `, - }, - }, - async ({ spawn, expect }) => { - let process = await spawn('pnpm vite dev') - await process.onStdout((m) => m.includes('ready in')) - - let url = '' - await process.onStdout((m) => { - let match = /Local:\s*(http.*)\//.exec(m) - if (match) url = match[1] - return Boolean(url) - }) - - await retryAssertion(async () => { - let styles = await fetchStyles(url, '/index.html') - expect(styles).toContain('.btn') - expect(styles).toContain('.btn-primary') - }) - }, -) - -test( - 'resolve relative CSS files correctly', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^8" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - - - - - - - `, - 'src/index.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - @import './themes/glow.css'; - `, - // References a file in the current folder, which names happens to match a - // file in the parent folder as well. - 'src/themes/glow.css': css`@import './entry.css';`, - 'src/themes/entry.css': css` - .do-include-me { - color: green; - } - `, - - // Never rerefenced, so should not be included - 'src/entry.css': css` - .do-not-include-me { - color: red; - } - `, - }, - }, - async ({ exec, fs, expect }) => { - await exec('pnpm vite build') - - expect( - (await fs.dumpFiles('./dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-.css'), - ).toMatchInlineSnapshot(` - " - --- ./dist/assets/index-.css --- - .do-include-me { - color: green; - } - " - `) - }, -) - -test( - 'resolve relative JS files correctly', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^8" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - - - - - - - `, - 'src/index.css': css` - @reference 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - @import './themes/glow.css'; - `, - // References a file in the current folder, which names happens to match a - // file in the parent folder as well. - 'src/themes/glow.css': css`@plugin "./my-plugin.js";`, - 'src/themes/my-plugin.js': ts` - export default function ({ addBase }) { - addBase({ '.do-include-me': { color: 'green' } }) - } - `, - - // Never rerefenced, so should not be included - 'src/my-plugin.js': css` - export default function ({ addBase }) { - addBase({ '.do-not-include-me': { 'color': 'red' } }) - } - `, - }, - }, - async ({ exec, fs, expect }) => { - await exec('pnpm vite build') - - expect( - (await fs.dumpFiles('./dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-.css'), - ).toMatchInlineSnapshot(` - " - --- ./dist/assets/index-.css --- - @layer base { - .do-include-me { - color: green; - } - } - " - `) - }, -) - -describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { - test( - 'resolves aliases in production build', - { - fs: { - 'package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "^5.3.5" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - import { fileURLToPath } from 'node:url' - - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - resolve: { - alias: { - '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)), - '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)), - }, - }, - }) - `, - 'index.html': html` - - - - -
Hello, world!
- - `, - 'src/index.css': css` - @import '#css-alias'; - @plugin '#js-alias'; - `, - 'src/alias.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 aliases in dev mode', - { - fs: { - 'package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "vite": "^5.3.5" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - import { fileURLToPath } from 'node:url' - - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - resolve: { - alias: { - '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)), - '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)), - }, - }, - }) - `, - 'index.html': html` - - - - -
Hello, world!
- - `, - 'src/index.css': css` - @import '#css-alias'; - @plugin '#js-alias'; - `, - 'src/alias.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`) - }) - }, - ) -}) diff --git a/integrations/vite/solidstart.test.ts b/integrations/vite/solidstart.test.ts deleted file mode 100644 index bbe9ccc88..000000000 --- a/integrations/vite/solidstart.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { candidate, css, fetchStyles, js, json, retryAssertion, test, ts } from '../utils' - -const WORKSPACE = { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@solidjs/start": "^1", - "solid-js": "^1", - "vinxi": "^0", - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - } - } - `, - 'jsconfig.json': json` - { - "compilerOptions": { - "jsx": "preserve", - "jsxImportSource": "solid-js" - } - } - `, - 'app.config.js': ts` - import { defineConfig } from '@solidjs/start/config' - import tailwindcss from '@tailwindcss/vite' - - export default defineConfig({ - vite: { - plugins: [tailwindcss()], - }, - }) - `, - 'src/entry-server.jsx': js` - // @refresh reload - import { createHandler, StartServer } from '@solidjs/start/server' - - export default createHandler(() => ( - ( - - {assets} - -
{children}
- {scripts} - - - )} - /> - )) - `, - 'src/entry-client.jsx': js` - // @refresh reload - import { mount, StartClient } from '@solidjs/start/client' - - mount(() => , document.getElementById('app')) - `, - 'src/app.jsx': js` - import './app.css' - export default function App() { - return

Hello world!

- } - `, - 'src/app.css': css`@import 'tailwindcss';`, -} - -test( - 'dev mode', - { - fs: WORKSPACE, - }, - async ({ fs, spawn, expect }) => { - let process = await spawn('pnpm vinxi dev', { - env: { - TEST: 'false', // VERY IMPORTANT OTHERWISE YOU WON'T GET OUTPUT - NODE_ENV: 'development', - }, - }) - - 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 css = await fetchStyles(url) - expect(css).toContain(candidate`underline`) - }) - - await retryAssertion(async () => { - await fs.write( - 'src/app.jsx', - js` - import './app.css' - export default function App() { - return

Hello world!

- } - `, - ) - - let css = await fetchStyles(url) - expect(css).toContain(candidate`underline`) - expect(css).toContain(candidate`font-bold`) - }) - }, -) diff --git a/integrations/vite/source-maps.test.ts b/integrations/vite/source-maps.test.ts deleted file mode 100644 index fe26d9a7c..000000000 --- a/integrations/vite/source-maps.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -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 deleted file mode 100644 index a1d660816..000000000 --- a/integrations/vite/ssr.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe } from 'vitest' -import { candidate, css, html, json, test, ts } from '../utils' - -describe.each([['^5.3'], ['^6.0'], ['^7'], ['^8']])('Using Vite %s', (version) => { - test( - `SSR build`, - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "${version}" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { - cssMinify: false, - ssrEmitAssets: true, - }, - plugins: [tailwindcss()], - }) - `, - 'index.html': html` - -
- - - `, - 'src/index.css': css`@import 'tailwindcss';`, - 'src/index.ts': ts` - import './index.css' - - document.querySelector('#app').innerHTML = \` -
Hello, world!
- \` - `, - 'server.ts': ts` - import css from './src/index.css?url' - - document.querySelector('#app').innerHTML = \` - -
Hello, world!
- \` - `, - }, - }, - 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 deleted file mode 100644 index e186c3f85..000000000 --- a/integrations/vite/svelte.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { candidate, css, html, json, retryAssertion, test, ts } from '../utils' - -test( - 'production build', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "svelte": "^5", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^5", - "@tailwindcss/vite": "workspace:^", - "vite": "^7" - } - } - `, - 'vite.config.ts': ts` - import { defineConfig } from 'vite' - import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte' - import tailwindcss from '@tailwindcss/vite' - - export default defineConfig({ - plugins: [ - svelte({ - preprocess: [vitePreprocess()], - }), - tailwindcss(), - ], - }) - `, - 'index.html': html` - - - -
- - - - `, - 'src/main.ts': ts` - import App from './App.svelte' - const app = new App({ - target: document.body, - }) - `, - 'src/index.css': css`@import 'tailwindcss';`, - 'src/App.svelte': html` - - -

Hello {name}!

- - - `, - 'src/other.css': css` - .local { - @apply text-red-500; - animation: 2s ease-in-out infinite localKeyframes; - } - - :global(.global) { - @apply text-green-500; - animation: 2s ease-in-out infinite globalKeyframes; - } - - @keyframes -global-globalKeyframes { - 0% { - opacity: 0; - } - 100% { - opacity: 100%; - } - } - - @keyframes localKeyframes { - 0% { - opacity: 0; - } - 100% { - opacity: 100%; - } - } - `, - }, - }, - async ({ exec, fs, expect }) => { - let output = await exec('pnpm vite build') - - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - - 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\}/, - /@keyframes globalKeyframes\{/, - /@keyframes svelte-.*-localKeyframes\{/, - ]) - - // Should not print any warnings - expect(output).not.toContain('vite-plugin-svelte') - }, -) - -test( - 'watch mode', - { - fs: { - 'package.json': json` - { - "type": "module", - "dependencies": { - "svelte": "^5", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^5", - "@tailwindcss/vite": "workspace:^", - "vite": "^7" - } - } - `, - 'vite.config.ts': ts` - import { defineConfig } from 'vite' - import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte' - import tailwindcss from '@tailwindcss/vite' - - export default defineConfig({ - plugins: [ - svelte({ - preprocess: [vitePreprocess()], - }), - tailwindcss(), - ], - }) - `, - 'index.html': html` - - - -
- - - - `, - 'src/main.ts': ts` - import App from './App.svelte' - const app = new App({ - target: document.body, - }) - `, - 'src/App.svelte': html` - - -

Hello {name}!

- - - `, - 'src/index.css': css` @import 'tailwindcss'; `, - 'src/other.css': css` - .local { - @apply text-red-500; - animation: 2s ease-in-out infinite localKeyframes; - } - - :global(.global) { - @apply text-green-500; - animation: 2s ease-in-out infinite globalKeyframes; - } - - @keyframes -global-globalKeyframes { - 0% { - opacity: 0; - } - 100% { - opacity: 100%; - } - } - - @keyframes localKeyframes { - 0% { - opacity: 0; - } - 100% { - opacity: 100%; - } - } - `, - }, - }, - async ({ fs, spawn, expect }) => { - let process = await spawn(`pnpm vite build --watch`) - await process.onStdout((m) => m.includes('built in')) - - await retryAssertion(async () => { - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [, css] = files[0] - expect(css).toContain(candidate`underline`) - expect(css).toContain( - '.global{color:var(--color-green-500,oklch(72.3% .219 149.579));animation:2s ease-in-out infinite globalKeyframes}', - ) - expect(css).toMatch( - /\.local.svelte-.*\{color:var\(--color-red-500,oklch\(63\.7% \.237 25\.331\)\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/, - ) - expect(css).toMatch(/@keyframes globalKeyframes\{/) - expect(css).toMatch(/@keyframes svelte-.*-localKeyframes\{/) - }) - - await fs.write( - 'src/App.svelte', - (await fs.read('src/App.svelte')).replace('underline', 'font-bold bar'), - ) - - await fs.write( - 'src/other.css', - `${await fs.read('src/other.css')}\n.bar { @apply text-pink-500; }`, - ) - - await retryAssertion(async () => { - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [, css] = files[0] - expect(css).toContain(candidate`font-bold`) - expect(css).toContain( - '.global{color:var(--color-green-500,oklch(72.3% .219 149.579));animation:2s ease-in-out infinite globalKeyframes}', - ) - expect(css).toMatch( - /\.local.svelte-.*\{color:var\(--color-red-500,oklch\(63\.7% \.237 25\.331\)\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/, - ) - expect(css).toMatch(/@keyframes globalKeyframes\{/) - expect(css).toMatch(/@keyframes svelte-.*-localKeyframes\{/) - expect(css).toMatch( - /\.bar.svelte-.*\{color:var\(--color-pink-500,oklch\(65\.6% \.241 354\.308\)\)\}/, - ) - }) - }, -) diff --git a/integrations/vite/sveltekit.test.ts b/integrations/vite/sveltekit.test.ts deleted file mode 100644 index 92e855da9..000000000 --- a/integrations/vite/sveltekit.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { candidate, css, html, js, json, test, ts } from '../utils' - -test( - 'sveltekit build resolves package plugins', - { - fs: { - 'package.json': json` - { - "type": "module", - "scripts": { - "prepare": "svelte-kit sync || echo ''" - }, - "dependencies": { - "svelte": "^5", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^6", - "@sveltejs/kit": "^2", - "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/vite": "workspace:^", - "vite": "^8" - } - } - `, - 'svelte.config.js': js` - import adapter from '@sveltejs/adapter-auto' - - export default { - kit: { - adapter: adapter(), - }, - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { sveltekit } from '@sveltejs/kit/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss(), sveltekit()], - }) - `, - 'src/app.html': html` - - - - %sveltekit.head% - - -
%sveltekit.body%
- - - `, - 'src/routes/+layout.svelte': html` - - - - `, - 'src/routes/+page.svelte': html` -
Hello, world!
- `, - 'src/app.css': css` - @import 'tailwindcss'; - @plugin '@tailwindcss/forms'; - `, - }, - }, - async ({ exec, fs, expect }) => { - await exec('pnpm vite build') - - let files = await fs.glob('.svelte-kit/output/client/**/*.css') - expect(files.length).toBeGreaterThan(0) - - await fs.expectFileToContain(files[0][0], [candidate`form-input`]) - }, -) diff --git a/integrations/vite/url-rewriting.test.ts b/integrations/vite/url-rewriting.test.ts deleted file mode 100644 index 500fe6d71..000000000 --- a/integrations/vite/url-rewriting.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { describe } from 'vitest' -import { binary, css, html, svg, test, ts, txt } from '../utils' - -const SIMPLE_IMAGE = `iVBORw0KGgoAAAANSUhEUgAAADAAAAAlAQAAAAAsYlcCAAAACklEQVR4AWMYBQABAwABRUEDtQAAAABJRU5ErkJggg==` - -describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { - test( - 'can rewrite urls in production builds', - { - fs: { - 'package.json': txt` - { - "type": "module", - "dependencies": { - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} - "@tailwindcss/vite": "workspace:^", - "vite": "^7" - } - } - `, - 'vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - plugins: [tailwindcss()], - build: { - assetsInlineLimit: 256, - cssMinify: false, - }, - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - }) - `, - 'index.html': html` - - - - - - -
- - - `, - 'src/app.css': css` - @reference 'tailwindcss'; - @import './dir-1/bar.css'; - @import './dir-1/dir-2/baz.css'; - @import './dir-1/dir-2/vector.css'; - `, - 'src/dir-1/bar.css': css` - .test1 { - background-image: url('../../resources/image.png'); - } - `, - 'src/dir-1/dir-2/baz.css': css` - .test2 { - background-image: url('../../../resources/image.png'); - } - `, - 'src/dir-1/dir-2/vector.css': css` - @import './dir-3/vector.css'; - .test3 { - background-image: url('../../../resources/vector.svg'); - } - `, - 'src/dir-1/dir-2/dir-3/vector.css': css` - .test4 { - background-image: url('./vector-2.svg'); - } - `, - 'resources/image.png': binary(SIMPLE_IMAGE), - 'resources/vector.svg': svg` - - - - - - - `, - 'src/dir-1/dir-2/dir-3/vector-2.svg': svg` - - - - - - - `, - }, - }, - 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], [SIMPLE_IMAGE]) - - let images = await fs.glob('dist/**/*.svg') - expect(images).toHaveLength(2) - - await fs.expectFileToContain(files[0][0], [/\/assets\/vector-.*?\.svg/]) - }, - ) -}) diff --git a/integrations/vite/virtual-modules.test.ts b/integrations/vite/virtual-modules.test.ts deleted file mode 100644 index 12eadb6c7..000000000 Binary files a/integrations/vite/virtual-modules.test.ts and /dev/null differ diff --git a/integrations/vite/vue.test.ts b/integrations/vite/vue.test.ts index b04f57c6b..4f582a99f 100644 --- a/integrations/vite/vue.test.ts +++ b/integrations/vite/vue.test.ts @@ -1,5 +1,5 @@ -import { stripVTControlCharacters } from 'node:util' -import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' +import { expect } from 'vitest' +import { candidate, html, json, test, ts } from '../utils' test( 'production build', @@ -15,7 +15,7 @@ test( "devDependencies": { "@vitejs/plugin-vue": "^5.1.2", "@tailwindcss/vite": "workspace:^", - "vite": "^7" + "vite": "^5.3.5" } } `, @@ -45,336 +45,25 @@ test( `, 'src/App.vue': html` - + `, }, }, - async ({ fs, exec, expect }) => { + async ({ fs, exec }) => { await exec('pnpm vite build') let files = await fs.glob('dist/**/*.css') expect(files).toHaveLength(1) await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`foo`]) - 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.mts b/integrations/vitest.config.mts deleted file mode 100644 index 6855b146b..000000000 --- a/integrations/vitest.config.mts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineProject } from 'vitest/config' - -export default defineProject({ - test: { - hideSkippedTests: true, - }, -}) diff --git a/integrations/webpack/index.test.ts b/integrations/webpack/index.test.ts deleted file mode 100644 index 2a91411d2..000000000 --- a/integrations/webpack/index.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { css, html, js, json, test } from '../utils' - -test( - 'webpack + PostCSS (watch)', - { - fs: { - 'package.json': json` - { - "main": "./src/index.js", - "browser": "./src/index.js", - "dependencies": { - "css-loader": "^6", - "postcss": "^8", - "postcss-loader": "^7", - "webpack": "^5", - "webpack-cli": "^5", - "mini-css-extract-plugin": "^2", - "tailwindcss": "workspace:^", - "@tailwindcss/postcss": "workspace:^" - } - } - `, - 'postcss.config.js': js` - /** @type {import('postcss-load-config').Config} */ - module.exports = { - plugins: { - '@tailwindcss/postcss': {}, - }, - } - `, - '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', 'postcss-loader'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - 'src/unrelated.module.css': css` - .module { - color: var(--color-blue-500); - } - `, - }, - }, - async ({ fs, spawn, exec, expect }) => { - // Generate the initial build so output CSS files exist on disk - await exec('pnpm webpack --mode=development') - - // NOTE: We are writing to an output CSS file which is not being ignored by - // `.gitignore` nor marked with `@source not`. This should not result in an - // infinite loop. - 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 --- - :root, :host { - --color-blue-500: oklch(62.3% 0.214 259.815); - } - .flex { - display: flex; - } - " - `) - - await fs.write( - 'src/unrelated.module.css', - css` - .module { - color: var(--color-blue-500); - background-color: var(--color-red-500); - } - `, - ) - await process.onStdout((m) => m.includes('compiled successfully in')) - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - :root, :host { - --color-red-500: oklch(63.7% 0.237 25.331); - --color-blue-500: oklch(62.3% 0.214 259.815); - } - .flex { - display: flex; - } - " - `) - }, -) diff --git a/integrations/webpack/loader.test.ts b/integrations/webpack/loader.test.ts deleted file mode 100644 index 9c3d870af..000000000 --- a/integrations/webpack/loader.test.ts +++ /dev/null @@ -1,495 +0,0 @@ -import { css, html, js, json, test } from '../utils' - -test( - '@tailwindcss/webpack loader (build)', - { - fs: { - 'package.json': json` - { - "main": "./src/index.js", - "browser": "./src/index.js", - "dependencies": { - "css-loader": "^6", - "webpack": "^5", - "webpack-cli": "^5", - "mini-css-extract-plugin": "^2", - "tailwindcss": "workspace:^", - "@tailwindcss/webpack": "workspace:^" - } - } - `, - 'webpack.config.js': js` - let MiniCssExtractPlugin = require('mini-css-extract-plugin') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .flex { - display: flex; - } - " - `) - }, -) - -test( - '@tailwindcss/webpack loader (watch)', - { - fs: { - 'package.json': json` - { - "main": "./src/index.js", - "browser": "./src/index.js", - "dependencies": { - "css-loader": "^6", - "webpack": "^5", - "webpack-cli": "^5", - "mini-css-extract-plugin": "^2", - "tailwindcss": "workspace:^", - "@tailwindcss/webpack": "workspace:^" - } - } - `, - 'webpack.config.js': js` - let MiniCssExtractPlugin = require('mini-css-extract-plugin') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - }, - }, - async ({ fs, spawn, exec, expect }) => { - // Generate the initial build so output CSS files exist on disk - await exec('pnpm webpack --mode=development') - - let process = await spawn('pnpm webpack --mode=development --watch') - await process.onStdout((m) => m.includes('compiled successfully in')) - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .flex { - display: flex; - } - " - `) - - // Add a new Tailwind class to the HTML file - await fs.write('src/index.html', html` -
- `) - await process.onStdout((m) => m.includes('compiled successfully in')) - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .flex { - display: flex; - } - .underline { - text-decoration-line: underline; - } - " - `) - }, -) - -test( - '@tailwindcss/webpack loader with @apply', - { - fs: { - 'package.json': json` - { - "main": "./src/index.js", - "browser": "./src/index.js", - "dependencies": { - "css-loader": "^6", - "webpack": "^5", - "webpack-cli": "^5", - "mini-css-extract-plugin": "^2", - "tailwindcss": "workspace:^", - "@tailwindcss/webpack": "workspace:^" - } - } - `, - 'webpack.config.js': js` - let MiniCssExtractPlugin = require('mini-css-extract-plugin') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.css': css` - @import 'tailwindcss/theme'; - - .btn { - @apply flex items-center px-4 py-2 rounded-md; - } - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - :root, :host { - --spacing: 0.25rem; - --radius-md: 0.375rem; - } - .btn { - display: flex; - align-items: center; - border-radius: var(--radius-md); - padding-inline: calc(var(--spacing) * 4); - padding-block: calc(var(--spacing) * 2); - } - " - `) - }, -) - -test( - '@tailwindcss/webpack loader with optimization', - { - fs: { - 'package.json': json` - { - "main": "./src/index.js", - "browser": "./src/index.js", - "dependencies": { - "css-loader": "^6", - "webpack": "^5", - "webpack-cli": "^5", - "mini-css-extract-plugin": "^2", - "tailwindcss": "workspace:^", - "@tailwindcss/webpack": "workspace:^" - } - } - `, - 'webpack.config.js': js` - let MiniCssExtractPlugin = require('mini-css-extract-plugin') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - { - loader: '@tailwindcss/webpack', - options: { - optimize: true, - }, - }, - ], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .flex{display:flex} - " - `) - }, -) - -test( - '@tailwindcss/webpack loader with CSS @import', - { - fs: { - 'package.json': json` - { - "main": "./src/index.js", - "browser": "./src/index.js", - "dependencies": { - "css-loader": "^6", - "webpack": "^5", - "webpack-cli": "^5", - "mini-css-extract-plugin": "^2", - "tailwindcss": "workspace:^", - "@tailwindcss/webpack": "workspace:^" - } - } - `, - 'webpack.config.js': js` - let MiniCssExtractPlugin = require('mini-css-extract-plugin') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import './custom.css'; - @import 'tailwindcss/theme'; - @import 'tailwindcss/utilities'; - `, - 'src/custom.css': css` - /**/ - @utility custom-util { - color: var(--color-red-500); - } - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - :root, :host { - --color-red-500: oklch(63.7% 0.237 25.331); - } - .flex { - display: flex; - } - .custom-util { - color: var(--color-red-500); - } - " - `) - }, -) - -test( - '@tailwindcss/webpack loader with @plugin', - { - fs: { - 'package.json': json` - { - "main": "./src/index.js", - "browser": "./src/index.js", - "dependencies": { - "css-loader": "^6", - "webpack": "^5", - "webpack-cli": "^5", - "mini-css-extract-plugin": "^2", - "tailwindcss": "workspace:^", - "@tailwindcss/webpack": "workspace:^" - } - } - `, - 'webpack.config.js': js` - let MiniCssExtractPlugin = require('mini-css-extract-plugin') - - module.exports = { - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, - } - `, - 'src/index.js': js`import './index.css'`, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @import 'tailwindcss/utilities'; - @plugin './plugin.js'; - `, - 'src/plugin.js': js` - export default function ({ addUtilities }) { - addUtilities({ - '.custom-underline': { - 'border-bottom': '1px solid green', - }, - }) - } - `, - }, - }, - async ({ fs, exec, expect }) => { - await exec('pnpm webpack --mode=development') - - expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` - " - --- ./dist/main.css --- - .custom-underline { - border-bottom: 1px solid green; - } - " - `) - }, -) - -test( - '@tailwindcss/webpack loader isolates cache by resource including query', - { - fs: { - 'package.json': json` - { - "main": "./src/index.js", - "browser": "./src/index.js", - "dependencies": { - "css-loader": "^6", - "webpack": "^5", - "webpack-cli": "^5", - "mini-css-extract-plugin": "^2", - "tailwindcss": "workspace:^", - "@tailwindcss/webpack": "workspace:^" - } - } - `, - 'webpack.config.js': js` - let MiniCssExtractPlugin = require('mini-css-extract-plugin') - let path = require('node:path') - - module.exports = { - mode: 'development', - entry: { - a: './src/a.js', - b: './src/b.js', - }, - output: { - clean: true, - }, - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /.css$/i, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - '@tailwindcss/webpack', - path.resolve(__dirname, 'query-loader.js'), - ], - }, - ], - }, - } - `, - 'query-loader.js': js` - module.exports = function (source) { - if (this.resourceQuery.includes('a')) { - return '@import "tailwindcss/utilities"; @utility only-a {color: var(--color-red-500);}' - } - - if (this.resourceQuery.includes('b')) { - return '@import "tailwindcss/utilities"; @utility only-b {color: var(--color-blue-500);}' - } - - return source - } - `, - 'src/a.js': js`import './index.css?a'`, - 'src/b.js': js`import './index.css?b'`, - 'src/index.css': css``, - }, - }, - async ({ fs, exec }) => { - await exec('pnpm webpack --mode=development') - - await fs.expectFileToContain('dist/a.css', ['only-a', '--color-red-500']) - await fs.expectFileToContain('dist/b.css', ['only-b', '--color-blue-500']) - await fs.expectFileNotToContain('dist/a.css', ['only-b', '--color-blue-500']) - await fs.expectFileNotToContain('dist/b.css', ['only-a', '--color-red-500']) - }, -) diff --git a/package.json b/package.json index 7d41a469e..726230f38 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,10 @@ "build": "turbo build --filter=!./playgrounds/*", "postbuild": "node ./scripts/pack-packages.mjs", "dev": "turbo dev --filter=!./playgrounds/*", - "test": "cargo test && vitest run --hideSkippedTests", - "test:integrations": "vitest --root=./integrations", - "test:ui": "pnpm run --filter=tailwindcss test:ui && pnpm run --filter=@tailwindcss/browser test:ui", - "tdd": "vitest --hideSkippedTests", + "test": "cargo test && vitest run", + "test:integrations": "vitest --root=./integrations --no-file-parallelism", + "test:ui": "pnpm run --filter=tailwindcss test:ui", + "tdd": "vitest", "bench": "vitest bench", "version-packages": "node ./scripts/version-packages.mjs", "vite": "pnpm run --filter=vite-playground dev", @@ -48,17 +48,23 @@ }, "license": "MIT", "devDependencies": { - "@playwright/test": "^1.62.1", + "@playwright/test": "^1.47.1", "@types/node": "catalog:", - "postcss": "catalog:", - "postcss-import": "^16.1.1", - "prettier": "catalog:", - "prettier-plugin-embed": "^0.5.1", - "prettier-plugin-organize-imports": "^4.3.0", - "tsup": "^8.5.1", - "turbo": "^2.10.8", - "typescript": "^5.9.3", - "vitest": "^4.1.10" + "postcss": "8.4.47", + "postcss-import": "^16.1.0", + "prettier": "^3.3.3", + "prettier-plugin-embed": "^0.4.15", + "prettier-plugin-organize-imports": "^4.0.0", + "tsup": "^8.2.4", + "turbo": "^2.1.3", + "typescript": "^5.5.4", + "vitest": "^2.0.5" }, - "packageManager": "pnpm@11.9.0" + "packageManager": "pnpm@9.6.0", + "pnpm": { + "patchedDependencies": { + "@parcel/watcher@2.4.1": "patches/@parcel__watcher@2.4.1.patch", + "lightningcss@1.26.0": "patches/lightningcss@1.26.0.patch" + } + } } diff --git a/packages/@tailwindcss-browser/README.md b/packages/@tailwindcss-browser/README.md deleted file mode 100644 index 5f532607d..000000000 --- a/packages/@tailwindcss-browser/README.md +++ /dev/null @@ -1,36 +0,0 @@ -

- - - - - Tailwind CSS - - -

- -

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

- -

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

- ---- - -## Documentation - -For full documentation, visit [tailwindcss.com](https://tailwindcss.com). - -## Community - -For help, discussion about best practices, or feature ideas: - -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) - -## Contributing - -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-browser/package.json b/packages/@tailwindcss-browser/package.json deleted file mode 100644 index 70af78c1e..000000000 --- a/packages/@tailwindcss-browser/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@tailwindcss/browser", - "version": "4.3.3", - "description": "A utility-first CSS framework for rapidly building custom user interfaces.", - "license": "MIT", - "main": "./dist/index.global.js", - "browser": "./dist/index.global.js", - "repository": { - "type": "git", - "url": "https://github.com/tailwindlabs/tailwindcss.git", - "directory": "packages/@tailwindcss-browser" - }, - "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", - "homepage": "https://tailwindcss.com", - "scripts": { - "lint": "tsc --noEmit", - "build": "tsup-node", - "dev": "pnpm run build -- --watch", - "test:ui": "playwright test" - }, - "exports": { - ".": "./dist/index.global.js", - "./package.json": "./package.json" - }, - "files": [ - "dist" - ], - "publishConfig": { - "provenance": true, - "access": "public" - }, - "devDependencies": { - "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 deleted file mode 100644 index c6059dd82..000000000 --- a/packages/@tailwindcss-browser/playwright.config.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { defineConfig, devices } from '@playwright/test' - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './tests', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [['html', { open: 'never' }]], - /* 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('/')`. */ - // baseURL: 'http://127.0.0.1:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - // Playwright 1.60's Windows WebKit worker hangs on shutdown after all tests - // pass, causing CI to fail with a worker force-kill error. - ...(process.platform === 'win32' - ? [] - : [{ name: 'webkit', use: { ...devices['Desktop Safari'] } }]), - { - name: 'firefox', - use: { - ...devices['Desktop Firefox'], - // https://playwright.dev/docs/test-use-options#more-browser-and-context-options - launchOptions: { - // https://playwright.dev/docs/api/class-browsertype#browser-type-launch-option-firefox-user-prefs - firefoxUserPrefs: { - // By default, headless Firefox runs as though no pointers - // capabilities are available. - // https://github.com/microsoft/playwright/issues/7769#issuecomment-966098074 - // - // This impacts our `hover` variant implementation which uses an - // '(hover: hover)' media query to determine if hover is available. - // - // Available values for pointer capabilities: - // NO_POINTER = 0x00; - // COARSE_POINTER = 0x01; - // FINE_POINTER = 0x02; - // HOVER_CAPABLE_POINTER = 0x04; - // - // Setting to 0x02 | 0x04 says the system supports a mouse - 'ui.primaryPointerCapabilities': 0x02 | 0x04, - 'ui.allPointerCapabilities': 0x02 | 0x04, - }, - }, - }, - }, - ], -}) diff --git a/packages/@tailwindcss-browser/src/assets.ts b/packages/@tailwindcss-browser/src/assets.ts deleted file mode 100644 index e72d47658..000000000 --- a/packages/@tailwindcss-browser/src/assets.ts +++ /dev/null @@ -1,11 +0,0 @@ -import index from 'tailwindcss/index.css' -import preflight from 'tailwindcss/preflight.css' -import theme from 'tailwindcss/theme.css' -import utilities from 'tailwindcss/utilities.css' - -export const css = { - index, - preflight, - theme, - utilities, -} diff --git a/packages/@tailwindcss-browser/src/index.ts b/packages/@tailwindcss-browser/src/index.ts deleted file mode 100644 index 49c7e4675..000000000 --- a/packages/@tailwindcss-browser/src/index.ts +++ /dev/null @@ -1,294 +0,0 @@ -import * as tailwindcss from 'tailwindcss' -import * as assets from './assets' -import { Instrumentation } from './instrumentation' - -/** - * The type used by ` - `, - body: html`
`, - }) - - await expect(page.locator('[data-test]')).toHaveCSS('background-color', 'rgb(255, 0, 0)') - - await page.evaluate(() => { - document.querySelector('[data-css]')!.textContent = ` - .foo { - @apply bg-blue; - } - ` - }) - - await expect(page.locator('[data-test]')).toHaveCSS('background-color', 'rgb(0, 0, 255)') -}) - -test('changes to `@theme`', async ({ page }) => { - await server.render({ - page, - head: html` - - `, - body: html`
`, - }) - - await expect(page.locator('[data-test]')).toHaveCSS('background-color', 'rgb(255, 0, 0)') - - await page.evaluate(() => { - document.querySelector('[data-css]')!.textContent = ` - @theme { - --color-primary: #0000ff; - } - ` - }) - - await expect(page.locator('[data-test]')).toHaveCSS('background-color', 'rgb(0, 0, 255)') -}) - -test('no classes', async ({ page }) => { - await server.render({ - page, - body: html`
test
`, - }) - - await expect(page.locator('body')).toHaveCSS('margin', '0px') -}) - -test('html classes', async ({ page }) => { - await server.render({ - page, - htmlClasses: 'h-4', - }) - - await expect(page.locator('html')).toHaveCSS('height', '16px') -}) - -async function createServer() { - const { createApp, createRouter, defineEventHandler, toNodeListener } = await import('h3') - const { listen } = await import('listhen') - - interface PageOptions { - page: Page - head?: string - body?: string - htmlClasses?: string - } - - async function render({ page, htmlClasses, head, body }: PageOptions) { - let content = html` - - - - - - - Document - - - ${head ?? ''} - - - ${body ?? ''} - - - ` - - router.get( - '/', - defineEventHandler(() => content), - ) - - await page.goto(server.url) - } - - const app = createApp() - const router = createRouter() - - router.get( - '/tailwindcss.js', - defineEventHandler(() => readFile(require.resolve('@tailwindcss/browser'))), - ) - - app.use(router) - - let workerIndex = Number(process.env.TEST_WORKER_INDEX ?? 0) - - let listener = await listen(toNodeListener(app), { - port: 3000 + workerIndex, - showURL: false, - open: false, - }) - - return { - app, - url: listener.url, - close: listener.close, - render, - } -} diff --git a/packages/@tailwindcss-browser/tsconfig.json b/packages/@tailwindcss-browser/tsconfig.json deleted file mode 100644 index 8ea040daf..000000000 --- a/packages/@tailwindcss-browser/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "compilerOptions": { - "lib": ["es2022", "esnext.disposable", "dom", "dom.iterable"], - }, -} diff --git a/packages/@tailwindcss-browser/tsup.config.ts b/packages/@tailwindcss-browser/tsup.config.ts deleted file mode 100644 index 10a73f2b7..000000000 --- a/packages/@tailwindcss-browser/tsup.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { defineConfig } from 'tsup' - -export default defineConfig({ - format: ['iife'], - clean: true, - minify: true, - entry: ['src/index.ts'], - noExternal: [/.*/], - loader: { - '.css': 'text', - }, - define: { - 'process.env.NODE_ENV': '"production"', - 'process.env.FEATURES_ENV': '"stable"', - }, - esbuildPlugins: [ - { - name: 'patch-intellisense-apis', - setup(build) { - build.onLoad({ filter: /intellisense.ts$/ }, () => { - return { - contents: ` - export function getClassList() { return [] } - export function getVariants() { return [] } - export function canonicalizeCandidates() { return [] } - `, - } - }) - }, - }, - ], -}) diff --git a/packages/@tailwindcss-browser/vitest.config.mts b/packages/@tailwindcss-browser/vitest.config.mts deleted file mode 100644 index f6f91227d..000000000 --- a/packages/@tailwindcss-browser/vitest.config.mts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - exclude: ['**/*.spec.?(c|m)[jt]s?(x)', '**/node_modules/**'], - }, -}) diff --git a/packages/@tailwindcss-cli/README.md b/packages/@tailwindcss-cli/README.md index 5f532607d..95ec9d87d 100644 --- a/packages/@tailwindcss-cli/README.md +++ b/packages/@tailwindcss-cli/README.md @@ -13,10 +13,10 @@

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

--- @@ -27,10 +27,14 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ## Community -For help, discussion about best practices, or feature ideas: +For help, discussion about best practices, or any other conversation that would benefit from being searchable: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[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) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-cli/package.json b/packages/@tailwindcss-cli/package.json index 7fa5b5500..aff1367ef 100644 --- a/packages/@tailwindcss-cli/package.json +++ b/packages/@tailwindcss-cli/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/cli", - "version": "4.3.3", + "version": "4.0.0-alpha.26", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -29,12 +29,13 @@ "access": "public" }, "dependencies": { - "@parcel/watcher": "catalog:", - "@tailwindcss/node": "workspace:*", - "@tailwindcss/oxide": "workspace:*", - "enhanced-resolve": "catalog:", + "@parcel/watcher": "^2.4.1", + "@tailwindcss/node": "workspace:^", + "@tailwindcss/oxide": "workspace:^", + "enhanced-resolve": "^5.17.1", + "lightningcss": "catalog:", "mri": "^1.2.0", - "picocolors": "^1.1.1", - "tailwindcss": "workspace:*" + "picocolors": "^1.0.1", + "tailwindcss": "workspace:^" } } diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index ffef2ae8b..02a39b266 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -1,13 +1,8 @@ -import { - compile, - env, - Instrumentation, - optimize, - toSourceMap, - type SourceMap, -} from '@tailwindcss/node' +import watcher from '@parcel/watcher' +import { compile, env } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner, type ChangedContent } from '@tailwindcss/oxide' +import { Features, transform } from 'lightningcss' import { existsSync, type Stats } from 'node:fs' import fs from 'node:fs/promises' import path from 'node:path' @@ -20,13 +15,10 @@ import { highlight, println, relative, - wordWrap, } from '../../utils/renderer' import { drainStdin, outputFile } from './utils' const css = String.raw -const DEBUG = env.DEBUG -const DEFAULT_POLL_INTERVAL_MS = 250 export function options() { return { @@ -39,20 +31,11 @@ export function options() { type: 'string', description: 'Output file', alias: '-o', - default: '-', }, '--watch': { type: 'boolean | string', - description: - 'Watch for changes and rebuild as needed, and use `always` to keep watching when stdin is closed', + description: 'Watch for changes and rebuild as needed', alias: '-w', - values: ['always'], - }, - '--poll': { - type: 'boolean | number', - description: 'Use polling instead of filesystem events when watching', - default: false, - values: ['ms'], }, '--minify': { type: 'boolean', @@ -68,73 +51,14 @@ export function options() { description: 'The current working directory', default: '.', }, - '--map': { - type: 'boolean | string', - description: 'Generate a source map', - default: false, - }, - '--silent': { - type: 'boolean', - description: 'Suppress non-error output', - }, } satisfies Arg } -function formatError(error: unknown): string { - let seen = new Set() // Track seen errors to prevent circular errors - function render(err: unknown, depth: number): string[] { - let indent = ' '.repeat(depth) - let width = (process.stderr.columns ?? Infinity) - 2 * (depth + 1) - - let output = [ - `${indent}${red(depth === 0 ? 'Error:' : 'Caused by:')}`, - `${indent}${dim('\u250C')}`, - ] - - for (let line of `${err}`.split('\n')) { - let wrapped = wordWrap(line, width) - if (wrapped.length === 0) wrapped = [''] - for (let chunk of wrapped) { - output.push(`${indent}${dim('\u2502')} ${chunk}`) - } - } - - output.push(`${indent}${dim('\u2514')}`) - - if (typeof err === 'object' && err !== null && 'cause' in err && err.cause != null) { - if (!seen.add(err).has(err.cause)) { - output.push(...render(err.cause, depth + 1)) - } - } - - return output - } - - return render(error, 0).join('\n') -} - -async function handleError(fn: () => T): Promise { - try { - return await fn() - } catch (err) { - eprintln(formatError(err)) - process.exit(1) - } -} - export async function handle(args: Result>) { - if (!args['--silent']) eprintln(header()) - if (!args['--silent']) eprintln() - - using I = new Instrumentation() - DEBUG && I.start('[@tailwindcss/cli] (initial build)') - let base = path.resolve(args['--cwd']) - // Resolve the output as an absolute path. If the output is a `-`, then we - // don't need to resolve it because this is a flag to indicate that we want to - // use `stdout` instead. - if (args['--output'] && args['--output'] !== '-') { + // Resolve the output as an absolute path. + if (args['--output']) { args['--output'] = path.resolve(base, args['--output']) } @@ -146,43 +70,13 @@ export async function handle(args: Result>) { // Ensure the provided `--input` exists. if (!existsSync(args['--input'])) { + eprintln(header()) + eprintln() eprintln(`Specified input file ${highlight(relative(args['--input']))} does not exist.`) process.exit(1) } } - // Check if the input and output file paths are identical, otherwise return an - // error to the user. - if (args['--input'] === args['--output'] && args['--input'] !== '-') { - eprintln( - `Specified input file ${highlight(relative(args['--input']))} and output file ${highlight(relative(args['--output']))} are identical.`, - ) - process.exit(1) - } - - // If the user passes `{bin} build --map -` then this likely means they want to output the map inline - // this is the default behavior of `{bin build} --map` to inform the user of that - if (args['--map'] === '-') { - eprintln(`Use --map without a value to inline the source map.`) - process.exit(1) - } - - if (args['--poll'] === undefined) { - eprintln(`Use --poll with a non-zero value in milliseconds.`) - process.exit(1) - } - let pollInterval = args['--poll'] === true ? DEFAULT_POLL_INTERVAL_MS : args['--poll'] - if (pollInterval !== false && pollInterval <= 0) { - eprintln(`Specified polling interval must be a positive number.`) - process.exit(1) - } - - // Resolve the map as an absolute path. If the output is true then we - // don't need to resolve it because it'll be an inline source map - if (args['--map'] && args['--map'] !== true) { - args['--map'] = path.resolve(base, args['--map']) - } - let start = process.hrtime.bigint() let input = args['--input'] @@ -196,165 +90,108 @@ export async function handle(args: Result>) { let previous = { css: '', optimizedCss: '', - output: null as string | null, } - async function write( - css: string, - map: SourceMap | null, - args: Result>, - I: Instrumentation, - ) { + async function write(css: string, args: Result>) { let output = css // Optimize the output if (args['--minify'] || args['--optimize']) { if (css !== previous.css) { - DEBUG && I.start('Optimize CSS') - let optimized = optimize(css, { + env.DEBUG && console.time('[@tailwindcss/cli] Optimize CSS') + let optimizedCss = optimizeCss(css, { file: args['--input'] ?? 'input.css', minify: args['--minify'] ?? false, - map: map?.raw ?? undefined, }) - DEBUG && I.end('Optimize CSS') + env.DEBUG && console.timeEnd('[@tailwindcss/cli] Optimize CSS') previous.css = css - previous.optimizedCss = optimized.code - if (optimized.map) { - map = toSourceMap(optimized.map) - } - output = optimized.code + previous.optimizedCss = optimizedCss + output = optimizedCss } else { output = previous.optimizedCss } } // Write the output - if (map) { - // Inline the source map - if (args['--map'] === true) { - output += `\n` - output += map.inline - } else if (typeof args['--map'] === 'string') { - let basePath = - args['--output'] && args['--output'] !== '-' - ? path.dirname(path.resolve(args['--output'])) - : process.cwd() - - let mapPath = path.resolve(args['--map']) - - let relativePath = path.relative(basePath, mapPath) - - output += `\n` - output += map.comment(relativePath) - - DEBUG && I.start('Write source map') - await outputFile(args['--map'], map.raw) - DEBUG && I.end('Write source map') - } - } - - let didChange = output !== previous.output - - DEBUG && I.start('Write output') - if (args['--output'] && args['--output'] !== '-') { + env.DEBUG && console.time('[@tailwindcss/cli] Write output') + if (args['--output']) { await outputFile(args['--output'], output) - } else if (didChange) { + } else { println(output) } - DEBUG && I.end('Write output') - - previous.output = output - return didChange + env.DEBUG && console.timeEnd('[@tailwindcss/cli] Write output') } - let inputFilePath = - args['--input'] && args['--input'] !== '-' ? path.resolve(args['--input']) : null + let inputBasePath = + args['--input'] && args['--input'] !== '-' + ? path.dirname(path.resolve(args['--input'])) + : process.cwd() + let fullRebuildPaths: string[] = [] - let inputBasePath = inputFilePath ? path.dirname(inputFilePath) : process.cwd() - - let fullRebuildPaths: string[] = inputFilePath ? [inputFilePath] : [] - let backupRebuildPaths = fullRebuildPaths - - async function createCompiler(css: string, I: Instrumentation) { - DEBUG && I.start('Setup compiler') + async function createCompiler(css: string) { + env.DEBUG && console.time('[@tailwindcss/cli] Setup compiler') let compiler = await compile(css, { - from: args['--output'] ? (inputFilePath ?? 'stdin.css') : undefined, base: inputBasePath, onDependency(path) { fullRebuildPaths.push(path) }, }) - - 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) - - // Do not scan the current executable. Otherwise when using the standalone - // CLI, if the CLI lives in the current repo we would be scanning that file. - // - // This is also immune against renames of the executable file. - sources.push({ - base: path.dirname(process.execPath), - pattern: path.basename(process.execPath), - negated: true, - }) - - if (inputFilePath !== null) { - sources.push({ - base: path.dirname(inputFilePath), - pattern: path.basename(inputFilePath), - negated: false, - }) - } - - let scanner = new Scanner({ sources }) - DEBUG && I.end('Setup compiler') - - return [compiler, scanner] as const + env.DEBUG && console.timeEnd('[@tailwindcss/cli] Setup compiler') + return compiler } - let [compiler, scanner] = await handleError(() => createCompiler(input, I)) - let cleanupWatchers: (() => Promise)[] = [] + // Compile the input + let compiler = await createCompiler(input) + let scanner = new Scanner({ + detectSources: { base }, + sources: compiler.globs, + }) // Watch for changes - if (args['--watch'] && pollInterval === false) { - // Ensure the file watcher can be loaded before setting up any watchers, - // such that we can present a helpful error message if needed. - await handleError(() => loadWatcher()) - - cleanupWatchers.push( - await createWatchers(await watchDirectories(scanner), async function handle(files) { + if (args['--watch']) { + let cleanupWatchers = await createWatchers( + watchDirectories(base, scanner), + async function handle(files) { try { // If the only change happened to the output file, then we don't want to // trigger a rebuild because that will result in an infinite loop. if (files.length === 1 && files[0] === args['--output']) return - using I = new Instrumentation() - DEBUG && I.start('[@tailwindcss/cli] (watcher)') + let changedFiles: ChangedContent[] = [] + let rebuildStrategy: 'incremental' | 'full' = 'incremental' + + let resolvedFullRebuildPaths = fullRebuildPaths + + for (let file of files) { + // If one of the changed files is related to the input CSS or JS + // config/plugin files, then we need to do a full rebuild because + // the theme might have changed. + if (resolvedFullRebuildPaths.includes(file)) { + rebuildStrategy = 'full' + + // No need to check the rest of the events, because we already know we + // need to do a full rebuild. + break + } + + // Track new and updated files for incremental rebuilds. + changedFiles.push({ + file, + extension: path.extname(file).slice(1), + } satisfies ChangedContent) + } // Re-compile the input let start = process.hrtime.bigint() - let resolvedFullRebuildPaths = fullRebuildPaths - let rebuildStrategy = getRebuildStrategy(files, resolvedFullRebuildPaths) - // Track the compiled CSS let compiledCss = '' - let compiledMap: SourceMap | null = null // Scan the entire `base` directory for full rebuilds. - if (rebuildStrategy.kind === 'full') { + if (rebuildStrategy === 'full') { + // Clear all watchers + cleanupWatchers() + // Read the new `input`. let input = args['--input'] ? args['--input'] === '-' @@ -364,134 +201,70 @@ export async function handle(args: Result>) { @import 'tailwindcss'; ` clearRequireCache(resolvedFullRebuildPaths) - - // Track current rebuild paths in case something goes wrong when - // performing a full rebuild. - backupRebuildPaths = fullRebuildPaths.slice() - - // The `inputFilePath`, if provided, will be the only known full - // rebuild path before the compiler is re-created. - fullRebuildPaths = inputFilePath ? [inputFilePath] : [] + fullRebuildPaths = [] // Create a new compiler, given the new `input` - ;[compiler, scanner] = await createCompiler(input, I) + compiler = await createCompiler(input) - // Successfully created a new compiler, so the `fullRebuildPaths` - // will be updated. If other errors occur, we should be able to - // restore the paths unconditionally. - backupRebuildPaths = fullRebuildPaths.slice() + // Re-scan the directory to get the new `candidates` + scanner = new Scanner({ + detectSources: { base }, + sources: compiler.globs, + }) // Scan the directory for candidates - DEBUG && I.start('Scan for candidates') + env.DEBUG && console.time('[@tailwindcss/cli] Scan for candidates') let candidates = scanner.scan() - DEBUG && I.end('Scan for candidates') + env.DEBUG && console.timeEnd('[@tailwindcss/cli] Scan for candidates') // Setup new watchers - DEBUG && I.start('Setup new watchers') - let newCleanupFunction = await createWatchers(await watchDirectories(scanner), handle) - DEBUG && I.end('Setup new watchers') - - // Clear old watchers - DEBUG && I.start('Cleanup old watchers') - await Promise.all(cleanupWatchers.splice(0).map((cleanup) => cleanup())) - DEBUG && I.end('Cleanup old watchers') - - cleanupWatchers.push(newCleanupFunction) + cleanupWatchers = await createWatchers(watchDirectories(base, scanner), handle) // Re-compile the CSS - DEBUG && I.start('Build CSS') + env.DEBUG && console.time('[@tailwindcss/cli] Build CSS') compiledCss = compiler.build(candidates) - DEBUG && I.end('Build CSS') - - if (args['--map']) { - DEBUG && I.start('Build Source Map') - compiledMap = toSourceMap(compiler.buildSourceMap()) - DEBUG && I.end('Build Source Map') - } + env.DEBUG && console.timeEnd('[@tailwindcss/cli] Build CSS') } // Scan changed files only for incremental rebuilds. - else if (rebuildStrategy.kind === 'incremental') { - DEBUG && I.start('Scan for candidates') - let newCandidates = scanner.scanFiles(rebuildStrategy.changedFiles) - DEBUG && I.end('Scan for candidates') + else if (rebuildStrategy === 'incremental') { + env.DEBUG && console.time('[@tailwindcss/cli] Scan for candidates') + let newCandidates = scanner.scanFiles(changedFiles) + env.DEBUG && console.timeEnd('[@tailwindcss/cli] Scan for candidates') // No new candidates found which means we don't need to write to // disk, and can return early. if (newCandidates.length <= 0) { let end = process.hrtime.bigint() - if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`) + eprintln(`Done in ${formatDuration(end - start)}`) return } - DEBUG && I.start('Build CSS') + env.DEBUG && console.time('[@tailwindcss/cli] Build CSS') compiledCss = compiler.build(newCandidates) - DEBUG && I.end('Build CSS') - - if (args['--map']) { - DEBUG && I.start('Build Source Map') - compiledMap = toSourceMap(compiler.buildSourceMap()) - DEBUG && I.end('Build Source Map') - } + env.DEBUG && console.timeEnd('[@tailwindcss/cli] Build CSS') } - await write(compiledCss, compiledMap, args, I) + await write(compiledCss, args) let end = process.hrtime.bigint() - if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`) + eprintln(`Done in ${formatDuration(end - start)}`) } catch (err) { - // It's important that we perform a full rebuild when any of the - // dependencies tracked in `fullRebuildPaths` has been changed. - // - // If we remove one of those files, then a subsequent build will be - // triggered, but it will fail because the dependency is gone. The - // compiler itself will be in a broken state and won't be able to - // register any dependencies therefore we want to restore all the - // dependencies from before. If we don't do that, then we won't be - // able to recover from a bug in a transitive dependency. - // - // E.g.: - // ```css - // /* input.css — known full rebuild path */ - // @import 'tailwindcss'; - // @config "./tailwind.config.js"; - // ``` - // - // ```js - // // tailwind.config.js - // const theme = require('./my-theme.js'); - // - // module.exports = { - // theme - // } - // ``` - // In this case `my-theme.js` is a transitive dependency of - // `input.css` via `tailwind.config.js`. Removing `my-theme.js` will - // result in an error, restoring `my-theme.js` should trigger a - // fresh build even though the compiler didn't restore. - // - // Once the build error is fixed, a fresh full rebuild will happen - // which in turn will fixup the full rebuild paths. - fullRebuildPaths = backupRebuildPaths - // Catch any errors and print them to stderr, but don't exit the process // and keep watching. - eprintln(formatError(err)) - - let end = process.hrtime.bigint() - if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`) + if (err instanceof Error) { + eprintln(err.toString()) + } } - }), + }, ) // Abort the watcher if `stdin` is closed to avoid zombie processes. You can // disable this behavior with `--watch=always`. if (args['--watch'] !== 'always') { process.stdin.on('end', () => { - Promise.all(cleanupWatchers.map((fn) => fn())).then( - () => process.exit(0), - () => process.exit(1), - ) + cleanupWatchers() + process.exit(0) }) } @@ -499,226 +272,36 @@ export async function handle(args: Result>) { process.stdin.resume() } - DEBUG && I.start('Scan for candidates') + env.DEBUG && console.time('[@tailwindcss/cli] Scan for candidates') let candidates = scanner.scan() - DEBUG && I.end('Scan for candidates') - DEBUG && I.start('Build CSS') - let output = await handleError(() => compiler.build(candidates)) - DEBUG && I.end('Build CSS') - - let map: SourceMap | null = null - - if (args['--map']) { - DEBUG && I.start('Build Source Map') - map = await handleError(() => toSourceMap(compiler.buildSourceMap())) - DEBUG && I.end('Build Source Map') - } - - await write(output, map, args, I) + env.DEBUG && console.timeEnd('[@tailwindcss/cli] Scan for candidates') + env.DEBUG && console.time('[@tailwindcss/cli] Build CSS') + let output = compiler.build(candidates) + env.DEBUG && console.timeEnd('[@tailwindcss/cli] Build CSS') + await write(output, args) let end = process.hrtime.bigint() - if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`) - - // Watch for changes, using polling - if (args['--watch'] && pollInterval !== false) { - let cleanupPollingIndicator = () => {} - - function logPollingMessage(message: string) { - if (!args['--silent']) { - process.stderr.write('\r\x1B[2K') - } - - eprintln(message) - - if (!args['--silent']) { - process.stderr.write(`\r\x1B[2K${dim(`Polling for changes…`)}`) - } - } - - async function fullRebuild(I: Instrumentation) { - clearRequireCache(fullRebuildPaths) - backupRebuildPaths = fullRebuildPaths.slice() - fullRebuildPaths = inputFilePath ? [inputFilePath] : [] - - let input = args['--input'] - ? args['--input'] === '-' - ? await drainStdin() - : await fs.readFile(args['--input'], 'utf-8') - : css` - @import 'tailwindcss'; - ` - - ;[compiler, scanner] = await createCompiler(input, I) - backupRebuildPaths = fullRebuildPaths.slice() - - DEBUG && I.start('Scan for candidates') - let candidates = scanner.scan() - DEBUG && I.end('Scan for candidates') - - DEBUG && I.start('Build CSS') - let output = compiler.build(candidates) - DEBUG && I.end('Build CSS') - - let map: SourceMap | null = null - - if (args['--map']) { - DEBUG && I.start('Build Source Map') - map = toSourceMap(compiler.buildSourceMap()) - DEBUG && I.end('Build Source Map') - } - - await write(output, map, args, I) - } - - if (!args['--silent']) { - let restored = false - - function restoreCursor() { - if (restored) return - restored = true - process.stderr.write('\r\x1B[2K\x1B[?25h') - process.off('exit', restoreCursor) - process.off('SIGINT', onSigint) - process.off('SIGTERM', onSigterm) - } - - function onSigint() { - restoreCursor() - process.exit(130) - } - - function onSigterm() { - restoreCursor() - process.exit(143) - } - - process.stderr.write('\x1B[?25l') - process.on('exit', restoreCursor) - process.on('SIGINT', onSigint) - process.on('SIGTERM', onSigterm) - - cleanupPollingIndicator = restoreCursor - cleanupWatchers.push(async () => cleanupPollingIndicator()) - } - - cleanupWatchers.push( - createPollingWatcher(async () => { - using I = new Instrumentation() - - DEBUG && I.start('Scan for candidates') - let candidates = scanner.scan() - DEBUG && I.end('Scan for candidates') - - let files = scanner.scannedFiles.filter( - (file) => file !== args['--output'] && file !== args['--map'], - ) - - if (files.length <= 0) return - - let start = process.hrtime.bigint() - - let strategy = getRebuildStrategy(files, fullRebuildPaths) - - if (strategy.kind === 'full') { - try { - await fullRebuild(I) - } catch (err) { - fullRebuildPaths = backupRebuildPaths - - logPollingMessage(formatError(err)) - } - - if (!args['--silent']) { - logPollingMessage(`Done in ${formatDuration(process.hrtime.bigint() - start)}`) - } - return - } - - // No candidates found which means we don't need to write to disk, and - // can return early. - if (candidates.length <= 0) { - if (!args['--silent']) { - logPollingMessage(`Done in ${formatDuration(process.hrtime.bigint() - start)}`) - } - return - } - - DEBUG && I.start('Build CSS') - let output = compiler.build(candidates) - DEBUG && I.end('Build CSS') - - let map: SourceMap | null = null - - if (args['--map']) { - DEBUG && I.start('Build Source Map') - map = toSourceMap(compiler.buildSourceMap()) - DEBUG && I.end('Build Source Map') - } - - await write(output, map, args, I) - - if (!args['--silent']) { - logPollingMessage(`Done in ${formatDuration(process.hrtime.bigint() - start)}`) - } - }, pollInterval), - ) - - if (args['--watch'] !== 'always') { - process.stdin.on('end', () => { - Promise.all(cleanupWatchers.map((fn) => fn())).then( - () => process.exit(0), - () => process.exit(1), - ) - }) - } - - process.stdin.resume() - if (!args['--silent']) { - process.stderr.write(`\r\x1B[2K${dim(`Polling for changes…`)}`) - } - } + eprintln(header()) + eprintln() + eprintln(`Done in ${formatDuration(end - start)}`) } -// Load `@parcel/watcher` lazily so a missing or broken native binding only -// affects `--watch` (without `--poll`), instead of crashing one-off builds and -// polling mode as well. -async function loadWatcher(): Promise { - try { - return (await import('@parcel/watcher')).default - } catch (err) { - throw new Error( - `Failed to load the file watcher. Your platform may not be supported by \`@parcel/watcher\`. As a workaround, you can use polling instead by passing the \`--watch --poll\` flags.`, - { cause: err }, - ) - } +function watchDirectories(base: string, scanner: Scanner) { + return [base].concat( + scanner.globs.flatMap((globEntry) => { + // We don't want a watcher for negated globs. + if (globEntry.pattern[0] === '!') return [] + + // We don't want a watcher for nested directories, these will be covered + // by the `base` directory already. + if (globEntry.base.startsWith(base)) return [] + + return globEntry.base + }), + ) } async function createWatchers(dirs: string[], cb: (files: string[]) => void) { - let watcher = await loadWatcher() - - // Remove any directories that are children of an already watched directory. - // If we don't we may not get notified of certain filesystem events regardless - // of whether or not they are for the directory that is duplicated. - - // 1. Sort in asc by length - dirs = dirs.sort((a, z) => a.length - z.length) - - // 2. Remove any directories that are children of another directory - let toRemove = [] - - // /project-a 0 - // /project-a/src 1 - - for (let i = 0; i < dirs.length; ++i) { - for (let j = 0; j < i; ++j) { - if (!dirs[i].startsWith(`${dirs[j]}/`)) continue - - toRemove.push(dirs[i]) - } - } - - dirs = dirs.filter((dir) => !toRemove.includes(dir)) - // Track all Parcel watchers for each glob. // // When we encounter a change in a CSS file, we need to setup new watchers and @@ -734,9 +317,9 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) { // A changed file can be watched by multiple watchers, but we only want to // handle the file once. We debounce the handle function with the collected // files to handle them in a single batch and to avoid multiple rebuilds. - async function enqueueCallback() { - // Dispose all existing macrotasks. - await debounceQueue.dispose() + function enqueueCallback() { + // Dispose all existing macrotask. + debounceQueue.dispose() // Setup a new macrotask to handle the files in batch. debounceQueue.queueMacrotask(() => { @@ -757,16 +340,10 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) { await Promise.all( events.map(async (event) => { - // When a file is deleted, a rebuild should be triggered such that we - // can figure out whether this file must trigger a fresh build or not. - // - // If it must trigger a fresh build, then we will temporarily end up - // in a broken state, but an error will be shown to the user. Once the - // user resolves the issue, the CLI will recover. - if (event.type === 'delete') { - files.add(event.path) - return - } + // We currently don't handle deleted files because it doesn't influence + // the CSS output. This is because we currently keep all scanned + // candidates in a cache for performance reasons. + if (event.type === 'delete') return // Ignore directory changes. We only care about file changes let stats: Stats | null = null @@ -783,7 +360,7 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) { ) // Handle the tracked files at some point in the future. - await enqueueCallback() + enqueueCallback() }) // Ensure we cleanup the watcher when we're done. @@ -791,84 +368,32 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) { } // Cleanup - return async () => { - await watchers.dispose() - await debounceQueue.dispose() + return () => { + watchers.dispose() + debounceQueue.dispose() } } -function getRebuildStrategy( - files: string[], - fullRebuildPaths: string[], -): { kind: 'incremental'; changedFiles: ChangedContent[] } | { kind: 'full' } { - let changedFiles: ChangedContent[] = [] - - for (let file of files) { - // If one of the changed files is related to the input CSS or JS - // config/plugin files, then we need to do a full rebuild because - // the theme might have changed. - if (fullRebuildPaths.includes(file)) { - return { kind: 'full' } - } - - // Track new and updated files for incremental rebuilds. - changedFiles.push({ - file, - extension: path.extname(file).slice(1), - } satisfies ChangedContent) - } - - return { kind: 'incremental', changedFiles } -} - -function createPollingWatcher(cb: () => Promise, pollInterval: number) { - let disposed = false - let timer: ReturnType | null = null - - async function poll() { - if (disposed) return - - try { - await cb() - } catch (err) { - console.error(err) - } finally { - if (!disposed) { - timer = setTimeout(poll, pollInterval) - } - } - } - - timer = setTimeout(poll, pollInterval) - - return async () => { - disposed = true - if (timer) clearTimeout(timer) - } -} - -async function watchDirectories(scanner: Scanner) { - let directories = ( - await Promise.all( - scanner.normalizedSources.map(async (globEntry) => { - let resolvedPath = path.resolve(globEntry.base) - let realPath = await fs.realpath(resolvedPath).catch(() => resolvedPath) - - return fs - .stat(realPath) - .then((stat) => (stat.isDirectory() ? [realPath] : [])) - .catch(() => []) - }), - ) - ).flat(1) - - return Array.from(new Set(directories)) -} - -function dim(str: string) { - return `\x1B[2m${str}\x1B[22m` -} - -function red(str: string) { - return `\x1B[31m${str}\x1B[39m` +function optimizeCss( + input: string, + { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {}, +) { + return transform({ + filename: file, + code: Buffer.from(input), + minify, + sourceMap: false, + drafts: { + customMedia: true, + }, + nonStandard: { + deepSelectorCombinator: true, + }, + include: Features.Nesting, + exclude: Features.LogicalProperties, + targets: { + safari: (16 << 16) | (4 << 8), + }, + errorRecovery: true, + }).code.toString() } diff --git a/packages/@tailwindcss-cli/src/commands/build/utils.ts b/packages/@tailwindcss-cli/src/commands/build/utils.ts index ecdd96726..b41a74358 100644 --- a/packages/@tailwindcss-cli/src/commands/build/utils.ts +++ b/packages/@tailwindcss-cli/src/commands/build/utils.ts @@ -13,19 +13,10 @@ export function drainStdin() { } export async function outputFile(file: string, contents: string) { - // Check for special files like `/dev/stdout` or pipes. We don't want to read from these as that - // will hang the process until the file descriptors are closed. - let isSpecialFile = await fs - .stat(file) - .then((stats) => stats.isCharacterDevice() || stats.isFIFO()) - .catch(() => false) - - if (!isSpecialFile) { - try { - let currentContents = await fs.readFile(file, 'utf8') - if (currentContents === contents) return // Skip writing the file - } catch {} - } + try { + let currentContents = await fs.readFile(file, 'utf8') + if (currentContents === contents) return // Skip writing the file + } catch {} // Ensure the parent directories exist await fs.mkdir(path.dirname(file), { recursive: true }) diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts b/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts deleted file mode 100644 index 4392380a5..000000000 --- a/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import path from 'node:path' -import { PassThrough, Readable } from 'node:stream' -import { fileURLToPath } from 'node:url' -import { describe, expect, test } from 'vitest' -import { runCommandLine, streamStdin } from '.' -import { normalizeWindowsSeparators } from '../../utils/test-helpers' - -let css = normalizeWindowsSeparators( - path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'fixtures/input.css'), -) - -describe('runCommandLine', { timeout: 30_000 }, () => { - test('canonicalizes, collapses, and sorts candidate groups from positional arguments', async () => { - let result = await runCommandLine({ - argv: ['--css', css, 'py-3 p-1 px-3'], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result).toEqual({ - exitCode: 0, - stdout: 'p-3', - stderr: '', - }) - }) - - test('falls back to the default tailwind import when --css is omitted', async () => { - let result = await runCommandLine({ - argv: ['py-3 p-1 px-3'], - cwd: path.dirname(css), - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result).toEqual({ - exitCode: 0, - stdout: 'p-3', - stderr: '', - }) - }) - - test('canonicalizes, collapses, and sorts multiple groups from stdin lines', async () => { - let result = await runCommandLine({ - argv: ['--css', css], - stdin: '[display:_flex_] py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2 focus:hover:p-3 hover:p-1 py-3\n', - stdinIsTTY: false, - stdoutIsTTY: false, - }) - - expect(result).toEqual({ - exitCode: 0, - stdout: 'flex p-3\nm-2 py-3 hover:p-1 focus:hover:p-3', - stderr: '', - }) - }) - - test('collapses equivalent candidates', async () => { - let result = await runCommandLine({ - argv: ['--css', css, 'mt-2 mr-2 mb-2 ml-2'], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result).toEqual({ - exitCode: 0, - stdout: 'm-2', - stderr: '', - }) - }) - - test('renders json output for processed candidate groups', async () => { - let result = await runCommandLine({ - argv: ['--css', css, '--format', 'json', 'py-3 p-1 px-3'], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result.exitCode).toBe(0) - expect(JSON.parse(result.stdout)).toEqual([ - { - input: 'py-3 p-1 px-3', - output: 'p-3', - changed: true, - }, - ]) - expect(result.stderr).toBe('') - }) - - test('splits candidate lists with segment-aware spacing', async () => { - let result = await runCommandLine({ - argv: ['--css', css, '--format', 'json', "content-['hello world'] p-1"], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result.exitCode).toBe(0) - expect(JSON.parse(result.stdout)).toEqual([ - { - input: "content-['hello world'] p-1", - output: "p-1 content-['hello_world']", - changed: true, - }, - ]) - expect(result.stderr).toBe('') - }) - - test('shows a usage error when no candidate groups are provided', async () => { - let result = await runCommandLine({ - argv: ['--css', css], - stdinIsTTY: true, - stdoutIsTTY: false, - }) - - expect(result.exitCode).toBe(1) - expect(result.stderr).toBe('No candidate groups provided') - expect(result.stdout).toContain('Usage:') - }) - - test('streams canonicalized output line by line', async () => { - let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n') - let { stream: output, collect: collectOutput } = createOutput() - - await streamStdin({ css, cwd: path.dirname(css), format: 'text', input, output }) - - expect(collectOutput()).toBe('p-3\nm-2\n') - }) - - test('streams empty lines as empty lines', async () => { - let input = Readable.from('py-3 p-1 px-3\n\nmt-2 mr-2 mb-2 ml-2\n') - let { stream: output, collect: collectOutput } = createOutput() - - await streamStdin({ css, cwd: path.dirname(css), format: 'text', input, output }) - - expect(collectOutput()).toBe('p-3\n\nm-2\n') - }) - - test('streams json output when requested', async () => { - let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n') - let { stream: output, collect: collectOutput } = createOutput() - - await streamStdin({ css, cwd: path.dirname(css), format: 'json', input, output }) - - expect(JSON.parse(collectOutput())).toEqual([ - { - input: 'py-3 p-1 px-3', - output: 'p-3', - changed: true, - }, - { - input: 'mt-2 mr-2 mb-2 ml-2', - output: 'm-2', - changed: true, - }, - ]) - }) - - test('streams jsonl output when requested', async () => { - let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n') - let { stream: output, collect: collectOutput } = createOutput() - - await streamStdin({ css, cwd: path.dirname(css), format: 'jsonl', input, output }) - - expect(collectOutput()).toBe( - '{"input":"py-3 p-1 px-3","output":"p-3","changed":true}\n' + - '{"input":"mt-2 mr-2 mb-2 ml-2","output":"m-2","changed":true}\n', - ) - }) -}) - -function createOutput() { - let stream = new PassThrough() - let chunks: Buffer[] = [] - stream.on('data', (chunk) => chunks.push(chunk)) - return { stream, collect: () => Buffer.concat(chunks).toString() } -} diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts b/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts deleted file mode 100644 index 5aecfe51e..000000000 --- a/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import fs from 'node:fs/promises' -import path from 'node:path' -import { createInterface } from 'node:readline' -import type { Readable, Writable } from 'node:stream' -import { compare } from '../../../../tailwindcss/src/utils/compare' -import { segment } from '../../../../tailwindcss/src/utils/segment' -import { args, type Arg } from '../../utils/args' -import { help } from '../help' - -const css = String.raw -export type OutputFormat = 'text' | 'json' | 'jsonl' - -export interface CandidateGroupResult { - input: string - output: string - changed: boolean -} - -export interface RunCommandLineOptions { - argv?: string[] - cwd?: string - stdin?: string | null - stdinIsTTY?: boolean - stdoutIsTTY?: boolean -} - -export interface RunCommandLineResult { - exitCode: number - stdout: string - stderr: string -} - -export function usage() { - return 'tailwindcss canonicalize [classes...]' -} - -function usageWithCss() { - return 'tailwindcss canonicalize --css input.css [classes...]' -} - -function usageWithStream() { - return 'tailwindcss canonicalize --stream [--css input.css]' -} - -export function options() { - return { - '--css': { - type: 'string', - description: - 'CSS entry file used to load the Tailwind design system (defaults to `@import "tailwindcss";`)', - }, - '--format': { - type: 'string', - description: 'Output format', - default: 'text', - values: ['text', 'json', 'jsonl'], - }, - '--stream': { - type: 'boolean', - description: 'Read candidate groups from stdin line by line and write results to stdout', - default: false, - }, - } satisfies Arg -} - -const sharedOptions = { - '--help': { - type: 'boolean', - description: 'Display usage information', - alias: '-h', - default: false, - }, -} satisfies Arg - -export async function runCommandLine({ - argv = process.argv.slice(2), - cwd = process.cwd(), - stdin = null, - stdinIsTTY = process.stdin.isTTY, - stdoutIsTTY = process.stdout.isTTY, -}: RunCommandLineOptions = {}): Promise { - try { - let flags = args( - { - ...options(), - ...sharedOptions, - }, - argv, - ) - - let format = parseFormat(flags['--format'] ?? 'text') - - if ((stdoutIsTTY && argv.length === 0) || flags['--help']) { - return { - exitCode: 0, - stdout: helpMessage() ?? '', - stderr: '', - } - } - - if (flags['--stream']) { - await streamStdin({ - css: flags['--css'], - cwd, - format, - input: process.stdin, - output: process.stdout, - }) - return { exitCode: 0, stdout: '', stderr: '' } - } - - let inputs = flags._.length > 0 ? flags._ : await readCandidateGroups({ stdin, stdinIsTTY }) - - if (inputs.length === 0) { - return usageError('No candidate groups provided') - } - - let results = await processCandidateGroups({ - css: flags['--css'], - cwd, - inputs, - }) - - return { - exitCode: 0, - stdout: formatCandidateResults(results, format), - stderr: '', - } - } catch (error) { - return { - exitCode: 1, - stdout: '', - stderr: error instanceof Error ? error.message : String(error), - } - } -} - -export async function streamStdin({ - css: cssFile, - cwd, - format, - input, - output, -}: { - css: string | null - cwd: string - format: OutputFormat - input: Readable - output: Writable -}): Promise { - let designSystem = await loadDesignSystem(cssFile, cwd) - let rl = createInterface({ input }) - let first = true - - if (format === 'json') { - output.write('[') - } - - for await (let line of rl) { - let result = createCandidateGroupResult(designSystem, line) - - switch (format) { - case 'text': { - output.write(result.output + '\n') - break - } - - case 'jsonl': { - output.write(JSON.stringify(result) + '\n') - break - } - - case 'json': { - if (first) { - output.write('\n') - } else { - output.write(',\n') - } - - output.write(indent(JSON.stringify(result, null, 2), 2)) - first = false - break - } - } - } - - if (format === 'json') { - output.write(first ? ']' : '\n]') - } -} - -export function readCandidateGroups({ - stdin, - stdinIsTTY, -}: { - stdin: string | null - stdinIsTTY: boolean -}) { - if (stdin !== null) { - return Promise.resolve(splitCandidateGroups(stdin)) - } - - if (stdinIsTTY) { - return Promise.resolve([]) - } - - return drainStdin().then(splitCandidateGroups) -} - -export async function drainStdin() { - return new Promise((resolve, reject) => { - let result = '' - - process.stdin.on('data', (chunk) => { - result += chunk - }) - - process.stdin.on('end', () => resolve(result)) - process.stdin.on('error', (err) => reject(err)) - }) -} - -export async function processCandidateGroups({ - css, - cwd = process.cwd(), - inputs, -}: { - css: string | null - cwd?: string - inputs: string[] -}): Promise { - let designSystem = await loadDesignSystem(css, cwd) - - return inputs.map((input) => createCandidateGroupResult(designSystem, input)) -} - -export function formatCandidateResults(results: CandidateGroupResult[], format: OutputFormat) { - switch (format) { - case 'json': - return JSON.stringify(results, null, 2) - case 'jsonl': - return results.map((result) => JSON.stringify(result)).join('\n') - case 'text': - return results.map((result) => result.output).join('\n') - } -} - -function helpMessage() { - return help({ - render: false, - usage: [usage(), usageWithCss(), usageWithStream()], - options: { - ...options(), - ...sharedOptions, - }, - }) -} - -async function loadDesignSystem(cssFile: string | null, cwd: string) { - if (cssFile === null) { - return __unstable__loadDesignSystem( - css` - @import 'tailwindcss'; - `, - { base: cwd }, - ) - } - - let resolvedCssFile = path.resolve(cwd, cssFile) - let content = await fs.readFile(resolvedCssFile, 'utf8') - return __unstable__loadDesignSystem(content, { - base: path.dirname(resolvedCssFile), - }) -} - -function splitCandidateGroups(input: string) { - return input - .split(/\r?\n/g) - .map((line) => line.trim()) - .filter((line) => line.length > 0) -} - -function parseFormat(input: string): OutputFormat { - if (input === 'text' || input === 'json' || input === 'jsonl') { - return input - } - - throw new Error(`Invalid value for --format: ${input}`) -} - -function usageError(message: string): RunCommandLineResult { - return { - exitCode: 1, - stdout: helpMessage() ?? '', - stderr: message, - } -} - -function splitCandidates(input: string) { - let trimmedInput = input.trim() - if (trimmedInput.length === 0) return [] - - return segment(trimmedInput, ' ') - .map((candidate) => candidate.trim()) - .filter((candidate) => candidate.length > 0) -} - -function canonicalize( - designSystem: Awaited>, - input: string, -) { - let candidates = splitCandidates(input) - candidates = designSystem.canonicalizeCandidates(candidates, { - collapse: true, - logicalToPhysical: true, - }) - return defaultSort(designSystem.getClassOrder(candidates)).join(' ') -} - -function createCandidateGroupResult( - designSystem: Awaited>, - input: string, -): CandidateGroupResult { - let output = canonicalize(designSystem, input) - - return { - input, - output, - changed: output !== input, - } -} - -function indent(input: string, size: number) { - let prefix = ' '.repeat(size) - return input - .split('\n') - .map((line) => prefix + line) - .join('\n') -} - -function defaultSort(entries: [string, bigint | null][]) { - return entries - .slice() - .sort(([candidateA, orderA], [candidateZ, orderZ]) => { - if (orderA === orderZ) return compare(candidateA, candidateZ) - if (orderA === null) return -1 - if (orderZ === null) return 1 - return bigSign(orderA - orderZ) || compare(candidateA, candidateZ) - }) - .map(([candidate]) => candidate) -} - -function bigSign(value: bigint) { - if (value > 0n) { - return 1 - } else if (value === 0n) { - return 0 - } else { - return -1 - } -} diff --git a/packages/@tailwindcss-cli/src/commands/help/index.ts b/packages/@tailwindcss-cli/src/commands/help/index.ts index b557d03e0..20ee78208 100644 --- a/packages/@tailwindcss-cli/src/commands/help/index.ts +++ b/packages/@tailwindcss-cli/src/commands/help/index.ts @@ -3,34 +3,30 @@ import type { Arg } from '../../utils/args' import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer' export function help({ - render = true, invalid, usage, options, }: { - render?: boolean invalid?: string usage?: string[] options?: Arg }) { // Available terminal width - let width = process.stdout.columns ?? 80 - let lines: string[] = [] - let writeLine = render ? println : (value = '') => void lines.push(value) + let width = process.stdout.columns // Render header - writeLine(header()) + println(header()) // Render the invalid command if (invalid) { - writeLine() - writeLine(`${pc.dim('Invalid command:')} ${invalid}`) + println() + println(`${pc.dim('Invalid command:')} ${invalid}`) } // Render usage if (usage && usage.length > 0) { - writeLine() - writeLine(pc.dim('Usage:')) + println() + println(pc.dim('Usage:')) for (let [idx, example] of usage.entries()) { // Split the usage example into the command and its options. This allows // us to wrap the options based on the available width of the terminal. @@ -69,7 +65,7 @@ export function help({ // tailwindcss other [--watch] [options...] // ``` if (lines.length > 1 && idx !== 0) { - writeLine() + println() } // Print the usage examples based on available width of the terminal. @@ -90,9 +86,9 @@ export function help({ // ``` // // > Note how the second line is indented to align with the first line. - writeLine(indent(`${command}${lines.shift()}`)) + println(indent(`${command}${lines.shift()}`)) for (let line of lines) { - writeLine(indent(line, command.length)) + println(indent(line, command.length)) } } } @@ -116,11 +112,7 @@ export function help({ // `alias` followed by `, ` and followed by the `flag`. let maxOptionLength = 0 - for (let [flag, { alias, values }] of Object.entries(options)) { - if (values?.length) { - flag += `[=${values.join(', ')}]` - } - + for (let [flag, { alias }] of Object.entries(options)) { // The option string, which is the combination of the alias and the flag // but already properly indented based on the other aliases to ensure // everything is aligned properly. @@ -135,8 +127,8 @@ export function help({ maxOptionLength = Math.max(maxOptionLength, option.length) } - writeLine() - writeLine(pc.dim('Options:')) + println() + println(pc.dim('Options:')) // The minimum amount of dots between the option and the description. let minimumGap = 8 @@ -164,19 +156,15 @@ export function help({ ) // Print the option, the spacer dots and the start of the description. - writeLine( + println( indent(`${pc.blue(option)} ${pc.dim(pc.gray('\u00B7')).repeat(dotCount)} ${lines.shift()}`), ) // Print the remaining lines of the description, indenting them to align // with the start of the description. for (let line of lines) { - writeLine(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`)) + println(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`)) } } } - - if (!render) { - return lines.join('\n') - } } diff --git a/packages/@tailwindcss-cli/src/index.ts b/packages/@tailwindcss-cli/src/index.ts index 1543f7d13..511781759 100644 --- a/packages/@tailwindcss-cli/src/index.ts +++ b/packages/@tailwindcss-cli/src/index.ts @@ -3,80 +3,46 @@ import { args, type Arg } from './utils/args' import * as build from './commands/build' -import * as canonicalize from './commands/canonicalize' import { help } from './commands/help' const sharedOptions = { '--help': { type: 'boolean', description: 'Display usage information', alias: '-h' }, } satisfies Arg -function buildUsage(command = 'tailwindcss') { - return `${command} [--input input.css] [--output output.css] [--watch] [--poll=ms] [options…]` -} +const flags = args({ + ...build.options(), + ...sharedOptions, +}) +const command = flags._[0] -function rootHelp({ invalid }: { invalid?: string } = {}) { +// Right now we don't support any sub-commands. Let's show the help message +// instead. +if (command) { help({ - invalid, - usage: [buildUsage('tailwindcss'), buildUsage('tailwindcss build'), canonicalize.usage()], - commands: { - build: 'Build your CSS', - canonicalize: 'Canonicalize candidate lists', - }, + invalid: command, + usage: ['tailwindcss [options]'], options: { ...build.options(), ...sharedOptions }, }) + process.exit(1) } -async function run() { - let argv = process.argv.slice(2) - let command = argv[0] - let rootFlags = args({ ...build.options(), ...sharedOptions }, argv) - - if (command === 'build') { - let flags = args({ ...build.options(), ...sharedOptions }, argv.slice(1)) - - if ((process.stdout.isTTY && argv.length === 1) || flags['--help']) { - help({ - usage: [buildUsage('tailwindcss build')], - options: { ...build.options(), ...sharedOptions }, - }) - process.exit(0) - } - - await build.handle(flags) - return - } - - if (command === 'canonicalize') { - let result = await canonicalize.runCommandLine({ argv: argv.slice(1) }) - - if (result.stdout.length > 0) { - process.stdout.write(`${result.stdout}\n`) - } - - if (result.stderr.length > 0) { - process.stderr.write(`${result.stderr}\n`) - } - - process.exitCode = result.exitCode - return - } - - if ((process.stdout.isTTY && command === undefined) || rootFlags['--help']) { - rootHelp() - process.exit(0) - } - - if (command && !command.startsWith('-')) { - rootHelp({ invalid: command }) - process.exit(1) - } - - let flags = args({ - ...build.options(), - ...sharedOptions, +// Display main help message if no command is being used. +// +// E.g.: +// +// - `tailwindcss` // should show the help message +// +// E.g.: implicit `build` command +// +// - `tailwindcss -o output.css` // should run the build command, not show the help message +// - `tailwindcss > output.css` // should run the build command, not show the help message +if ((process.stdout.isTTY && !flags['--output']) || flags['--help']) { + help({ + usage: ['tailwindcss [--input input.css] [--output output.css] [--watch] [options…]'], + options: { ...build.options(), ...sharedOptions }, }) - - await build.handle(flags) + process.exit(0) } -await run() +// Handle the build command +build.handle(flags) diff --git a/packages/@tailwindcss-cli/src/utils/args.test.ts b/packages/@tailwindcss-cli/src/utils/args.test.ts index 249fb23eb..51d2c4747 100644 --- a/packages/@tailwindcss-cli/src/utils/args.test.ts +++ b/packages/@tailwindcss-cli/src/utils/args.test.ts @@ -17,38 +17,6 @@ it('should be possible to parse a single argument', () => { `) }) -it('should only return the last value for duplicate arguments', () => { - expect( - args( - { - '--output': { type: 'string', description: 'Output file' }, - }, - ['--output', 'output.css', '--output', 'override.css'], - ), - ).toMatchInlineSnapshot(` - { - "--output": "override.css", - "_": [], - } - `) -}) - -it('uses last value when flag with "-" is supplied multiple times', () => { - let result = args( - { - '--output': { type: 'string', description: 'Output file', alias: '-o' }, - }, - ['--output', 'output.css', '--output', '-'], - ) - - expect(result).toMatchInlineSnapshot(` - { - "--output": "-", - "_": [], - } - `) -}) - it('should fallback to the default value if no flag is passed', () => { expect( args( @@ -153,34 +121,3 @@ it('should be possible to provide multiple types, and convert the value to that } `) }) - -it('should be possible to provide boolean or number arguments', () => { - let options = { - '--poll': { - type: 'boolean | number', - description: 'Use polling instead of filesystem events when watching', - default: false, - }, - } satisfies Arg - - expect(args(options, ['--poll'])).toMatchInlineSnapshot(` - { - "--poll": true, - "_": [], - } - `) - - expect(args(options, ['--poll=50'])).toMatchInlineSnapshot(` - { - "--poll": 50, - "_": [], - } - `) - - expect(args(options, ['--poll=foo'])).toMatchInlineSnapshot(` - { - "--poll": undefined, - "_": [], - } - `) -}) diff --git a/packages/@tailwindcss-cli/src/utils/args.ts b/packages/@tailwindcss-cli/src/utils/args.ts index 88b306b05..81bd847d5 100644 --- a/packages/@tailwindcss-cli/src/utils/args.ts +++ b/packages/@tailwindcss-cli/src/utils/args.ts @@ -7,7 +7,6 @@ export type Arg = { description: string alias?: `-${string}` default?: Types[keyof Types] - values?: string[] } } @@ -68,28 +67,8 @@ export type Result = { } export function args(options: T, argv = process.argv.slice(2)): Result { - for (let [idx, value] of argv.entries()) { - if (value === '-') { - argv[idx] = '__IO_DEFAULT_VALUE__' - } - } - let parsed = parse(argv) - for (let key in parsed) { - let value = parsed[key] - - if (key !== '_' && Array.isArray(value)) { - value = value[value.length - 1] - } - - if (value === '__IO_DEFAULT_VALUE__') { - value = '-' - } - - parsed[key] = value - } - let result: { _: string[]; [key: string]: unknown } = { _: parsed._, } diff --git a/packages/@tailwindcss-cli/src/utils/disposables.ts b/packages/@tailwindcss-cli/src/utils/disposables.ts index 737f3ed1e..cb0d982c1 100644 --- a/packages/@tailwindcss-cli/src/utils/disposables.ts +++ b/packages/@tailwindcss-cli/src/utils/disposables.ts @@ -35,9 +35,9 @@ export class Disposables { /** * Dispose all disposables at once. */ - async dispose() { + dispose() { for (let dispose of this.#disposables) { - await dispose() + dispose() } this.#disposables.clear() diff --git a/packages/@tailwindcss-cli/src/utils/renderer.test.ts b/packages/@tailwindcss-cli/src/utils/renderer.test.ts index 7acef4551..0df4cde83 100644 --- a/packages/@tailwindcss-cli/src/utils/renderer.test.ts +++ b/packages/@tailwindcss-cli/src/utils/renderer.test.ts @@ -1,29 +1,29 @@ -import path from 'node:path' +import path from 'path' import { describe, expect, it } from 'vitest' import { relative, wordWrap } from './renderer' -import { normalizeWindowsSeparators } from './test-helpers' +import { normalizeWindowsSeperators } from './test-helpers' describe('relative', () => { it('should print an absolute path relative to the current working directory', () => { - expect(normalizeWindowsSeparators(relative(path.resolve('index.css')))).toMatchInlineSnapshot( + expect(normalizeWindowsSeperators(relative(path.resolve('index.css')))).toMatchInlineSnapshot( `"./index.css"`, ) }) it('should prefer the shortest value by default', () => { // Shortest between absolute and relative paths - expect(normalizeWindowsSeparators(relative('index.css'))).toMatchInlineSnapshot(`"index.css"`) + expect(normalizeWindowsSeperators(relative('index.css'))).toMatchInlineSnapshot(`"index.css"`) }) it('should be possible to override the current working directory', () => { - expect(normalizeWindowsSeparators(relative('../utils/index.css', '..'))).toMatchInlineSnapshot( + expect(normalizeWindowsSeperators(relative('../utils/index.css', '..'))).toMatchInlineSnapshot( `"./utils/index.css"`, ) }) it('should be possible to always prefer the relative path', () => { expect( - normalizeWindowsSeparators( + normalizeWindowsSeperators( relative('index.css', process.cwd(), { preferAbsoluteIfShorter: false }), ), ).toMatchInlineSnapshot(`"./index.css"`) diff --git a/packages/@tailwindcss-cli/src/utils/renderer.ts b/packages/@tailwindcss-cli/src/utils/renderer.ts index da817dda7..6b0098586 100644 --- a/packages/@tailwindcss-cli/src/utils/renderer.ts +++ b/packages/@tailwindcss-cli/src/utils/renderer.ts @@ -49,9 +49,7 @@ export function wordWrap(text: string, width: number) { for (let word of words) { let wordLength = stripVTControlCharacters(word).length - // A word longer than `width` is kept on its own line rather than - // introducing an empty line before it. - if (lineLength > 0 && lineLength + wordLength + 1 > width) { + if (lineLength + wordLength + 1 > width) { lines.push(line) line = '' lineLength = 0 diff --git a/packages/@tailwindcss-cli/src/utils/test-helpers.ts b/packages/@tailwindcss-cli/src/utils/test-helpers.ts index 30f4fc777..8e61b0a13 100644 --- a/packages/@tailwindcss-cli/src/utils/test-helpers.ts +++ b/packages/@tailwindcss-cli/src/utils/test-helpers.ts @@ -1,5 +1,5 @@ import path from 'node:path' -export function normalizeWindowsSeparators(p: string) { +export function normalizeWindowsSeperators(p: string) { return path.sep === '\\' ? p.replaceAll('\\', '/') : p } diff --git a/packages/@tailwindcss-node/README.md b/packages/@tailwindcss-node/README.md index 5f532607d..95ec9d87d 100644 --- a/packages/@tailwindcss-node/README.md +++ b/packages/@tailwindcss-node/README.md @@ -13,10 +13,10 @@

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

--- @@ -27,10 +27,14 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ## Community -For help, discussion about best practices, or feature ideas: +For help, discussion about best practices, or any other conversation that would benefit from being searchable: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[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) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index 0eced7366..1dd0afca3 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/node", - "version": "4.3.3", + "version": "4.0.0-alpha.26", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -19,46 +19,28 @@ ], "publishConfig": { "provenance": true, - "access": "public", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - }, - "./require-cache": { - "types": "./dist/require-cache.d.ts", - "default": "./dist/require-cache.js" - }, - "./esm-cache-loader": { - "types": "./dist/esm-cache.loader.d.mts", - "default": "./dist/esm-cache.loader.mjs" - } - } + "access": "public" }, "exports": { ".": { - "types": "./src/index.ts", - "import": "./src/index.ts", - "require": "./src/index.cts" + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" }, "./require-cache": { - "types": "./src/require-cache.ts", - "import": "./src/require-cache.ts", - "require": "./src/require-cache.cts" + "types": "./dist/require-cache.d.ts", + "default": "./dist/require-cache.js" }, "./esm-cache-loader": { - "types": "./src/esm-cache.loader.mts", - "default": "./src/esm-cache.loader.mts" + "types": "./dist/esm-cache.loader.d.mts", + "default": "./dist/esm-cache.loader.mjs" } }, + "devDependencies": { + "tailwindcss": "workspace:^" + }, "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "catalog:", - "jiti": "^2.7.0", - "lightningcss": "catalog:", - "magic-string": "^1.1.0", - "source-map-js": "^1.2.1", - "tailwindcss": "workspace:*" + "enhanced-resolve": "^5.17.1", + "jiti": "^2.0.0-beta.3" } } diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index e5f8782c8..c31868ce5 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -2,105 +2,27 @@ import EnhancedResolve from 'enhanced-resolve' import { createJiti, type Jiti } from 'jiti' import fs from 'node:fs' import fsPromises from 'node:fs/promises' -import path from 'node:path' +import path, { dirname, extname } from 'node:path' import { pathToFileURL } from 'node:url' import { __unstable__loadDesignSystem as ___unstable__loadDesignSystem, compile as _compile, - compileAst as _compileAst, - Features, - Polyfills, } from 'tailwindcss' -import type { AstNode } from '../../tailwindcss/src/ast' import { getModuleDependencies } from './get-module-dependencies' -import { rewriteUrls } from './urls' -export { Features, Polyfills } - -export type Resolver = (id: string, base: string) => Promise - -export interface CompileOptions { - base: string - from?: string - onDependency: (path: string) => void - shouldRewriteUrls?: boolean - polyfills?: Polyfills - - customCssResolver?: Resolver - customJsResolver?: Resolver -} - -function createCompileOptions({ - base, - from, - polyfills, - onDependency, - shouldRewriteUrls, - - customCssResolver, - customJsResolver, -}: CompileOptions) { - return { +export function compile( + css: string, + { base, onDependency }: { base: string; onDependency: (path: string) => void }, +) { + return _compile(css, { base, - polyfills, - from, - async loadModule(id: string, base: string) { - return loadModule(id, base, onDependency, customJsResolver) + async loadModule(id, base) { + return loadModule(id, base, onDependency) }, - async loadStylesheet(id: string, sheetBase: string) { - let sheet = await loadStylesheet(id, sheetBase, onDependency, customCssResolver) - - if (shouldRewriteUrls) { - sheet.content = await rewriteUrls({ - css: sheet.content, - root: base, - base: sheet.base, - }) - } - - return sheet + async loadStylesheet(id, base) { + return loadStylesheet(id, base, onDependency) }, - } -} - -async function ensureSourceDetectionRootExists(compiler: { - root: Awaited>['root'] -}) { - // Verify if the `source(…)` path exists (until the glob pattern starts) - if (compiler.root && compiler.root !== 'none') { - let globSymbols = /[*{]/ - let basePath = [] - for (let segment of compiler.root.pattern.split('/')) { - if (globSymbols.test(segment)) { - break - } - - basePath.push(segment) - } - - let exists = await fsPromises - .stat(path.resolve(compiler.root.base, basePath.join('/'))) - .then((stat) => stat.isDirectory()) - .catch(() => false) - - if (!exists) { - throw new Error( - `The \`source(${compiler.root.pattern})\` does not exist or is not a directory.`, - ) - } - } -} - -export async function compileAst(ast: AstNode[], options: CompileOptions) { - let compiler = await _compileAst(ast, createCompileOptions(options)) - await ensureSourceDetectionRootExists(compiler) - return compiler -} - -export async function compile(css: string, options: CompileOptions) { - let compiler = await _compile(css, createCompileOptions(options)) - await ensureSourceDetectionRootExists(compiler) - return compiler + }) } export async function __unstable__loadDesignSystem(css: string, { base }: { base: string }) { @@ -115,31 +37,24 @@ export async function __unstable__loadDesignSystem(css: string, { base }: { base }) } -export async function loadModule( - id: string, - base: string, - onDependency: (path: string) => void, - customJsResolver?: Resolver, -) { +export async function loadModule(id: string, base: string, onDependency: (path: string) => void) { if (id[0] !== '.') { - let resolvedPath = await resolveJsId(id, base, customJsResolver) + let resolvedPath = await resolveJsId(id, base) if (!resolvedPath) { throw new Error(`Could not resolve '${id}' from '${base}'`) } let module = await importModule(pathToFileURL(resolvedPath).href) return { - path: resolvedPath, - base: path.dirname(resolvedPath), + base: dirname(resolvedPath), module: module.default ?? module, } } - let resolvedPath = await resolveJsId(id, base, customJsResolver) + let resolvedPath = await resolveJsId(id, base) if (!resolvedPath) { throw new Error(`Could not resolve '${id}' from '${base}'`) } - let [module, moduleDependencies] = await Promise.all([ importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()), getModuleDependencies(resolvedPath), @@ -149,26 +64,29 @@ export async function loadModule( onDependency(file) } return { - path: resolvedPath, - base: path.dirname(resolvedPath), + base: dirname(resolvedPath), module: module.default ?? module, } } -async function loadStylesheet( - id: string, - base: string, - onDependency: (path: string) => void, - cssResolver?: Resolver, -) { - let resolvedPath = await resolveCssId(id, base, cssResolver) +async function loadStylesheet(id: string, base: string, onDependency: (path: string) => void) { + let resolvedPath = await resolveCssId(id, base) if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${base}'`) onDependency(resolvedPath) + if (typeof globalThis.__tw_readFile === 'function') { + let file = await globalThis.__tw_readFile(resolvedPath, 'utf-8') + if (file) { + return { + base: path.dirname(resolvedPath), + content: file, + } + } + } + let file = await fsPromises.readFile(resolvedPath, 'utf-8') return { - path: resolvedPath, base: path.dirname(resolvedPath), content: file, } @@ -179,39 +97,25 @@ async function loadStylesheet( // can be resolved properly. let jiti: null | Jiti = null async function importModule(path: string): Promise { - if (typeof globalThis.__tw_load === 'function') { - let module = await globalThis.__tw_load(path) - if (module) { - return module - } - } - try { return await import(path) } catch (error) { - jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false }) - return await jiti.import(path) + try { + jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false }) + return await jiti.import(path) + } catch {} + throw error } } -const modules = [ - 'node_modules', - ...(process.env.NODE_PATH ? [...process.env.NODE_PATH.split(path.delimiter)] : []), -] - const cssResolver = EnhancedResolve.ResolverFactory.createResolver({ fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), useSyncFileSystemCalls: true, extensions: ['.css'], mainFields: ['style'], conditionNames: ['style'], - modules, }) -async function resolveCssId( - id: string, - base: string, - customCssResolver?: Resolver, -): Promise { +async function resolveCssId(id: string, base: string): Promise { if (typeof globalThis.__tw_resolve === 'function') { let resolved = globalThis.__tw_resolve(id, base) if (resolved) { @@ -219,52 +123,31 @@ async function resolveCssId( } } - if (customCssResolver) { - let customResolution = await customCssResolver(id, base) - if (customResolution) { - return customResolution - } + // CSS imports that do not have a dir prefix are considered relative. Since + // the resolver does not account for this, we need to do a first pass with an + // assumed relative import by prefixing `./${path}`. We don't have to do this + // when the path starts with a `.` or when the path has no extension (at which + // case it's likely an npm package and not a relative stylesheet). + let skipRelativeCheck = extname(id) === '' || id.startsWith('.') + + if (!skipRelativeCheck) { + try { + let dotResolved = await runResolver(cssResolver, `./${id}`, base) + if (!dotResolved) throw new Error() + return dotResolved + } catch {} } return runResolver(cssResolver, id, base) } -const esmResolver = EnhancedResolve.ResolverFactory.createResolver({ +const jsResolver = EnhancedResolve.ResolverFactory.createResolver({ fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), useSyncFileSystemCalls: true, - extensions: ['.js', '.json', '.node', '.ts'], - conditionNames: ['node', 'import'], - modules, }) -const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({ - fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), - useSyncFileSystemCalls: true, - extensions: ['.js', '.json', '.node', '.ts'], - conditionNames: ['node', 'require'], - modules, -}) - -async function resolveJsId( - id: string, - base: string, - customJsResolver?: Resolver, -): Promise { - if (typeof globalThis.__tw_resolve === 'function') { - let resolved = globalThis.__tw_resolve(id, base) - if (resolved) { - return Promise.resolve(resolved) - } - } - - if (customJsResolver) { - let customResolution = await customJsResolver(id, base) - if (customResolution) { - return customResolution - } - } - - return runResolver(esmResolver, id, base).catch(() => runResolver(cjsResolver, id, base)) +function resolveJsId(id: string, base: string): Promise { + return runResolver(jsResolver, id, base) } function runResolver( diff --git a/packages/@tailwindcss-node/src/env.ts b/packages/@tailwindcss-node/src/env.ts index 3b1a1b726..517ccc6c5 100644 --- a/packages/@tailwindcss-node/src/env.ts +++ b/packages/@tailwindcss-node/src/env.ts @@ -1,10 +1,6 @@ export const DEBUG = resolveDebug(process.env.DEBUG) function resolveDebug(debug: typeof process.env.DEBUG) { - if (typeof debug === 'boolean') { - return debug - } - if (debug === undefined) { return false } diff --git a/packages/@tailwindcss-node/src/esm-cache.loader.mts b/packages/@tailwindcss-node/src/esm-cache.loader.mts index 96d297a29..edcfc8d77 100644 --- a/packages/@tailwindcss-node/src/esm-cache.loader.mts +++ b/packages/@tailwindcss-node/src/esm-cache.loader.mts @@ -1,40 +1,22 @@ -import { - isBuiltin, - type ResolveFnOutput, - type ResolveHook, - type ResolveHookContext, - type ResolveHookSync, -} from 'node:module' +import { isBuiltin, type ResolveHook } from 'node:module' export let resolve: ResolveHook = async (specifier, context, nextResolve) => { let result = await nextResolve(specifier, context) - return processResolve(context, result) -} -export let resolveSync: ResolveHookSync = (specifier, context, nextResolve) => { - let result = nextResolve(specifier, context) - return processResolve(context, result) -} + if (result.url === import.meta.url) return result + if (isBuiltin(result.url)) return result + if (!context.parentURL) return result -function processResolve(context: ResolveHookContext, result: ResolveFnOutput) { - try { - if (result.url === import.meta.url) return result - if (isBuiltin(result.url)) return result - if (!context.parentURL) return result + let parent = new URL(context.parentURL) - let parent = new URL(context.parentURL) + let id = parent.searchParams.get('id') + if (id === null) return result - let id = parent.searchParams.get('id') - if (id === null) return result + let url = new URL(result.url) + url.searchParams.set('id', id) - let url = new URL(result.url) - url.searchParams.set('id', id) - - return { - ...result, - url: `${url}`, - } - } catch { - return result + return { + ...result, + url: `${url}`, } } diff --git a/packages/@tailwindcss-node/src/index.cts b/packages/@tailwindcss-node/src/index.cts index 9f2788933..da6770954 100644 --- a/packages/@tailwindcss-node/src/index.cts +++ b/packages/@tailwindcss-node/src/index.cts @@ -1,26 +1,16 @@ import * as Module from 'node:module' import { pathToFileURL } from 'node:url' import * as env from './env' -import { resolveSync } from './esm-cache.loader.mjs' export * from './compile' -export * from './instrumentation' export * from './normalize-path' -export * from './optimize' -export * from './source-maps' export { env } // In Bun, ESM modules will also populate `require.cache`, so the module hook is // not necessary. if (!process.versions.bun) { // `Module#register` was added in Node v18.19.0 and v20.6.0 - // `Module#registerHooks` was added in Node v22.15.0 and v23.5.0 and is the preferred API since v25.9.0, - // runtime-deprecating `Module#register` since v26 // // Not calling it means that while ESM dependencies don't get reloaded, the // actual included files will because they cache bust directly via `?id=…` - if (Module.registerHooks) { - Module.registerHooks({ resolve: resolveSync }) - } else { - Module.register?.(pathToFileURL(require.resolve('@tailwindcss/node/esm-cache-loader'))) - } + Module.register?.(pathToFileURL(require.resolve('@tailwindcss/node/esm-cache-loader'))) } diff --git a/packages/@tailwindcss-node/src/index.ts b/packages/@tailwindcss-node/src/index.ts index 8eff1466a..eb287ae82 100644 --- a/packages/@tailwindcss-node/src/index.ts +++ b/packages/@tailwindcss-node/src/index.ts @@ -1,27 +1,18 @@ import * as Module from 'node:module' import { pathToFileURL } from 'node:url' import * as env from './env' -import { resolveSync } from './esm-cache.loader.mjs' -export * from './compile' -export * from './instrumentation' +export { __unstable__loadDesignSystem, compile } from './compile' export * from './normalize-path' -export * from './optimize' -export * from './source-maps' export { env } // In Bun, ESM modules will also populate `require.cache`, so the module hook is // not necessary. if (!process.versions.bun) { + let localRequire = Module.createRequire(import.meta.url) + // `Module#register` was added in Node v18.19.0 and v20.6.0 - // `Module#registerHooks` was added in Node v22.15.0 and v23.5.0 and is the preferred API since v25.9.0, - // runtime-deprecating `Module#register` since v26 // // Not calling it means that while ESM dependencies don't get reloaded, the // actual included files will because they cache bust directly via `?id=…` - if (Module.registerHooks) { - Module.registerHooks({ resolve: resolveSync }) - } else { - let localRequire = Module.createRequire(import.meta.url) - Module.register?.(pathToFileURL(localRequire.resolve('@tailwindcss/node/esm-cache-loader'))) - } + Module.register?.(pathToFileURL(localRequire.resolve('@tailwindcss/node/esm-cache-loader'))) } diff --git a/packages/@tailwindcss-node/src/instrumentation.test.ts b/packages/@tailwindcss-node/src/instrumentation.test.ts deleted file mode 100644 index 102c7ed8e..000000000 --- a/packages/@tailwindcss-node/src/instrumentation.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { stripVTControlCharacters } from 'util' -import { expect, it } from 'vitest' -import { Instrumentation } from './instrumentation' - -it('should add instrumentation using start/end markers', () => { - let I = new Instrumentation() - - I.start('Foo') - let x = 1 - for (let i = 0; i < 100; i++) { - I.start('Bar') - x **= 2 - I.end('Bar') - } - I.end('Foo') - - I.hit('Potato') - I.hit('Potato') - I.hit('Potato') - I.hit('Potato') - - expect.assertions(1) - - I.report((output) => { - expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(` - " - Hits: - Potato × 4 - - Timers: - [0.xxms] Foo - [0.xxms] ↳ Bar × 100 - " - `) - }) -}) - -it('should measure callbacks via the `span` api', () => { - let I = new Instrumentation() - - I.span('Foo', () => { - let x = 1 - for (let i = 0; i < 100; i++) { - I.span('Bar', () => { - x **= 2 - }) - } - }) - - expect.assertions(1) - - I.report((output) => { - expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(` - " - [0.xxms] Foo - [0.xxms] ↳ Bar × 100 - " - `) - }) -}) - -it('should measure async callbacks via the `span` api', async () => { - let I = new Instrumentation() - - await I.span('Foo', async () => { - let x = 1 - for (let i = 0; i < 100; i++) { - I.span('Bar', () => { - x **= 2 - }) - } - await new Promise((r) => setTimeout(r, 500)) - }) - - expect.assertions(2) - - I.report((output) => { - expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(` - " - [0.xxms] Foo - [0.xxms] ↳ Bar × 100 - " - `) - - let [, duration] = stripVTControlCharacters(output).match(/\[(.*)\]/)! - expect(parseFloat(duration)).toBeGreaterThanOrEqual(500 - 2 /* Margin of error */) - }) -}) - -it('should measure blocks until they go out of scope via `using`', () => { - let I = new Instrumentation() - - { - using _ = I.track('Foo') - - let x = 1 - for (let i = 0; i < 100; i++) { - using _ = I.track('Bar') - x **= 2 - } - } - - expect.assertions(1) - - I.report((output) => { - expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(` - " - [0.xxms] Foo - [0.xxms] ↳ Bar × 100 - " - `) - }) -}) - -it('should auto end pending timers when reporting', () => { - let I = new Instrumentation() - - I.start('Foo') - let x = 1 - for (let i = 0; i < 100; i++) { - I.start('Bar') - x **= 2 - I.end('Bar') - } - I.start('Baz') - - expect.assertions(1) - - I.report((output) => { - expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(` - " - [0.xxms] Foo - [0.xxms] ↳ Bar × 100 - [0.xxms] ↳ Baz - " - `) - }) -}) diff --git a/packages/@tailwindcss-node/src/instrumentation.ts b/packages/@tailwindcss-node/src/instrumentation.ts deleted file mode 100644 index e454e6744..000000000 --- a/packages/@tailwindcss-node/src/instrumentation.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { DefaultMap } from '../../tailwindcss/src/utils/default-map' -import * as env from './env' - -// See: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#:~:text=Symbol.dispose,-??=%20Symbol(%22Symbol.dispose -// @ts-expect-error — Ensure Symbol.dispose exists -Symbol.dispose ??= Symbol('Symbol.dispose') -// @ts-expect-error — Ensure Symbol.asyncDispose exists -Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose') - -export class Instrumentation implements Disposable { - #hits = new DefaultMap(() => ({ value: 0 })) - #timers = new DefaultMap(() => ({ value: 0n })) - #timerStack: { id: string; label: string; namespace: string; value: bigint }[] = [] - - constructor( - private shouldReport = env.DEBUG, - private defaultFlush = (message: string) => void process.stderr.write(`${message}\n`), - ) {} - - hit(label: string) { - this.#hits.get(label).value++ - } - - start(label: string) { - let namespace = this.#timerStack.map((t) => t.label).join('//') - let id = `${namespace}${namespace.length === 0 ? '' : '//'}${label}` - - this.#hits.get(id).value++ - - // Create the timer if it doesn't exist yet - this.#timers.get(id) - - this.#timerStack.push({ id, label, namespace, value: process.hrtime.bigint() }) - } - - end(label: string) { - let end = process.hrtime.bigint() - - if (this.#timerStack[this.#timerStack.length - 1].label !== label) { - throw new Error( - `Mismatched timer label: \`${label}\`, expected \`${ - this.#timerStack[this.#timerStack.length - 1].label - }\``, - ) - } - - let parent = this.#timerStack.pop()! - let elapsed = end - parent.value - this.#timers.get(parent.id).value += elapsed - } - - track(label: string) { - this.start(label) - let done = false - - return { - [Symbol.dispose]: () => { - if (!done) { - this.end(label) - done = true - } - }, - [Symbol.asyncDispose]: () => { - if (!done) { - this.end(label) - done = true - } - }, - } - } - - span(label: string, fn: () => T): T { - this.start(label) - let isPromise = false - try { - let result = fn() - - isPromise = result && typeof (result as any).then === 'function' - - // @ts-expect-error — TS can't infer that result is a Promise here - return isPromise ? result.finally(() => this.end(label)) : result - } finally { - if (!isPromise) this.end(label) - } - } - - reset() { - this.#hits.clear() - this.#timers.clear() - this.#timerStack.splice(0) - } - - report(flush = this.defaultFlush) { - let output: string[] = [] - let hasHits = false - - // Auto end any pending timers - for (let i = this.#timerStack.length - 1; i >= 0; i--) { - this.end(this.#timerStack[i].label) - } - - for (let [label, { value: count }] of this.#hits.entries()) { - if (this.#timers.has(label)) continue - if (output.length === 0) { - hasHits = true - output.push('Hits:') - } - - let depth = label.split('//').length - output.push(`${' '.repeat(depth)}${label} ${dim(blue(`× ${count}`))}`) - } - - if (this.#timers.size > 0 && hasHits) { - output.push('\nTimers:') - } - - let max = -Infinity - let computed = new Map() - for (let [label, { value }] of this.#timers) { - let x = `${(Number(value) / 1e6).toFixed(2)}ms` - computed.set(label, x) - max = Math.max(max, x.length) - } - - for (let label of this.#timers.keys()) { - let depth = label.split('//').length - output.push( - `${dim(`[${computed.get(label)!.padStart(max, ' ')}]`)}${' '.repeat(depth - 1)}${depth === 1 ? ' ' : dim(' ↳ ')}${label.split('//').pop()} ${ - this.#hits.get(label).value === 1 ? '' : dim(blue(`× ${this.#hits.get(label).value}`)) - }`.trimEnd(), - ) - } - - flush(`\n${output.join('\n')}\n`) - this.reset() - } - - [Symbol.dispose]() { - this.shouldReport && this.report() - } -} - -function dim(input: string) { - return `\u001b[2m${input}\u001b[22m` -} - -function blue(input: string) { - return `\u001b[34m${input}\u001b[39m` -} diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts deleted file mode 100644 index 2f0d39589..000000000 --- a/packages/@tailwindcss-node/src/optimize.ts +++ /dev/null @@ -1,155 +0,0 @@ -import remapping from '@jridgewell/remapping' -import { Features, transform } from 'lightningcss' -import MagicString from 'magic-string' - -export interface OptimizeOptions { - /** - * The file being transformed - */ - file?: string - - /** - * Enabled minified output - */ - minify?: boolean - - /** - * The output source map before optimization - * - * If omitted a resulting source map will not be available - */ - map?: string -} - -export interface TransformResult { - code: string - map: string | undefined -} - -export function optimize( - input: string, - { file = 'input.css', minify = false, map }: OptimizeOptions = {}, -): TransformResult { - function optimize(code: Buffer | Uint8Array, map: string | undefined) { - return transform({ - filename: file, - code, - minify, - sourceMap: typeof map !== 'undefined', - inputSourceMap: map, - drafts: { - customMedia: true, - }, - nonStandard: { - deepSelectorCombinator: true, - }, - include: Features.Nesting | Features.MediaQueries, - exclude: Features.LogicalProperties | Features.DirSelector | Features.LightDark, - targets: { - safari: (16 << 16) | (4 << 8), - ios_saf: (16 << 16) | (4 << 8), - firefox: 128 << 16, - chrome: 111 << 16, - }, - errorRecovery: true, - }) - } - - // Running Lightning CSS twice to ensure that adjacent rules are merged after - // nesting is applied. This creates a more optimized output. - let result = optimize(Buffer.from(input), map) - map = result.map?.toString() - - result.warnings = result.warnings.filter((warning) => { - // Ignore warnings about unknown pseudo-classes that are used in frameworks - // such as Vue or Angular and are handled by their own compilers, before - // reaching the browser. - if ( - /'(deep|slotted|global|ng-deep|host-context)' is not recognized as a valid pseudo-/.test( - warning.message, - ) - ) { - return false - } - - return true - }) - - // Because of `errorRecovery: true`, there could be warnings, so let's let the - // user know about them. - if (process.env.NODE_ENV !== 'test' && result.warnings.length > 0) { - let lines = input.split('\n') - - let output = [ - `Found ${result.warnings.length} ${result.warnings.length === 1 ? 'warning' : 'warnings'} while optimizing generated CSS:`, - ] - - for (let [idx, warning] of result.warnings.entries()) { - output.push('') - if (result.warnings.length > 1) { - output.push(`Issue #${idx + 1}:`) - } - - let context = 2 - - let start = Math.max(0, warning.loc.line - context - 1) - let end = Math.min(lines.length, warning.loc.line + context) - - let snippet = lines.slice(start, end).map((line, idx) => { - if (start + idx + 1 === warning.loc.line) { - return `${dim(`\u2502`)} ${line}` - } else { - return dim(`\u2502 ${line}`) - } - }) - - snippet.splice( - warning.loc.line - start, - 0, - `${dim('\u2506')}${' '.repeat(warning.loc.column - 1)} ${yellow(`${dim('^--')} ${warning.message}`)}`, - `${dim('\u2506')}`, - ) - - output.push(...snippet) - } - output.push('') - - console.warn(output.join('\n')) - } - - result = optimize(result.code, map) - map = result.map?.toString() - - let code = result.code.toString() - - // Work around an issue where the media query range syntax transpilation - // generates code that is invalid with `@media` queries level 3. - let magic = new MagicString(code) - magic.replaceAll('@media not (', '@media not all and (') - - // We have to use a source-map-preserving method of replacing the content - // which requires the use of Magic String + remapping(…) to make sure - // the resulting map is correct - if (map !== undefined && magic.hasChanged()) { - let magicMap = magic.generateMap({ source: 'original', hires: 'boundary' }).toString() - - let remapped = remapping([magicMap, map], () => null) - - map = remapped.toString() - } - - code = magic.toString() - - return { - code, - map, - } -} - -function dim(str: string) { - return `\x1B[2m${str}\x1B[22m` -} - -function yellow(str: string) { - return `\x1B[33m${str}\x1B[39m` -} diff --git a/packages/@tailwindcss-node/src/require-cache.ts b/packages/@tailwindcss-node/src/require-cache.ts deleted file mode 100644 index ea0562019..000000000 --- a/packages/@tailwindcss-node/src/require-cache.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function clearRequireCache(files: string[]) { - for (let key of files) { - delete require.cache[key] - } -} diff --git a/packages/@tailwindcss-node/src/source-maps.test.ts b/packages/@tailwindcss-node/src/source-maps.test.ts deleted file mode 100644 index 96ff8f05c..000000000 --- a/packages/@tailwindcss-node/src/source-maps.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expect, it } from 'vitest' -import { toSourceMap } from './source-maps' - -it('should emit source maps', () => { - let map = toSourceMap('{"version":3,"sources":[],"names":[],"mappings":""}') - - expect(map.comment('app.css.map')).toBe('/*# sourceMappingURL=app.css.map */\n') -}) diff --git a/packages/@tailwindcss-node/src/source-maps.ts b/packages/@tailwindcss-node/src/source-maps.ts deleted file mode 100644 index 58b79cb25..000000000 --- a/packages/@tailwindcss-node/src/source-maps.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { SourceMapGenerator } from 'source-map-js' -import type { DecodedSource, DecodedSourceMap } from '../../tailwindcss/src/source-maps/source-map' -import { DefaultMap } from '../../tailwindcss/src/utils/default-map' - -export type { DecodedSource, DecodedSourceMap } -export interface SourceMap { - readonly raw: string - readonly inline: string - comment(url: string): string -} - -function serializeSourceMap(map: DecodedSourceMap): string { - let generator = new SourceMapGenerator() - - let id = 1 - let sourceTable = new DefaultMap< - DecodedSource | null, - { - url: string - content: string - } - >((src) => { - return { - url: src?.url ?? ``, - content: src?.content ?? '', - } - }) - - for (let mapping of map.mappings) { - let original = sourceTable.get(mapping.originalPosition?.source ?? null) - - generator.addMapping({ - generated: mapping.generatedPosition, - original: mapping.originalPosition, - source: original.url, - name: mapping.name, - }) - - generator.setSourceContent(original.url, original.content) - } - - return generator.toString() -} - -export function toSourceMap(map: DecodedSourceMap | string): SourceMap { - let raw = typeof map === 'string' ? map : serializeSourceMap(map) - - function comment(url: string) { - return `/*# sourceMappingURL\x3d${url} */\n` - } - - return { - raw, - get inline() { - let inlined = Buffer.from(raw, 'utf-8').toString('base64') - return comment(`data:application/json;base64,${inlined}`) - }, - comment, - } -} diff --git a/packages/@tailwindcss-node/src/urls.test.ts b/packages/@tailwindcss-node/src/urls.test.ts deleted file mode 100644 index 16ba352a7..000000000 --- a/packages/@tailwindcss-node/src/urls.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { expect, test } from 'vitest' -import { rewriteUrls } from './urls' - -const css = String.raw - -test('URLs can be rewritten', async () => { - let root = '/root' - - let result = await rewriteUrls({ - root, - base: '/root/foo/bar', - // prettier-ignore - css: css` - .foo { - /* Relative URLs: replaced */ - background: url(./image.jpg); - background: url(../image.jpg); - background: url('./image.jpg'); - background: url("./image.jpg"); - - /* Absolute URLs: ignored */ - background: url(/image.jpg); - background: url(/foo/image.jpg); - background: url('/image.jpg'); - background: url("/image.jpg"); - - /* Potentially Vite-aliased URLs: ignored */ - background: url(~/image.jpg); - background: url(~/foo/image.jpg); - background: url('~/image.jpg'); - background: url("~/image.jpg"); - background: url(#/image.jpg); - background: url(#/foo/image.jpg); - background: url('#/image.jpg'); - background: url("#/image.jpg"); - background: url(@/image.jpg); - background: url(@/foo/image.jpg); - background: url('@/image.jpg'); - background: url("@/image.jpg"); - - /* External URL: ignored */ - background: url(http://example.com/image.jpg); - background: url('http://example.com/image.jpg'); - background: url("http://example.com/image.jpg"); - - /* Data URI: ignored */ - /* background: url(data:image/png;base64,abc==); */ - background: url('data:image/png;base64,abc=='); - background: url("data:image/png;base64,abc=="); - - /* Function calls: ignored */ - background: url(var(--foo)); - background: url(var(--foo, './image.jpg')); - background: url(var(--foo, "./image.jpg")); - - /* Fragments: ignored */ - background: url(#dont-touch-this); - - /* Image Sets - Raw URL: replaced */ - background: image-set( - image1.jpg 1x, - image2.jpg 2x - ); - background: image-set( - 'image1.jpg' 1x, - 'image2.jpg' 2x - ); - background: image-set( - "image1.jpg" 1x, - "image2.jpg" 2x - ); - - /* Image Sets - Relative URLs: replaced */ - background: image-set( - url('image1.jpg') 1x, - url('image2.jpg') 2x - ); - background: image-set( - url("image1.jpg") 1x, - url("image2.jpg") 2x - ); - background: image-set( - url('image1.avif') type('image/avif'), - url('image2.jpg') type('image/jpeg') - ); - background: image-set( - url("image1.avif") type('image/avif'), - url("image2.jpg") type('image/jpeg') - ); - - /* Image Sets - Function calls: ignored */ - background: image-set( - linear-gradient(blue, white) 1x, - linear-gradient(blue, green) 2x - ); - - /* Image Sets - Mixed: replaced */ - background: image-set( - linear-gradient(blue, white) 1x, - url("image2.jpg") 2x - ); - } - - /* Fonts - Multiple URLs: replaced */ - @font-face { - font-family: "Newman"; - src: - local("Newman"), - url("newman-COLRv1.otf") format("opentype") tech(color-COLRv1), - url("newman-outline.otf") format("opentype"), - url("newman-outline.woff") format("woff"); - } - `, - }) - - expect(result).toMatchInlineSnapshot(` - ".foo { - background: url(./foo/bar/image.jpg); - background: url(./foo/image.jpg); - background: url('./foo/bar/image.jpg'); - background: url("./foo/bar/image.jpg"); - background: url(/image.jpg); - background: url(/foo/image.jpg); - background: url('/image.jpg'); - background: url("/image.jpg"); - background: url(~/image.jpg); - background: url(~/foo/image.jpg); - background: url('~/image.jpg'); - background: url("~/image.jpg"); - background: url(#/image.jpg); - background: url(#/foo/image.jpg); - background: url('#/image.jpg'); - background: url("#/image.jpg"); - background: url(@/image.jpg); - background: url(@/foo/image.jpg); - background: url('@/image.jpg'); - background: url("@/image.jpg"); - background: url(http://example.com/image.jpg); - background: url('http://example.com/image.jpg'); - background: url("http://example.com/image.jpg"); - background: url('data:image/png;base64,abc=='); - background: url("data:image/png;base64,abc=="); - background: url(var(--foo)); - background: url(var(--foo, './image.jpg')); - background: url(var(--foo, "./image.jpg")); - background: url(#dont-touch-this); - background: image-set(url(./foo/bar/image1.jpg) 1x, url(./foo/bar/image2.jpg) 2x); - background: image-set(url('./foo/bar/image1.jpg') 1x, url('./foo/bar/image2.jpg') 2x); - background: image-set(url("./foo/bar/image1.jpg") 1x, url("./foo/bar/image2.jpg") 2x); - background: image-set(url('./foo/bar/image1.jpg') 1x, url('./foo/bar/image2.jpg') 2x); - background: image-set(url("./foo/bar/image1.jpg") 1x, url("./foo/bar/image2.jpg") 2x); - background: image-set(url('./foo/bar/image1.avif') type('image/avif'), url('./foo/bar/image2.jpg') type('image/jpeg')); - background: image-set(url("./foo/bar/image1.avif") type('image/avif'), url("./foo/bar/image2.jpg") type('image/jpeg')); - background: image-set(linear-gradient(blue, white) 1x, linear-gradient(blue, green) 2x); - background: image-set(linear-gradient(blue, white) 1x, url("./foo/bar/image2.jpg") 2x); - } - @font-face { - font-family: "Newman"; - src: local("Newman"), url("./foo/bar/newman-COLRv1.otf") format("opentype") tech(color-COLRv1), url("./foo/bar/newman-outline.otf") format("opentype"), url("./foo/bar/newman-outline.woff") format("woff"); - } - " - `) -}) diff --git a/packages/@tailwindcss-node/src/urls.ts b/packages/@tailwindcss-node/src/urls.ts deleted file mode 100644 index 61695a9d1..000000000 --- a/packages/@tailwindcss-node/src/urls.ts +++ /dev/null @@ -1,207 +0,0 @@ -// Inlined version of code from Vite -// Copyright (c) 2019-present, VoidZero Inc. and Vite contributors -// Released under the MIT License. -// -// Minor modifications have been made to work with the Tailwind CSS codebase - -import * as path from 'node:path' -import { toCss } from '../../tailwindcss/src/ast' -import { parse } from '../../tailwindcss/src/css-parser' -import { walk } from '../../tailwindcss/src/walk' -import { normalizePath } from './normalize-path' - -const cssUrlRE = - /(?[\w-]+\([^)]*\)|"[^"]*"|'[^']*'|[^,]\S*[^,])\s*(?:\s(?\w[^,]+))?(?:,|$)/g -const nonEscapedDoubleQuoteRE = /(? dataUrlRE.test(url) -const isExternalUrl = (url: string): boolean => externalRE.test(url) - -type CssUrlReplacer = (url: string, importer?: string) => string | Promise - -interface ImageCandidate { - url: string - descriptor: string -} - -export async function rewriteUrls({ - css, - base, - root, -}: { - css: string - base: string - root: string -}) { - if (!css.includes('url(') && !css.includes('image-set(')) { - return css - } - - let ast = parse(css) - - let promises: Promise[] = [] - - function replacerForDeclaration(url: string) { - if (url[0] === '/') return url - - let absoluteUrl = path.posix.join(normalizePath(base), url) - let relativeUrl = path.posix.relative(normalizePath(root), absoluteUrl) - - // If the path points to a file in the same directory, `path.relative` will - // remove the leading `./` and we need to add it back in order to still - // consider the path relative - if (!relativeUrl.startsWith('.')) { - relativeUrl = './' + relativeUrl - } - - return relativeUrl - } - - walk(ast, (node) => { - if (node.kind !== 'declaration') return - if (!node.value) return - - let isCssUrl = cssUrlRE.test(node.value) - let isCssImageSet = cssImageSetRE.test(node.value) - - if (isCssUrl || isCssImageSet) { - let rewriterToUse = isCssImageSet ? rewriteCssImageSet : rewriteCssUrls - - promises.push( - rewriterToUse(node.value, replacerForDeclaration).then((url) => { - node.value = url - }), - ) - } - }) - - if (promises.length) { - await Promise.all(promises) - } - - return toCss(ast) -} - -function rewriteCssUrls(css: string, replacer: CssUrlReplacer): Promise { - return asyncReplace(css, cssUrlRE, async (match) => { - const [matched, rawUrl] = match - return await doUrlReplace(rawUrl.trim(), matched, replacer) - }) -} - -async function rewriteCssImageSet(css: string, replacer: CssUrlReplacer): Promise { - return await asyncReplace(css, cssImageSetRE, async (match) => { - const [, rawUrl] = match - const url = await processSrcSet(rawUrl, async ({ url }) => { - // the url maybe url(...) - if (cssUrlRE.test(url)) { - return await rewriteCssUrls(url, replacer) - } - if (!cssNotProcessedRE.test(url)) { - return await doUrlReplace(url, url, replacer) - } - return url - }) - return url - }) -} - -async function doUrlReplace( - rawUrl: string, - matched: string, - replacer: CssUrlReplacer, - funcName: string = 'url', -) { - let wrap = '' - const first = rawUrl[0] - if (first === `"` || first === `'`) { - wrap = first - rawUrl = rawUrl.slice(1, -1) - } - - if (skipUrlReplacer(rawUrl)) { - return matched - } - - let newUrl = await replacer(rawUrl) - // The new url might need wrapping even if the original did not have it, e.g. if a space was added during replacement - if (wrap === '' && newUrl !== encodeURI(newUrl)) { - wrap = '"' - } - // If wrapping in single quotes and newUrl also contains single quotes, switch to double quotes. - // Give preference to double quotes since SVG inlining converts double quotes to single quotes. - if (wrap === "'" && newUrl.includes("'")) { - wrap = '"' - } - // Escape double quotes if they exist (they also tend to be rarer than single quotes) - if (wrap === '"' && newUrl.includes('"')) { - newUrl = newUrl.replace(nonEscapedDoubleQuoteRE, '\\"') - } - return `${funcName}(${wrap}${newUrl}${wrap})` -} - -function skipUrlReplacer(rawUrl: string, aliases?: string[]) { - return ( - isExternalUrl(rawUrl) || - isDataUrl(rawUrl) || - !rawUrl[0].match(/[.a-zA-Z0-9_]/) || - functionCallRE.test(rawUrl) - ) -} - -function processSrcSet( - srcs: string, - replacer: (arg: ImageCandidate) => Promise, -): Promise { - return Promise.all( - parseSrcset(srcs).map(async ({ url, descriptor }) => ({ - url: await replacer({ url, descriptor }), - descriptor, - })), - ).then(joinSrcset) -} - -function parseSrcset(string: string): ImageCandidate[] { - const matches = string - .trim() - .replace(escapedSpaceCharactersRE, ' ') - .replace(/\r?\n/, '') - .replace(/,\s+/, ', ') - .replaceAll(/\s+/g, ' ') - .matchAll(imageCandidateRE) - return Array.from(matches, ({ groups }) => ({ - url: groups?.url?.trim() ?? '', - descriptor: groups?.descriptor?.trim() ?? '', - })).filter(({ url }) => !!url) -} - -function joinSrcset(ret: ImageCandidate[]) { - return ret.map(({ url, descriptor }) => url + (descriptor ? ` ${descriptor}` : '')).join(', ') -} - -async function asyncReplace( - input: string, - re: RegExp, - replacer: (match: RegExpExecArray) => string | Promise, -): Promise { - let match: RegExpExecArray | null - let remaining = input - let rewritten = '' - while ((match = re.exec(remaining))) { - rewritten += remaining.slice(0, match.index) - rewritten += await replacer(match) - remaining = remaining.slice(match.index + match[0].length) - } - rewritten += remaining - return rewritten -} diff --git a/packages/@tailwindcss-node/tsconfig.json b/packages/@tailwindcss-node/tsconfig.json index d2131cbeb..6ae022f65 100644 --- a/packages/@tailwindcss-node/tsconfig.json +++ b/packages/@tailwindcss-node/tsconfig.json @@ -1,6 +1,3 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { - "verbatimModuleSyntax": false, - }, } diff --git a/packages/@tailwindcss-node/tsup.config.ts b/packages/@tailwindcss-node/tsup.config.ts index 3f2d184d5..0f2ac5855 100644 --- a/packages/@tailwindcss-node/tsup.config.ts +++ b/packages/@tailwindcss-node/tsup.config.ts @@ -6,35 +6,23 @@ export default defineConfig([ minify: true, dts: true, entry: ['src/index.cts'], - define: { - 'process.env.NODE_ENV': '"production"', - }, }, { format: ['esm'], minify: true, dts: true, entry: ['src/index.ts'], - define: { - 'process.env.NODE_ENV': '"production"', - }, }, { format: ['esm'], minify: true, dts: true, entry: ['src/esm-cache.loader.mts'], - define: { - 'process.env.NODE_ENV': '"production"', - }, }, { format: ['cjs'], minify: true, dts: true, entry: ['src/require-cache.cts'], - define: { - 'process.env.NODE_ENV': '"production"', - }, }, ]) diff --git a/packages/@tailwindcss-postcss/README.md b/packages/@tailwindcss-postcss/README.md index 3facf2887..95ec9d87d 100644 --- a/packages/@tailwindcss-postcss/README.md +++ b/packages/@tailwindcss-postcss/README.md @@ -13,10 +13,10 @@

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

--- @@ -27,85 +27,14 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ## Community -For help, discussion about best practices, or feature ideas: +For help, discussion about best practices, or any other conversation that would benefit from being searchable: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[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) ## 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/postcss` plugin API - -### Changing where the plugin searches for source files - -You can use the `base` option (defaults to the current working directory) to change the directory in which the plugin searches for source files: - -```js -import tailwindcss from '@tailwindcss/postcss' - -export default { - plugins: [ - tailwindcss({ - base: path.resolve(__dirname, './path'), - }), - ], -} -``` - -### 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/postcss' - -export default { - plugins: [ - tailwindcss({ - // Enable or disable Lightning CSS - optimize: false, - }), - ], -} -``` - -It's also possible to keep Lightning CSS enabled but disable minification: - -```js -import tailwindcss from '@tailwindcss/postcss' - -export default { - plugins: [ - tailwindcss({ - optimize: { minify: false }, - }), - ], -} -``` - -### Enabling or disabling `url(…)` rewriting - -Our PostCSS plugin can rewrite `url(…)`s for you since it also handles `@import` (no `postcss-import` is needed). This feature is enabled by default. - -In some situations the bundler or framework you're using may provide this feature itself. In this case you can set `transformAssetUrls` to `false` to disable this feature: - -```js -import tailwindcss from '@tailwindcss/postcss' - -export default { - plugins: [ - tailwindcss({ - // Disable `url(…)` rewriting - transformAssetUrls: false, - - // Enable `url(…)` rewriting (the default) - transformAssetUrls: true, - }), - ], -} -``` +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-postcss/package.json b/packages/@tailwindcss-postcss/package.json index 891578509..5e43cdf9c 100644 --- a/packages/@tailwindcss-postcss/package.json +++ b/packages/@tailwindcss-postcss/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/postcss", - "version": "4.3.3", + "version": "4.0.0-alpha.26", "description": "PostCSS plugin for Tailwind CSS, a utility-first CSS framework for rapidly building custom user interfaces", "license": "MIT", "repository": { @@ -24,28 +24,23 @@ }, "exports": { ".": { - "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" - }, - "require": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" } }, "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "workspace:*", - "@tailwindcss/oxide": "workspace:*", - "postcss": "catalog:", - "tailwindcss": "workspace:*" + "@tailwindcss/node": "workspace:^", + "@tailwindcss/oxide": "workspace:^", + "lightningcss": "catalog:", + "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", "@types/postcss-import": "14.0.3", - "dedent": "catalog:", "internal-example-plugin": "workspace:*", - "postcss-import": "^16.1.1" + "postcss": "^8.4.41", + "postcss-import": "^16.1.0" } } diff --git a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap index 05087fa39..a3442c5c9 100644 --- a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap +++ b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap @@ -1,27 +1,409 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 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-font-weight: initial; - } - } -} - -@layer theme { - :root, :host { - --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, - sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", - monospace; +"@layer theme { + :root { + --default-transition-duration: .15s; + --default-transition-timing-function: cubic-bezier(.4, 0, .2, 1); + --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); + --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); + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; --color-black: #000; - --text-2xl: 1.5rem; - --text-2xl--line-height: calc(2 / 1.5); - --font-weight-bold: 700; - --default-font-family: var(--font-sans); - --default-mono-font-family: var(--font-mono); + --color-white: #fff; + --color-slate-50: #f8fafc; + --color-slate-100: #f1f5f9; + --color-slate-200: #e2e8f0; + --color-slate-300: #cbd5e1; + --color-slate-400: #94a3b8; + --color-slate-500: #64748b; + --color-slate-600: #475569; + --color-slate-700: #334155; + --color-slate-800: #1e293b; + --color-slate-900: #0f172a; + --color-slate-950: #020617; + --color-gray-50: #f9fafb; + --color-gray-100: #f3f4f6; + --color-gray-200: #e5e7eb; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; + --color-gray-500: #6b7280; + --color-gray-600: #4b5563; + --color-gray-700: #374151; + --color-gray-800: #1f2937; + --color-gray-900: #111827; + --color-gray-950: #030712; + --color-zinc-50: #fafafa; + --color-zinc-100: #f4f4f5; + --color-zinc-200: #e4e4e7; + --color-zinc-300: #d4d4d8; + --color-zinc-400: #a1a1aa; + --color-zinc-500: #71717a; + --color-zinc-600: #52525b; + --color-zinc-700: #3f3f46; + --color-zinc-800: #27272a; + --color-zinc-900: #18181b; + --color-zinc-950: #09090b; + --color-neutral-50: #fafafa; + --color-neutral-100: #f5f5f5; + --color-neutral-200: #e5e5e5; + --color-neutral-300: #d4d4d4; + --color-neutral-400: #a3a3a3; + --color-neutral-500: #737373; + --color-neutral-600: #525252; + --color-neutral-700: #404040; + --color-neutral-800: #262626; + --color-neutral-900: #171717; + --color-neutral-950: #0a0a0a; + --color-stone-50: #fafaf9; + --color-stone-100: #f5f5f4; + --color-stone-200: #e7e5e4; + --color-stone-300: #d6d3d1; + --color-stone-400: #a8a29e; + --color-stone-500: #78716c; + --color-stone-600: #57534e; + --color-stone-700: #44403c; + --color-stone-800: #292524; + --color-stone-900: #1c1917; + --color-stone-950: #0c0a09; + --color-red-50: #fef2f2; + --color-red-100: #fee2e2; + --color-red-200: #fecaca; + --color-red-300: #fca5a5; + --color-red-400: #f87171; + --color-red-500: #ef4444; + --color-red-600: #dc2626; + --color-red-700: #b91c1c; + --color-red-800: #991b1b; + --color-red-900: #7f1d1d; + --color-red-950: #450a0a; + --color-orange-50: #fff7ed; + --color-orange-100: #ffedd5; + --color-orange-200: #fed7aa; + --color-orange-300: #fdba74; + --color-orange-400: #fb923c; + --color-orange-500: #f97316; + --color-orange-600: #ea580c; + --color-orange-700: #c2410c; + --color-orange-800: #9a3412; + --color-orange-900: #7c2d12; + --color-orange-950: #431407; + --color-amber-50: #fffbeb; + --color-amber-100: #fef3c7; + --color-amber-200: #fde68a; + --color-amber-300: #fcd34d; + --color-amber-400: #fbbf24; + --color-amber-500: #f59e0b; + --color-amber-600: #d97706; + --color-amber-700: #b45309; + --color-amber-800: #92400e; + --color-amber-900: #78350f; + --color-amber-950: #451a03; + --color-yellow-50: #fefce8; + --color-yellow-100: #fef9c3; + --color-yellow-200: #fef08a; + --color-yellow-300: #fde047; + --color-yellow-400: #facc15; + --color-yellow-500: #eab308; + --color-yellow-600: #ca8a04; + --color-yellow-700: #a16207; + --color-yellow-800: #854d0e; + --color-yellow-900: #713f12; + --color-yellow-950: #422006; + --color-lime-50: #f7fee7; + --color-lime-100: #ecfccb; + --color-lime-200: #d9f99d; + --color-lime-300: #bef264; + --color-lime-400: #a3e635; + --color-lime-500: #84cc16; + --color-lime-600: #65a30d; + --color-lime-700: #4d7c0f; + --color-lime-800: #3f6212; + --color-lime-900: #365314; + --color-lime-950: #1a2e05; + --color-green-50: #f0fdf4; + --color-green-100: #dcfce7; + --color-green-200: #bbf7d0; + --color-green-300: #86efac; + --color-green-400: #4ade80; + --color-green-500: #22c55e; + --color-green-600: #16a34a; + --color-green-700: #15803d; + --color-green-800: #166534; + --color-green-900: #14532d; + --color-green-950: #052e16; + --color-emerald-50: #ecfdf5; + --color-emerald-100: #d1fae5; + --color-emerald-200: #a7f3d0; + --color-emerald-300: #6ee7b7; + --color-emerald-400: #34d399; + --color-emerald-500: #10b981; + --color-emerald-600: #059669; + --color-emerald-700: #047857; + --color-emerald-800: #065f46; + --color-emerald-900: #064e3b; + --color-emerald-950: #022c22; + --color-teal-50: #f0fdfa; + --color-teal-100: #ccfbf1; + --color-teal-200: #99f6e4; + --color-teal-300: #5eead4; + --color-teal-400: #2dd4bf; + --color-teal-500: #14b8a6; + --color-teal-600: #0d9488; + --color-teal-700: #0f766e; + --color-teal-800: #115e59; + --color-teal-900: #134e4a; + --color-teal-950: #042f2e; + --color-cyan-50: #ecfeff; + --color-cyan-100: #cffafe; + --color-cyan-200: #a5f3fc; + --color-cyan-300: #67e8f9; + --color-cyan-400: #22d3ee; + --color-cyan-500: #06b6d4; + --color-cyan-600: #0891b2; + --color-cyan-700: #0e7490; + --color-cyan-800: #155e75; + --color-cyan-900: #164e63; + --color-cyan-950: #083344; + --color-sky-50: #f0f9ff; + --color-sky-100: #e0f2fe; + --color-sky-200: #bae6fd; + --color-sky-300: #7dd3fc; + --color-sky-400: #38bdf8; + --color-sky-500: #0ea5e9; + --color-sky-600: #0284c7; + --color-sky-700: #0369a1; + --color-sky-800: #075985; + --color-sky-900: #0c4a6e; + --color-sky-950: #082f49; + --color-blue-50: #eff6ff; + --color-blue-100: #dbeafe; + --color-blue-200: #bfdbfe; + --color-blue-300: #93c5fd; + --color-blue-400: #60a5fa; + --color-blue-500: #3b82f6; + --color-blue-600: #2563eb; + --color-blue-700: #1d4ed8; + --color-blue-800: #1e40af; + --color-blue-900: #1e3a8a; + --color-blue-950: #172554; + --color-indigo-50: #eef2ff; + --color-indigo-100: #e0e7ff; + --color-indigo-200: #c7d2fe; + --color-indigo-300: #a5b4fc; + --color-indigo-400: #818cf8; + --color-indigo-500: #6366f1; + --color-indigo-600: #4f46e5; + --color-indigo-700: #4338ca; + --color-indigo-800: #3730a3; + --color-indigo-900: #312e81; + --color-indigo-950: #1e1b4b; + --color-violet-50: #f5f3ff; + --color-violet-100: #ede9fe; + --color-violet-200: #ddd6fe; + --color-violet-300: #c4b5fd; + --color-violet-400: #a78bfa; + --color-violet-500: #8b5cf6; + --color-violet-600: #7c3aed; + --color-violet-700: #6d28d9; + --color-violet-800: #5b21b6; + --color-violet-900: #4c1d95; + --color-violet-950: #2e1065; + --color-purple-50: #faf5ff; + --color-purple-100: #f3e8ff; + --color-purple-200: #e9d5ff; + --color-purple-300: #d8b4fe; + --color-purple-400: #c084fc; + --color-purple-500: #a855f7; + --color-purple-600: #9333ea; + --color-purple-700: #7e22ce; + --color-purple-800: #6b21a8; + --color-purple-900: #581c87; + --color-purple-950: #3b0764; + --color-fuchsia-50: #fdf4ff; + --color-fuchsia-100: #fae8ff; + --color-fuchsia-200: #f5d0fe; + --color-fuchsia-300: #f0abfc; + --color-fuchsia-400: #e879f9; + --color-fuchsia-500: #d946ef; + --color-fuchsia-600: #c026d3; + --color-fuchsia-700: #a21caf; + --color-fuchsia-800: #86198f; + --color-fuchsia-900: #701a75; + --color-fuchsia-950: #4a044e; + --color-pink-50: #fdf2f8; + --color-pink-100: #fce7f3; + --color-pink-200: #fbcfe8; + --color-pink-300: #f9a8d4; + --color-pink-400: #f472b6; + --color-pink-500: #ec4899; + --color-pink-600: #db2777; + --color-pink-700: #be185d; + --color-pink-800: #9d174d; + --color-pink-900: #831843; + --color-pink-950: #500724; + --color-rose-50: #fff1f2; + --color-rose-100: #ffe4e6; + --color-rose-200: #fecdd3; + --color-rose-300: #fda4af; + --color-rose-400: #fb7185; + --color-rose-500: #f43f5e; + --color-rose-600: #e11d48; + --color-rose-700: #be123c; + --color-rose-800: #9f1239; + --color-rose-900: #881337; + --color-rose-950: #4c0519; + --animate-spin: spin 1s linear infinite; + --animate-ping: ping 1s cubic-bezier(0, 0, .2, 1) infinite; + --animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite; + --animate-bounce: bounce 1s infinite; + --blur: 8px; + --blur-sm: 4px; + --blur-md: 12px; + --blur-lg: 16px; + --blur-xl: 24px; + --blur-2xl: 40px; + --blur-3xl: 64px; + --radius: .25rem; + --radius-sm: .125rem; + --radius-md: .375rem; + --radius-lg: .5rem; + --radius-xl: .75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --radius-4xl: 2rem; + --shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a; + --shadow-xs: 0 1px #0000000d; + --shadow-sm: 0 1px 2px 0 #0000000d; + --shadow-md: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a; + --shadow-lg: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a; + --shadow-xl: 0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a; + --shadow-2xl: 0 25px 50px -12px #00000040; + --shadow-inner: inset 0 2px 4px 0 #0000000d; + --inset-shadow-xs: inset 0 1px #0000000d; + --inset-shadow-sm: inset 0 1px 1px #0000000d; + --inset-shadow: inset 0 2px 4px #0000000d; + --drop-shadow: 0 1px 2px #0000001a, 0 1px 1px #0000000f; + --drop-shadow-sm: 0 1px 1px #0000000d; + --drop-shadow-md: 0 4px 3px #00000012, 0 2px 2px #0000000f; + --drop-shadow-lg: 0 10px 8px #0000000a, 0 4px 3px #0000001a; + --drop-shadow-xl: 0 20px 13px #00000008, 0 8px 5px #00000014; + --drop-shadow-2xl: 0 25px 25px #00000026; + --drop-shadow-none: 0 0 #0000; + --spacing-px: 1px; + --spacing-0: 0px; + --spacing-0_5: .125rem; + --spacing-1: .25rem; + --spacing-1_5: .375rem; + --spacing-2: .5rem; + --spacing-2_5: .625rem; + --spacing-3: .75rem; + --spacing-3_5: .875rem; + --spacing-4: 1rem; + --spacing-5: 1.25rem; + --spacing-6: 1.5rem; + --spacing-7: 1.75rem; + --spacing-8: 2rem; + --spacing-9: 2.25rem; + --spacing-10: 2.5rem; + --spacing-11: 2.75rem; + --spacing-12: 3rem; + --spacing-14: 3.5rem; + --spacing-16: 4rem; + --spacing-20: 5rem; + --spacing-24: 6rem; + --spacing-28: 7rem; + --spacing-32: 8rem; + --spacing-36: 9rem; + --spacing-40: 10rem; + --spacing-44: 11rem; + --spacing-48: 12rem; + --spacing-52: 13rem; + --spacing-56: 14rem; + --spacing-60: 15rem; + --spacing-64: 16rem; + --spacing-72: 18rem; + --spacing-80: 20rem; + --spacing-96: 24rem; + --width-3xs: 16rem; + --width-2xs: 18rem; + --width-xs: 20rem; + --width-sm: 24rem; + --width-md: 28rem; + --width-lg: 32rem; + --width-xl: 36rem; + --width-2xl: 42rem; + --width-3xl: 48rem; + --width-4xl: 56rem; + --width-5xl: 64rem; + --width-6xl: 72rem; + --width-7xl: 80rem; + --width-prose: 65ch; + --font-family-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --font-size-xs: .75rem; + --font-size-xs--line-height: 1rem; + --font-size-sm: .875rem; + --font-size-sm--line-height: 1.25rem; + --font-size-base: 1rem; + --font-size-base--line-height: 1.5rem; + --font-size-lg: 1.125rem; + --font-size-lg--line-height: 1.75rem; + --font-size-xl: 1.25rem; + --font-size-xl--line-height: 1.75rem; + --font-size-2xl: 1.5rem; + --font-size-2xl--line-height: 2rem; + --font-size-3xl: 1.875rem; + --font-size-3xl--line-height: 2.25rem; + --font-size-4xl: 2.25rem; + --font-size-4xl--line-height: 2.5rem; + --font-size-5xl: 3rem; + --font-size-5xl--line-height: 1; + --font-size-6xl: 3.75rem; + --font-size-6xl--line-height: 1; + --font-size-7xl: 4.5rem; + --font-size-7xl--line-height: 1; + --font-size-8xl: 6rem; + --font-size-8xl--line-height: 1; + --font-size-9xl: 8rem; + --font-size-9xl--line-height: 1; + --letter-spacing-tighter: -.05em; + --letter-spacing-tight: -.025em; + --letter-spacing-normal: 0em; + --letter-spacing-wide: .025em; + --letter-spacing-wider: .05em; + --letter-spacing-widest: .1em; + --line-height-none: 1; + --line-height-tight: 1.25; + --line-height-snug: 1.375; + --line-height-normal: 1.5; + --line-height-relaxed: 1.625; + --line-height-loose: 2; + --line-height-3: .75rem; + --line-height-4: 1rem; + --line-height-5: 1.25rem; + --line-height-6: 1.5rem; + --line-height-7: 1.75rem; + --line-height-8: 2rem; + --line-height-9: 2.25rem; + --line-height-10: 2.5rem; + --perspective-dramatic: 100px; + --perspective-near: 300px; + --perspective-normal: 500px; + --perspective-midrange: 800px; + --perspective-distant: 1200px; + --transition-timing-function-linear: linear; + --transition-timing-function-in: cubic-bezier(.4, 0, 1, 1); + --transition-timing-function-out: cubic-bezier(0, 0, .2, 1); + --transition-timing-function-in-out: cubic-bezier(.4, 0, .2, 1); } } @@ -44,16 +426,20 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` -webkit-text-size-adjust: 100%; tab-size: 4; line-height: 1.5; - font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); font-feature-settings: var(--default-font-feature-settings, normal); font-variation-settings: var(--default-font-variation-settings, normal); -webkit-tap-highlight-color: transparent; } + body { + line-height: inherit; + } + hr { - height: 0; color: inherit; border-top-width: 1px; + height: 0; } abbr:where([title]) { @@ -70,7 +456,6 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` color: inherit; -webkit-text-decoration: inherit; -webkit-text-decoration: inherit; - -webkit-text-decoration: inherit; text-decoration: inherit; } @@ -110,14 +495,60 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` border-collapse: collapse; } - :-moz-focusring:where(:not(iframe)) { + button, input, optgroup, select, textarea { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + background: none; + } + + ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + background: none; + } + + input:where(:not([type="button"], [type="reset"], [type="submit"])), select, textarea { + border: 1px solid; + } + + button, input:where([type="button"], [type="reset"], [type="submit"]) { + appearance: button; + } + + ::file-selector-button { + appearance: button; + } + + :-moz-focusring { outline: auto; } + :-moz-ui-invalid { + box-shadow: none; + } + progress { vertical-align: baseline; } + ::-webkit-inner-spin-button { + height: auto; + } + + ::-webkit-outer-spin-button { + height: auto; + } + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + summary { display: list-item; } @@ -126,6 +557,15 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` list-style: none; } + textarea { + resize: vertical; + } + + ::placeholder { + opacity: 1; + color: color-mix(in srgb, currentColor 50%, transparent); + } + img, svg, video, canvas, audio, iframe, embed, object { vertical-align: middle; display: block; @@ -136,137 +576,6 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` height: auto; } - button, input, select, optgroup, textarea { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - opacity: 1; - background-color: #0000; - border-radius: 0; - } - - ::file-selector-button { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - opacity: 1; - background-color: #0000; - border-radius: 0; - } - - :where(select:is([multiple], [size])) optgroup { - font-weight: bolder; - } - - :where(select:is([multiple], [size])) optgroup option { - padding-inline-start: 20px; - } - - ::file-selector-button { - margin-inline-end: 4px; - } - - ::placeholder { - opacity: 1; - } - - @supports (not ((-webkit-appearance: -apple-pay-button))) or (contain-intrinsic-size: 1px) { - ::placeholder { - color: currentColor; - } - - @supports (color: color-mix(in lab, red, red)) { - ::placeholder { - color: color-mix(in oklab, currentcolor 50%, transparent); - } - } - } - - textarea { - resize: vertical; - } - - ::-webkit-search-decoration { - -webkit-appearance: none; - } - - ::-webkit-date-and-time-value { - min-height: 1lh; - text-align: inherit; - } - - ::-webkit-datetime-edit { - display: inline-flex; - } - - ::-webkit-datetime-edit-fields-wrapper { - padding: 0; - } - - ::-webkit-datetime-edit { - padding-block: 0; - } - - ::-webkit-datetime-edit-year-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-month-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-day-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-hour-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-minute-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-second-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-millisecond-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-meridiem-field { - padding-block: 0; - } - - ::-webkit-calendar-picker-indicator { - line-height: 1; - } - - :-moz-ui-invalid { - box-shadow: none; - } - - button, input:where([type="button"], [type="reset"], [type="submit"]) { - appearance: button; - } - - ::file-selector-button { - appearance: button; - } - - ::-webkit-inner-spin-button { - height: auto; - } - - ::-webkit-outer-spin-button { - height: auto; - } - [hidden]:where(:not([hidden="until-found"])) { display: none !important; } @@ -276,35 +585,67 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` @layer utilities { .text-2xl { - font-size: var(--text-2xl); - line-height: var(--tw-leading, var(--text-2xl--line-height)); + font-size: var(--font-size-2xl, 1.5rem); + line-height: var(--tw-leading, var(--font-size-2xl--line-height, 2rem)); } .text-black\\/50 { - color: #00000080; - } - - @supports (color: color-mix(in lab, red, red)) { - .text-black\\/50 { - color: color-mix(in oklab, var(--color-black) 50%, transparent); - } + color: color-mix(in srgb, var(--color-black, #000) 50%, transparent); } .underline { text-decoration-line: underline; } - @media (min-width: 96rem) { + @media (width >= 96rem) { .\\32 xl\\:font-bold { - --tw-font-weight: var(--font-weight-bold); - font-weight: var(--font-weight-bold); + --tw-font-weight: 700; + font-weight: 700; } } } +@supports (-moz-orient: inline) { + @layer base { + *, :before, :after, ::backdrop { + --tw-font-weight: initial; + } + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes ping { + 75%, 100% { + opacity: 0; + transform: scale(2); + } +} + +@keyframes pulse { + 50% { + opacity: .5; + } +} + +@keyframes bounce { + 0%, 100% { + animation-timing-function: cubic-bezier(.8, 0, 1, 1); + transform: translateY(-25%); + } + + 50% { + animation-timing-function: cubic-bezier(0, 0, .2, 1); + transform: none; + } +} + @property --tw-font-weight { syntax: "*"; inherits: false -} -" +}" `; diff --git a/packages/@tailwindcss-postcss/src/ast.test.ts b/packages/@tailwindcss-postcss/src/ast.test.ts deleted file mode 100644 index 8a2a5b019..000000000 --- a/packages/@tailwindcss-postcss/src/ast.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import dedent from 'dedent' -import postcss from 'postcss' -import { expect, it } from 'vitest' -import { toCss } from '../../tailwindcss/src/ast' -import { parse } from '../../tailwindcss/src/css-parser' -import { cssAstToPostCssAst, postCssAstToCssAst } from './ast' - -let css = dedent - -it('should convert a PostCSS AST into a Tailwind CSS AST', () => { - let input = css` - @charset "UTF-8"; - - @layer foo, bar, baz; - - @import 'tailwindcss'; - - .foo { - color: red; - - &:hover { - color: blue; - } - - .bar { - color: green !important; - background-color: yellow; - - @media (min-width: 640px) { - color: orange; - } - } - } - ` - - let ast = postcss.parse(input) - let transformedAst = postCssAstToCssAst(ast) - - expect(toCss(transformedAst)).toMatchInlineSnapshot(` - "@charset "UTF-8"; - @layer foo, bar, baz; - @import 'tailwindcss'; - .foo { - color: red; - &:hover { - color: blue; - } - .bar { - color: green !important; - background-color: yellow; - @media (min-width: 640px) { - color: orange; - } - } - } - " - `) -}) - -it('should convert a Tailwind CSS AST into a PostCSS AST', () => { - let input = css` - @charset "UTF-8"; - - @layer foo, bar, baz; - - @import 'tailwindcss'; - - .foo { - color: red; - - &:hover { - color: blue; - } - - .bar { - color: green !important; - background-color: yellow; - - @media (min-width: 640px) { - color: orange; - } - } - } - ` - - let ast = parse(input) - let transformedAst = cssAstToPostCssAst(postcss, ast) - - expect(transformedAst.toString()).toMatchInlineSnapshot(` - "@charset "UTF-8"; - @layer foo, bar, baz; - @import 'tailwindcss'; - .foo { - color: red; - &:hover { - color: blue; - } - .bar { - color: green !important; - background-color: yellow; - @media (min-width: 640px) { - color: orange; - } - } - }" - `) -}) diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts deleted file mode 100644 index 7ca9e2add..000000000 --- a/packages/@tailwindcss-postcss/src/ast.ts +++ /dev/null @@ -1,189 +0,0 @@ -import type * as postcss from 'postcss' -import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast' -import { createLineTable, type LineTable } from '../../tailwindcss/src/source-maps/line-table' -import type { Source, SourceLocation } from '../../tailwindcss/src/source-maps/source' -import { DefaultMap } from '../../tailwindcss/src/utils/default-map' - -const EXCLAMATION_MARK = 0x21 - -export function cssAstToPostCssAst( - postcss: postcss.Postcss, - ast: AstNode[], - source?: postcss.Source, -): postcss.Root { - let inputMap = new DefaultMap((src) => { - return new postcss.Input(src.code, { - map: source?.input.map, - from: src.file ?? undefined, - }) - }) - - let lineTables = new DefaultMap((src) => createLineTable(src.code)) - - let root = postcss.root() - root.source = source - - function toSource(loc: SourceLocation | undefined): postcss.Source | undefined { - // Use the fallback if this node has no location info in the AST - if (!loc) return - if (!loc[0]) return - - let table = lineTables.get(loc[0]) - let start = table.find(loc[1]) - let end = table.find(loc[2]) - - return { - input: inputMap.get(loc[0]), - start: { - line: start.line, - column: start.column + 1, - offset: loc[1], - }, - end: { - line: end.line, - column: end.column + 1, - offset: loc[2], - }, - } - } - - function updateSource(astNode: postcss.ChildNode, loc: SourceLocation | undefined) { - let source = toSource(loc) - - // The `source` property on PostCSS nodes must be defined if present because - // `toJSON()` reads each property and tries to read from source.input if it - // sees a `source` property. This means for a missing or otherwise absent - // source it must be *missing* from the object rather than just `undefined` - if (source) { - astNode.source = source - } else { - delete astNode.source - } - } - - function transform(node: AstNode, parent: postcss.Container) { - // Declaration - if (node.kind === 'declaration') { - let astNode = postcss.decl({ - prop: node.property, - value: node.value ?? '', - important: node.important, - }) - updateSource(astNode, node.src) - parent.append(astNode) - } - - // Rule - else if (node.kind === 'rule') { - let astNode = postcss.rule({ selector: node.selector }) - updateSource(astNode, node.src) - astNode.raws.semicolon = true - parent.append(astNode) - for (let child of node.nodes) { - transform(child, astNode) - } - } - - // AtRule - else if (node.kind === 'at-rule') { - let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params }) - updateSource(astNode, node.src) - astNode.raws.semicolon = true - parent.append(astNode) - for (let child of node.nodes) { - transform(child, astNode) - } - } - - // Comment - else if (node.kind === 'comment') { - let astNode = postcss.comment({ text: node.value }) - // Spaces are encoded in our node.value already, no need to add additional - // spaces. - astNode.raws.left = '' - astNode.raws.right = '' - updateSource(astNode, node.src) - parent.append(astNode) - } - - // AtRoot & Context should not happen - else if (node.kind === 'at-root' || node.kind === 'context') { - } - - // Unknown - else { - node satisfies never - } - } - - for (let node of ast) { - transform(node, root) - } - - return root -} - -export function postCssAstToCssAst(root: postcss.Root): AstNode[] { - let inputMap = new DefaultMap((input) => ({ - file: input.file ?? input.id ?? null, - code: input.css, - })) - - function toSource(node: postcss.ChildNode): SourceLocation | undefined { - let source = node.source - if (!source) return - - let input = source.input - if (!input) return - if (source.start === undefined) return - if (source.end === undefined) return - - return [inputMap.get(input), source.start.offset, source.end.offset] - } - - function transform( - node: postcss.ChildNode, - parent: Extract['nodes'], - ) { - // Declaration - if (node.type === 'decl') { - let astNode = decl(node.prop, node.value, node.important) - astNode.src = toSource(node) - parent.push(astNode) - } - - // Rule - else if (node.type === 'rule') { - let astNode = rule(node.selector) - astNode.src = toSource(node) - node.each((child) => transform(child, astNode.nodes)) - parent.push(astNode) - } - - // AtRule - else if (node.type === 'atrule') { - let astNode = atRule(`@${node.name}`, node.params) - astNode.src = toSource(node) - node.each((child) => transform(child, astNode.nodes)) - parent.push(astNode) - } - - // Comment - else if (node.type === 'comment') { - if (node.text.charCodeAt(0) !== EXCLAMATION_MARK) return - let astNode = comment(node.text) - astNode.src = toSource(node) - parent.push(astNode) - } - - // Unknown - else { - node satisfies never - } - } - - let ast: AstNode[] = [] - root.each((node) => transform(node, ast)) - - return ast -} diff --git a/packages/@tailwindcss-postcss/src/fixtures/example-project/input.css b/packages/@tailwindcss-postcss/src/fixtures/example-project/input.css deleted file mode 100644 index f1af498fc..000000000 --- a/packages/@tailwindcss-postcss/src/fixtures/example-project/input.css +++ /dev/null @@ -1 +0,0 @@ -/* the content for this file is set in the tests */ diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index b393cc382..ab1c25dd4 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -1,10 +1,7 @@ -import dedent from 'dedent' -import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import path from 'path' +import { unlink, writeFile } from 'node:fs/promises' import postcss from 'postcss' -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { pretty } from '../../tailwindcss/src/test-utils/run' +import { afterEach, beforeEach, describe, expect, test } from 'vitest' +// @ts-ignore import tailwindcss from './index' // We give this file path to PostCSS for processing. @@ -12,32 +9,18 @@ import tailwindcss from './index' // We place it in packages/ because Vitest runs in the monorepo root, // and packages/tailwindcss must be a sub-folder for // @import 'tailwindcss' to work. -function inputCssFilePath() { - // Including the current test name to ensure that the cache is invalidated per - // test otherwise the cache will be used across tests. - return `${__dirname}/fixtures/example-project/input.css?test=${expect.getState().currentTestName}` -} +const INPUT_CSS_PATH = `${__dirname}/fixtures/example-project/input.css` -const css = dedent - -async function run(plugin: any, from: string, input: string): Promise { - let ast = postcss.parse(input) - for (let runner of plugin.plugins) { - if (runner.Once) { - await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } }) - } - } - return ast.toString() -} +const css = String.raw test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => { let processor = postcss([ tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), ]) - let result = await processor.process(`@import 'tailwindcss'`, { from: inputCssFilePath() }) + let result = await processor.process(`@import 'tailwindcss'`, { from: INPUT_CSS_PATH }) - expect(pretty(result.css)).toMatchSnapshot() + expect(result.css.trim()).toMatchSnapshot() // Check for dependency messages expect(result.messages).toContainEqual({ @@ -54,7 +37,7 @@ test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => { }) expect(result.messages).toContainEqual({ type: 'dir-dependency', - dir: expect.stringMatching(/example-project[/|\\]src$/g), + dir: expect.stringMatching(/example-project[\/|\\]src$/g), glob: expect.stringMatching(/^\*\*\/\*/g), parent: expect.any(String), plugin: expect.any(String), @@ -66,6 +49,8 @@ test('output is optimized by Lightning CSS', async () => { tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), ]) + // `@apply` is used because Lightning is skipped if neither `@tailwind` nor + // `@apply` is used. let result = await processor.process( css` @layer utilities { @@ -80,12 +65,11 @@ test('output is optimized by Lightning CSS', async () => { } } `, - { from: inputCssFilePath() }, + { from: INPUT_CSS_PATH }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @layer utilities { + expect(result.css.trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo { color: #000; } @@ -93,8 +77,7 @@ test('output is optimized by Lightning CSS', async () => { .bar { color: red; } - } - " + }" `) }) @@ -103,44 +86,37 @@ test('@apply can be used without emitting the theme in the CSS file', async () = tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), ]) + // `@apply` is used because Lightning is skipped if neither `@tailwind` nor + // `@apply` is used. let result = await processor.process( css` - @reference 'tailwindcss/theme.css'; + @import 'tailwindcss/theme.css' theme(reference); .foo { @apply text-red-500; } `, - { from: inputCssFilePath() }, + { from: INPUT_CSS_PATH }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .foo { - color: var(--color-red-500, oklch(63.7% .237 25.331)); - } - " + expect(result.css.trim()).toMatchInlineSnapshot(` + ".foo { + color: var(--color-red-500, #ef4444); + }" `) }) describe('processing without specifying a base path', () => { - let filepath: string - let dir: string + let filepath = `${process.cwd()}/my-test-file.html` - beforeEach(async () => { - dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss')) - await mkdir(dir, { recursive: true }) - filepath = path.join(dir, 'my-test-file.html') - await writeFile(filepath, `
`) - }) + beforeEach(() => + writeFile(filepath, `
`), + ) afterEach(() => unlink(filepath)) test('the current working directory is used by default', async () => { - using spy = vi.spyOn(process, 'cwd') - spy.mockReturnValue(dir) - let processor = postcss([tailwindcss({ optimize: { minify: false } })]) - let result = await processor.process(`@import "tailwindcss"`, { from: inputCssFilePath() }) + let result = await processor.process(`@import "tailwindcss"`, { from: INPUT_CSS_PATH }) expect(result.css).toContain( ".md\\:\\[\\&\\:hover\\]\\:content-\\[\\'testing_default_base_path\\'\\]", @@ -155,18 +131,6 @@ describe('processing without specifying a base path', () => { }) }) -test('fallback to `base` directory when `result.opts.from` is not provided', async () => { - using _ = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - let processor = postcss([ - tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), - ]) - - let result = await processor.process(`@import 'tailwindcss'`) - - expect(result.css.length).toBeGreaterThan(0) -}) - describe('plugins', () => { test('local CJS plugin', async () => { let processor = postcss([ @@ -178,12 +142,11 @@ describe('plugins', () => { @import 'tailwindcss/utilities'; @plugin './plugin.js'; `, - { from: inputCssFilePath() }, + { from: INPUT_CSS_PATH }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .underline { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { text-decoration-line: underline; } @@ -195,8 +158,7 @@ describe('plugins', () => { .hocus\\:underline:focus, .hocus\\:underline:hover { text-decoration-line: underline; - } - " + }" `) }) @@ -213,9 +175,8 @@ describe('plugins', () => { { from: `${__dirname}/fixtures/another-project/input.css` }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .underline { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { text-decoration-line: underline; } @@ -227,8 +188,7 @@ describe('plugins', () => { .hocus\\:underline:focus, .hocus\\:underline:hover { text-decoration-line: underline; - } - " + }" `) }) @@ -242,12 +202,11 @@ describe('plugins', () => { @import 'tailwindcss/utilities'; @plugin 'internal-example-plugin'; `, - { from: inputCssFilePath() }, + { from: INPUT_CSS_PATH }, ) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .underline { + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { text-decoration-line: underline; } @@ -259,260 +218,7 @@ describe('plugins', () => { .hocus\\:underline:focus, .hocus\\:underline:hover { text-decoration-line: underline; - } - " + }" `) }) }) - -test('bail early when Tailwind is not used', async () => { - let processor = postcss([ - tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), - ]) - - let result = await processor.process( - css` - .custom-css { - color: red; - } - `, - { from: inputCssFilePath() }, - ) - - // `fixtures/example-project` includes an `underline` candidate. But since we - // didn't use `@tailwind utilities` we didn't scan for utilities. - expect(result.css).not.toContain('.underline {') - - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .custom-css { - color: red; - } - " - `) -}) - -test('handle CSS when only using a `@reference` (we should not bail early)', async () => { - let processor = postcss([ - tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), - ]) - - let result = await processor.process( - css` - @reference "tailwindcss/theme.css"; - - .foo { - @variant md { - bar: baz; - } - } - `, - { from: inputCssFilePath() }, - ) - - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @media (min-width: 48rem) { - .foo { - bar: baz; - } - } - " - `) -}) - -test('handle CSS when using a `@variant` using variants that do not rely on the `@theme`', async () => { - let processor = postcss([ - tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), - ]) - - let result = await processor.process( - css` - .foo { - @variant data-is-hoverable { - bar: baz; - } - } - `, - { from: inputCssFilePath() }, - ) - - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .foo[data-is-hoverable] { - bar: baz; - } - " - `) -}) - -test('runs `Once` plugins in the right order', async () => { - let before = '' - let after = '' - let processor = postcss([ - { - postcssPlugin: 'before', - Once(root) { - before = root.toString() - }, - }, - tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), - { - postcssPlugin: 'after', - Once(root) { - after = root.toString() - }, - }, - ]) - - let result = await processor.process( - css` - @theme { - --color-red-500: red; - } - .custom-css { - color: theme(--color-red-500); - } - `, - { from: inputCssFilePath() }, - ) - - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .custom-css { - color: red; - } - " - `) - expect(pretty(before)).toMatchInlineSnapshot(` - " - @theme { - --color-red-500: red; - } - .custom-css { - color: theme(--color-red-500); - } - " - `) - expect(pretty(after)).toMatchInlineSnapshot(` - " - .custom-css { - color: red; - } - " - `) -}) - -describe('concurrent builds', () => { - let dir: string - beforeEach(async () => { - dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss')) - await writeFile(path.join(dir, 'index.html'), `
`) - await writeFile( - path.join(dir, 'index.css'), - css` - @import './dependency.css'; - `, - ) - await writeFile( - path.join(dir, 'dependency.css'), - css` - @tailwind utilities; - `, - ) - }) - afterEach(() => rm(dir, { recursive: true, force: true })) - - test('does experience a race-condition when calling the plugin two times for the same change', async () => { - using spy = vi.spyOn(process, 'cwd') - spy.mockReturnValue(dir) - - let from = path.join(dir, 'index.css') - let input = (await readFile(path.join(dir, 'index.css'))).toString() - - let plugin = tailwindcss({ optimize: { minify: false } }) - - let result = await run(plugin, from, input) - - expect(result).toContain('.underline') - - // Ensure that the mtime is updated - await new Promise((resolve) => setTimeout(resolve, 100)) - await writeFile( - path.join(dir, 'dependency.css'), - css` - @tailwind utilities; - .red { - color: red; - } - `, - ) - - let promise1 = run(plugin, from, input) - let promise2 = run(plugin, from, input) - - expect(await promise1).toContain('.red') - expect(await promise2).toContain('.red') - }) -}) - -test('rebuilds when the input CSS changes even if the `from` file on disk did not', async () => { - let dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss')) - // The `from` file exists on disk with a stable mtime. The input CSS is handed - // to the plugin directly (as a preprocessor like Sass would produce it), so it - // can change without `from`'s mtime ever changing. - await writeFile(path.join(dir, 'index.css'), '') - - let from = path.join(dir, 'index.css') - let plugin = tailwindcss({ base: dir, optimize: { minify: false } }) - - let first = await run( - plugin, - from, - css` - @tailwind utilities; - .first { - @apply underline; - } - `, - ) - let second = await run( - plugin, - from, - css` - @tailwind utilities; - .second { - @apply line-through; - } - `, - ) - - expect(first).toContain('.first') - expect(second).toContain('.second') - - await rm(dir, { recursive: true, force: true }) -}) - -test('does not register the input file as a dependency, even if it is passed in as relative path', async () => { - let processor = postcss([ - tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), - ]) - - let result = await processor.process(`@tailwind utilities`, { from: './input.css' }) - - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - .underline { - text-decoration-line: underline; - } - " - `) - - // Check for dependency messages - expect(result.messages).not.toContainEqual({ - type: 'dependency', - file: expect.stringMatching(/input.css$/g), - parent: expect.any(String), - plugin: expect.any(String), - }) -}) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index a4a8d9326..69a693971 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -1,48 +1,32 @@ import QuickLRU from '@alloc/quick-lru' -import { - compileAst, - env, - Features, - Instrumentation, - optimize as optimizeCss, - Polyfills, -} from '@tailwindcss/node' +import { compile, env } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' -import fs from 'node:fs' -import path, { relative } from 'node:path' -import type { AcceptedPlugin, PluginCreator, Postcss, Root } from 'postcss' -import { toCss, type AstNode } from '../../tailwindcss/src/ast' -import { cssAstToPostCssAst, postCssAstToCssAst } from './ast' +import fs from 'fs' +import { Features, transform } from 'lightningcss' +import path from 'path' +import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss' import fixRelativePathsPlugin from './postcss-fix-relative-paths' -const DEBUG = env.DEBUG - interface CacheEntry { mtimes: Map - inputCss: string - compiler: null | ReturnType + compiler: null | Awaited> scanner: null | Scanner - tailwindCssAst: AstNode[] - cachedPostCssAst: Root - optimizedPostCssAst: Root + css: string + optimizedCss: string fullRebuildPaths: string[] } -const cache = new QuickLRU({ maxSize: 50 }) +let cache = new QuickLRU({ maxSize: 50 }) -function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOptions): CacheEntry { - let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}` +function getContextFromCache(inputFile: string, opts: PluginOptions): CacheEntry { + let key = `${inputFile}:${opts.base ?? ''}:${opts.optimize ?? ''}` if (cache.has(key)) return cache.get(key)! let entry = { mtimes: new Map(), - inputCss: '', compiler: null, scanner: null, - - tailwindCssAst: [], - cachedPostCssAst: postcss.root(), - optimizedPostCssAst: postcss.root(), - + css: '', + optimizedCss: '', fullRebuildPaths: [] as string[], } cache.set(key, entry) @@ -50,30 +34,16 @@ function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOp } export type PluginOptions = { - /** - * The base directory to scan for class candidates. - * - * Defaults to the current working directory. - */ + // The base directory to scan for class candidates. base?: string - /** - * Optimize and minify the output CSS. - */ + // Optimize and minify the output CSS. optimize?: boolean | { minify?: boolean } - - /** - * Enable or disable asset URL rewriting. - * - * Defaults to `true`. - */ - transformAssetUrls?: boolean } function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { let base = opts.base ?? process.cwd() let optimize = opts.optimize ?? process.env.NODE_ENV === 'production' - let shouldRewriteUrls = opts.transformAssetUrls ?? true return { postcssPlugin: '@tailwindcss/postcss', @@ -85,293 +55,166 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { { postcssPlugin: 'tailwindcss', - async Once(root, { result, postcss }) { - using I = new Instrumentation() - + async OnceExit(root, { result }) { + env.DEBUG && console.time('[@tailwindcss/postcss] Total time in @tailwindcss/postcss') let inputFile = result.opts.from ?? '' - let isCSSModuleFile = inputFile.endsWith('.module.css') + let context = getContextFromCache(inputFile, opts) + let inputBasePath = path.dirname(path.resolve(inputFile)) - DEBUG && I.start(`[@tailwindcss/postcss] ${relative(base, inputFile)}`) + async function createCompiler() { + env.DEBUG && console.time('[@tailwindcss/postcss] Setup compiler') + clearRequireCache(context.fullRebuildPaths) - // Bail out early if this is guaranteed to be a non-Tailwind CSS file. - { - DEBUG && I.start('Quick bail check') - let canBail = true - root.walkAtRules((node) => { - if ( - node.name === 'import' || - node.name === 'reference' || - node.name === 'theme' || - node.name === 'variant' || - node.name === 'config' || - node.name === 'plugin' || - node.name === 'apply' || - node.name === 'tailwind' - ) { - canBail = false - return false - } + context.fullRebuildPaths = [] + + let compiler = await compile(root.toString(), { + base: inputBasePath, + onDependency: (path) => { + context.fullRebuildPaths.push(path) + }, }) - if (canBail) return - DEBUG && I.end('Quick bail check') - } - let context = getContextFromCache(postcss, inputFile, opts) - let inputBasePath = inputFile ? path.dirname(path.resolve(inputFile)) : base + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Setup compiler') + return compiler + } // Whether this is the first build or not, if it is, then we can // optimize the build by not creating the compiler until we need it. let isInitialBuild = context.compiler === null - async function createCompiler() { - DEBUG && I.start('Setup compiler') - if (context.fullRebuildPaths.length > 0 && !isInitialBuild) { - clearRequireCache(context.fullRebuildPaths) - } + // Setup the compiler if it doesn't exist yet. This way we can + // guarantee a `build()` function is available. + context.compiler ??= await createCompiler() - context.fullRebuildPaths = [] + let rebuildStrategy: 'full' | 'incremental' = 'incremental' - DEBUG && I.start('PostCSS AST -> Tailwind CSS AST') - let ast = postCssAstToCssAst(root) - DEBUG && I.end('PostCSS AST -> Tailwind CSS AST') - - DEBUG && I.start('Create compiler') - let compiler = await compileAst(ast, { - from: result.opts.from, - base: inputBasePath, - shouldRewriteUrls, - onDependency: (path) => context.fullRebuildPaths.push(path), - // 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 - } - - try { - // Setup the compiler if it doesn't exist yet. This way we can - // guarantee a `build()` function is available. - context.compiler ??= createCompiler() - - if ((await context.compiler).features === Features.None) { - return - } - - let rebuildStrategy: 'full' | 'incremental' = 'incremental' - - // Track file modification times to CSS files - DEBUG && I.start('Register full rebuild paths') - { - for (let file of context.fullRebuildPaths) { - result.messages.push({ - type: 'dependency', - plugin: '@tailwindcss/postcss', - file: path.resolve(file), - parent: result.opts.from, - }) - } - - let files = result.messages.flatMap((message) => { - if (message.type !== 'dependency') return [] - return message.file - }) - files.push(inputFile) - - for (let file of files) { - let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null - 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) - } - - // The mtimes above only track files on disk. When the input CSS is - // generated by another tool (e.g. Sass) and passed to `process()`, - // it can change without any tracked file's mtime changing, so also - // rebuild when the input CSS itself differs from the last build. - let inputCss = root.source?.input.css ?? root.toString() - if (context.inputCss !== inputCss) { - rebuildStrategy = 'full' - context.inputCss = inputCss - } - } - DEBUG && I.end('Register full rebuild paths') - - if ( - rebuildStrategy === 'full' && - // We can reuse the compiler if it was created during the - // initial build. If it wasn't, we need to create a new one. - !isInitialBuild - ) { - context.compiler = createCompiler() - } - - let compiler = await context.compiler - - 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) - - // Look for candidates used to generate the CSS - context.scanner = new Scanner({ sources }) - DEBUG && I.end('Setup scanner') - } - - DEBUG && I.start('Scan for candidates') - let candidates = compiler.features & Features.Utilities ? context.scanner.scan() : [] - DEBUG && I.end('Scan for candidates') - - if (compiler.features & Features.Utilities) { - DEBUG && I.start('Register dependency messages') - // Add all found files as direct dependencies - // Note: With Turbopack, the input file might not be a resolved path - 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 - } - result.messages.push({ - type: 'dependency', - plugin: '@tailwindcss/postcss', - file: absolutePath, - parent: result.opts.from, - }) - } - - // Register dependencies so changes in `base` cause a rebuild while - // giving tools like Vite or Parcel a glob that can be used to limit - // the files that cause a rebuild to only those that match it. - for (let { base: globBase, pattern } of context.scanner.globs) { - // Avoid adding a dependency on the base directory itself, since it - // causes Next.js to start an endless recursion if the `distDir` is - // configured to anything other than the default `.next` dir. - if (pattern === '*' && base === globBase) { - continue - } - - if (pattern === '') { - result.messages.push({ - type: 'dependency', - plugin: '@tailwindcss/postcss', - file: path.resolve(globBase), - parent: result.opts.from, - }) - } else { - result.messages.push({ - type: 'dir-dependency', - plugin: '@tailwindcss/postcss', - dir: path.resolve(globBase), - glob: pattern, - parent: result.opts.from, - }) - } - } - DEBUG && I.end('Register dependency messages') - } - - DEBUG && I.start('Build utilities') - let tailwindCssAst = compiler.build(candidates) - DEBUG && I.end('Build utilities') - - if (context.tailwindCssAst !== tailwindCssAst) { - if (optimize) { - DEBUG && I.start('Optimization') - - DEBUG && I.start('AST -> CSS') - let css = toCss(tailwindCssAst) - DEBUG && I.end('AST -> CSS') - - DEBUG && I.start('Lightning CSS') - let optimized = optimizeCss(css, { - minify: typeof optimize === 'object' ? optimize.minify : true, - }) - DEBUG && I.end('Lightning CSS') - - DEBUG && I.start('CSS -> PostCSS AST') - context.optimizedPostCssAst = postcss.parse(optimized.code, result.opts) - DEBUG && I.end('CSS -> PostCSS AST') - - DEBUG && I.end('Optimization') - } else { - // Convert our AST to a PostCSS AST - DEBUG && I.start('Transform Tailwind CSS AST into PostCSS AST') - context.cachedPostCssAst = cssAstToPostCssAst(postcss, tailwindCssAst, root.source) - DEBUG && I.end('Transform Tailwind CSS AST into PostCSS AST') - } - } - - context.tailwindCssAst = tailwindCssAst - - DEBUG && I.start('Update PostCSS AST') - root.removeAll() - root.append( - optimize - ? context.optimizedPostCssAst.clone().nodes - : context.cachedPostCssAst.clone().nodes, - ) - - // Trick PostCSS into thinking the indent is 2 spaces, so it uses that - // as the default instead of 4. - root.raws.indent = ' ' - DEBUG && I.end('Update PostCSS AST') - - DEBUG && I.end(`[@tailwindcss/postcss] ${relative(base, inputFile)}`) - } catch (error) { - // An error requires a full rebuild to fix - context.compiler = null - - // Ensure all dependencies we have collected thus far are included so that the rebuild - // is correctly triggered + // Track file modification times to CSS files + { for (let file of context.fullRebuildPaths) { result.messages.push({ type: 'dependency', plugin: '@tailwindcss/postcss', - file: path.resolve(file), + file, parent: result.opts.from, }) } - // We found that throwing the error will cause PostCSS to no longer watch for changes - // in some situations so we instead log the error and continue with an empty stylesheet. - console.error(error) + let files = result.messages.flatMap((message) => { + if (message.type !== 'dependency') return [] + return message.file + }) + files.push(inputFile) - if (error && typeof error === 'object' && 'message' in error) { - throw root.error(`${error.message}`) + for (let file of files) { + let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null + 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) } - - throw root.error(`${error}`) } + + let css = '' + + if ( + rebuildStrategy === 'full' && + // We can re-use the compiler if it was created during the + // initial build. If it wasn't, we need to create a new one. + !isInitialBuild + ) { + context.compiler = await createCompiler() + } + + if (context.scanner === null || rebuildStrategy === 'full') { + // Look for candidates used to generate the CSS + context.scanner = new Scanner({ + detectSources: { base }, + sources: context.compiler.globs, + }) + } + + env.DEBUG && console.time('[@tailwindcss/postcss] Scan for candidates') + let candidates = context.scanner.scan() + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Scan for candidates') + + // Add all found files as direct dependencies + for (let file of context.scanner.files) { + result.messages.push({ + type: 'dependency', + plugin: '@tailwindcss/postcss', + file, + parent: result.opts.from, + }) + } + + // Register dependencies so changes in `base` cause a rebuild while + // giving tools like Vite or Parcel a glob that can be used to limit + // the files that cause a rebuild to only those that match it. + for (let { base, pattern } of context.scanner.globs) { + result.messages.push({ + type: 'dir-dependency', + plugin: '@tailwindcss/postcss', + dir: base, + glob: pattern, + parent: result.opts.from, + }) + } + + env.DEBUG && console.time('[@tailwindcss/postcss] Build CSS') + css = context.compiler.build(candidates) + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build CSS') + + // Replace CSS + if (css !== context.css && optimize) { + env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS') + context.optimizedCss = optimizeCss(css, { + minify: typeof optimize === 'object' ? optimize.minify : true, + }) + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Optimize CSS') + } + context.css = css + + env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') + root.removeAll() + root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts)) + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Update PostCSS AST') + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Total time in @tailwindcss/postcss') }, }, ], } } +function optimizeCss( + input: string, + { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {}, +) { + return transform({ + filename: file, + code: Buffer.from(input), + minify, + sourceMap: false, + drafts: { + customMedia: true, + }, + nonStandard: { + deepSelectorCombinator: true, + }, + include: Features.Nesting, + exclude: Features.LogicalProperties, + targets: { + safari: (16 << 16) | (4 << 8), + }, + errorRecovery: true, + }).code.toString() +} + export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts index fd90c483e..d2f72f664 100644 --- a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts @@ -4,7 +4,6 @@ import postcss from 'postcss' import atImport from 'postcss-import' import { describe, expect, test } from 'vitest' import fixRelativePathsPlugin from '.' -import { pretty } from '../../../tailwindcss/src/test-utils/run' describe('fixRelativePathsPlugin', () => { test('rewrites @source and @plugin to be relative to the initial css file', async () => { @@ -15,13 +14,11 @@ describe('fixRelativePathsPlugin', () => { let result = await processor.process(css, { from: cssPath }) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @source "../../example-project/src/**/*.ts"; + expect(result.css.trim()).toMatchInlineSnapshot(` + "@source "../../example-project/src/**/*.ts"; @source "!../../example-project/src/**/*.ts"; @plugin "../../example-project/src/plugin.js"; - @plugin "../../example-project/src/what\\"s-this.js"; - " + @plugin "../../example-project/src/what\\"s-this.js";" `) }) @@ -33,13 +30,11 @@ describe('fixRelativePathsPlugin', () => { let result = await processor.process(css, { from: cssPath }) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @plugin "/absolute/paths"; + expect(result.css.trim()).toMatchInlineSnapshot(` + "@plugin "/absolute/paths"; @plugin "C:\\Program Files\\HAL 9000"; @plugin "\\\\Media\\Pictures\\Worth\\1000 words"; - @plugin "some-node-dep"; - " + @plugin "some-node-dep";" `) }) @@ -51,16 +46,13 @@ describe('fixRelativePathsPlugin', () => { let result = await processor.process(css, { from: cssPath }) - expect(pretty(result.css)).toMatchInlineSnapshot(` - " - @plugin './plugin-in-sibling.ts'; + expect(result.css.trim()).toMatchInlineSnapshot(` + "@plugin './plugin-in-sibling.ts'; @plugin '../plugin-in-sibling.ts'; @plugin 'plugin-in-sibling'; - @plugin './plugin-in-root.ts'; @plugin '../plugin-in-root.ts'; - @plugin 'plugin-in-root'; - " + @plugin 'plugin-in-root';" `) }) }) diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts index 68dcb5551..2b88014a3 100644 --- a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts @@ -67,8 +67,10 @@ export default function fixRelativePathsPlugin(): Plugin { return { postcssPlugin: 'tailwindcss-postcss-fix-relative-paths', - Once(root) { - root.walkAtRules(/source|plugin|config/, fixRelativePath) + AtRule: { + source: fixRelativePath, + plugin: fixRelativePath, + config: fixRelativePath, }, } } diff --git a/packages/@tailwindcss-postcss/tsconfig.json b/packages/@tailwindcss-postcss/tsconfig.json index d2131cbeb..6ae022f65 100644 --- a/packages/@tailwindcss-postcss/tsconfig.json +++ b/packages/@tailwindcss-postcss/tsconfig.json @@ -1,6 +1,3 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { - "verbatimModuleSyntax": false, - }, } diff --git a/packages/@tailwindcss-standalone/package.json b/packages/@tailwindcss-standalone/package.json index 24f8c93c7..4763ed67a 100644 --- a/packages/@tailwindcss-standalone/package.json +++ b/packages/@tailwindcss-standalone/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/standalone", - "version": "4.3.3", + "version": "4.0.0-alpha.26", "private": true, "description": "Standalone CLI for Tailwind CSS", "license": "MIT", @@ -25,31 +25,28 @@ "dist" ], "dependencies": { - "@tailwindcss/aspect-ratio": "^0.4.2", - "@tailwindcss/cli": "workspace:*", - "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/typography": "^0.5.20", + "@tailwindcss/cli": "workspace:^", "detect-libc": "1.0.3", - "enhanced-resolve": "catalog:", - "tailwindcss": "workspace:*" + "enhanced-resolve": "^5.17.1", + "tailwindcss": "workspace:^" }, "__notes": "These binary packages must be included so Bun can build the CLI for all supported platforms. We also rely on Lightning CSS and Parcel being patched so Bun can statically analyze the executables.", "devDependencies": { - "@parcel/watcher-darwin-arm64": "catalog:", - "@parcel/watcher-darwin-x64": "catalog:", - "@parcel/watcher-linux-arm64-glibc": "catalog:", - "@parcel/watcher-linux-arm64-musl": "catalog:", - "@parcel/watcher-linux-x64-glibc": "catalog:", - "@parcel/watcher-linux-x64-musl": "catalog:", - "@parcel/watcher-win32-x64": "catalog:", - "@types/bun": "^1.3.14", - "bun": "^1.3.14", - "lightningcss-darwin-arm64": "catalog:", - "lightningcss-darwin-x64": "catalog:", - "lightningcss-linux-arm64-gnu": "catalog:", - "lightningcss-linux-arm64-musl": "catalog:", - "lightningcss-linux-x64-gnu": "catalog:", - "lightningcss-linux-x64-musl": "catalog:", - "lightningcss-win32-x64-msvc": "catalog:" + "@parcel/watcher-darwin-arm64": "^2.4.2-alpha.0", + "@parcel/watcher-darwin-x64": "^2.4.2-alpha.0", + "@parcel/watcher-linux-arm64-glibc": "^2.4.2-alpha.0", + "@parcel/watcher-linux-arm64-musl": "^2.4.2-alpha.0", + "@parcel/watcher-linux-x64-glibc": "^2.4.2-alpha.0", + "@parcel/watcher-linux-x64-musl": "^2.4.2-alpha.0", + "@parcel/watcher-win32-x64": "^2.4.2-alpha.0", + "@types/bun": "^1.1.10", + "bun": "^1.1.29", + "lightningcss-darwin-arm64": "^1.25.1", + "lightningcss-darwin-x64": "^1.25.1", + "lightningcss-linux-arm64-gnu": "^1.25.1", + "lightningcss-linux-arm64-musl": "^1.25.1", + "lightningcss-linux-x64-gnu": "^1.25.1", + "lightningcss-linux-x64-musl": "^1.25.1", + "lightningcss-win32-x64-msvc": "^1.25.1" } } diff --git a/packages/@tailwindcss-standalone/scripts/build.ts b/packages/@tailwindcss-standalone/scripts/build.ts index 0aee26435..83f3b20a9 100644 --- a/packages/@tailwindcss-standalone/scripts/build.ts +++ b/packages/@tailwindcss-standalone/scripts/build.ts @@ -1,3 +1,4 @@ +import { $ } from 'bun' import { createHash } from 'node:crypto' import { mkdir, readFile, writeFile } from 'node:fs/promises' import * as path from 'node:path' @@ -5,107 +6,66 @@ import { fileURLToPath } from 'node:url' const __dirname = fileURLToPath(new URL('.', import.meta.url)) -// Workaround for Bun binary downloads failing on Windows CI when -// USERPROFILE is passed through by Turborepo. -// -// Unfortunately, setting this at runtime doesn't appear to work so we have to -// spawn a new process without the env var. -if (process.env.NESTED_BUILD !== '1' && process.env.USERPROFILE && process.env.USERPROFILE !== '') { - let result = await Bun.$`bun ${fileURLToPath(import.meta.url)}`.env({ - USERPROFILE: '', - NESTED_BUILD: '1', - }) +async function buildForPlatform(triple: string, outfile: string) { + // We wrap this in a retry because occasionally the atomic rename fails for some reason + for (let i = 0; i < 5; ++i) { + try { + return await $`bun build --compile --target=${triple} ./src/index.ts --outfile=${outfile}` + } catch (err) { + if (i < 5) continue - process.exit(result.exitCode) + throw new Error(`Failed to build for platform ${triple}`, { cause: err }) + } + } } -// We use baseline builds for all x64 platforms to ensure compatibility with -// older hardware. -let builds: { target: Bun.Build.Target; name: string }[] = [ - { name: 'tailwindcss-linux-arm64', target: 'bun-linux-arm64' }, - { name: 'tailwindcss-linux-arm64-musl', target: 'bun-linux-arm64-musl' }, - // @ts-expect-error: Either the types are wrong or the runtime needs to be updated - // to accept a `-glibc` at the end like the types suggest. - { name: 'tailwindcss-linux-x64', target: 'bun-linux-x64-baseline' }, - { name: 'tailwindcss-linux-x64-musl', target: 'bun-linux-x64-baseline-musl' }, - { name: 'tailwindcss-macos-arm64', target: 'bun-darwin-arm64' }, - { name: 'tailwindcss-macos-x64', target: 'bun-darwin-x64-baseline' }, - { name: 'tailwindcss-windows-x64.exe', target: 'bun-windows-x64-baseline' }, -] +async function build(triple: string, file: string) { + let start = process.hrtime.bigint() -let summary: { target: Bun.Build.Target; name: string; sum: string }[] = [] + let outfile = path.resolve(__dirname, `../dist/${file}`) -// Build platform binaries and checksum them. -let start = process.hrtime.bigint() -for (let { target, name } of builds) { - let outfile = path.resolve(__dirname, `../dist/${name}`) + await buildForPlatform(triple, outfile) - let result = await Bun.build({ - entrypoints: ['./src/index.ts'], - target: 'node', - minify: { - whitespace: false, - syntax: true, - identifiers: false, - keepNames: true, - }, - - define: { - // This ensures only necessary binaries are bundled for linux targets - // It reduces binary size since no runtime selection is necessary - 'process.env.PLATFORM_LIBC': JSON.stringify(target.includes('-musl') ? 'musl' : 'glibc'), - - // This prevents the WASI build from being bundled with the binary - 'process.env.NAPI_RS_FORCE_WASI': JSON.stringify(''), - - // This simplifies the Oxide loading code a small amount - 'process.env.NAPI_RS_NATIVE_LIBRARY_PATH': JSON.stringify(''), - }, - - compile: { - target, - outfile, - - // Disable .env loading - autoloadDotenv: false, - - // Disable bunfig.toml loading - autoloadBunfig: false, - }, - - plugins: [ - { - name: 'tailwindcss-plugin', - setup(build) { - build.onLoad({ filter: /tailwindcss-oxide\.wasi\.cjs$/ }, async (args) => { - return { contents: '' } - }) - }, - }, - ], - }) - - let entry = result.outputs.find((output) => output.kind === 'entry-point') - if (!entry) throw new Error(`Build failed for ${target}`) + await new Promise((resolve) => setTimeout(resolve, 100)) let content = await readFile(outfile) + let sum = createHash('sha256').update(content).digest('hex') - summary.push({ - target, - name, - sum: createHash('sha256').update(content).digest('hex'), - }) + let elapsed = process.hrtime.bigint() - start + + return { + triple, + file, + sum, + elapsed, + } } await mkdir(path.resolve(__dirname, '../dist'), { recursive: true }) +// Build platform binaries and checksum them +let results = await Promise.all([ + build('bun-linux-arm64', './tailwindcss-linux-arm64'), + build('bun-linux-x64', './tailwindcss-linux-x64'), + // build('linux-armv7', 'tailwindcss-linux-armv7'), + build('bun-darwin-arm64', './tailwindcss-macos-arm64'), + build('bun-darwin-x64', './tailwindcss-macos-x64'), + // The Windows x64 build uses `bun-baseline` instead of the regular bun build. + // This enables support for running inside the ARM emulation mode. + build('bun-windows-x64-baseline', './tailwindcss-windows-x64.exe'), + // buildForPlatform('win32-arm64', 'tailwindcss-windows-arm64'), +]) + // Write the checksums to a file let sumsFile = path.resolve(__dirname, '../dist/sha256sums.txt') -let sums = summary.map(({ name, sum }) => `${sum} ./${name}`) +let sums = results.map(({ file, sum }) => `${sum} ${file}`) + +console.table( + results.map(({ triple, sum, elapsed }) => ({ + triple, + sum, + elapsed: `${(Number(elapsed) / 1e6).toFixed(0)}ms`, + })), +) await writeFile(sumsFile, sums.join('\n') + '\n') - -console.table(summary.map(({ target, sum }) => ({ target, sum }))) - -let elapsed = process.hrtime.bigint() - start -console.log(`Build completed in ${(Number(elapsed) / 1e6).toFixed(0)}ms`) diff --git a/packages/@tailwindcss-standalone/src/index.ts b/packages/@tailwindcss-standalone/src/index.ts index 23a928f8b..ae90e2b6e 100644 --- a/packages/@tailwindcss-standalone/src/index.ts +++ b/packages/@tailwindcss-standalone/src/index.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs' import { createRequire } from 'node:module' import packageJson from 'tailwindcss/package.json' @@ -11,10 +12,7 @@ const localResolve = createRequire(import.meta.url).resolve globalThis.__tw_resolve = (id, baseDir) => { let isEmbeddedFileBase = baseDir === '/$bunfs/root' || baseDir?.includes(':/~BUN/root') const likelyEmbeddedFile = - id === 'tailwindcss' || - id.startsWith('tailwindcss/') || - id.startsWith('@tailwindcss/') || - isEmbeddedFileBase + id === 'tailwindcss' || id.startsWith('tailwindcss/') || isEmbeddedFileBase if (!likelyEmbeddedFile) { return false @@ -40,59 +38,19 @@ globalThis.__tw_resolve = (id, baseDir) => { case 'utilities': case 'utilities.css': return localResolve(utilitiesCss) - case '@tailwindcss/forms': - case '@tailwindcss/typography': - case '@tailwindcss/aspect-ratio': - return id default: return false } } -globalThis.__tw_load = async (id) => { - if (id.endsWith('@tailwindcss/forms')) { - return require('@tailwindcss/forms') - } else if (id.endsWith('@tailwindcss/typography')) { - return require('@tailwindcss/typography') - } else if (id.endsWith('@tailwindcss/aspect-ratio')) { - return require('@tailwindcss/aspect-ratio') - } else { - return undefined - } -} globalThis.__tw_version = packageJson.version - -// We use a plugin to make sure that the JS APIs are bundled with the standalone -// CLI and can be imported inside configs and plugins -Bun.plugin({ - name: 'bundle-tailwindcss-apis', - target: 'bun', - async setup(build) { - // These imports must be static strings otherwise they won't be bundled - let bundled = { - tailwindcss: await import('tailwindcss'), - 'tailwindcss/colors': await import('tailwindcss/colors'), - 'tailwindcss/colors.js': await import('tailwindcss/colors'), - 'tailwindcss/plugin': await import('tailwindcss/plugin'), - 'tailwindcss/plugin.js': await import('tailwindcss/plugin'), - 'tailwindcss/package.json': await import('tailwindcss/package.json'), - 'tailwindcss/lib/util/flattenColorPalette': - await import('tailwindcss/lib/util/flattenColorPalette'), - 'tailwindcss/lib/util/flattenColorPalette.js': - await import('tailwindcss/lib/util/flattenColorPalette'), - 'tailwindcss/defaultTheme': await import('tailwindcss/defaultTheme'), - 'tailwindcss/defaultTheme.js': await import('tailwindcss/defaultTheme'), - } - - for (let [id, exports] of Object.entries(bundled)) { - build.module(id, () => ({ - loader: 'object', - exports: { - ...exports, - __esModule: true, - }, - })) - } - }, -}) +globalThis.__tw_readFile = async (path, encoding) => { + // When reading a file from the `$bunfs`, we need to use the synchronous + // `readFileSync` API + let isEmbeddedFileBase = path.includes('/$bunfs/root') || path.includes(':/~BUN/root') + if (!isEmbeddedFileBase) { + return + } + return fs.readFileSync(path, encoding) +} await import('../../@tailwindcss-cli/src/index.ts') diff --git a/packages/@tailwindcss-standalone/src/types.d.ts b/packages/@tailwindcss-standalone/src/types.d.ts index 0b5deb6ea..e13e4e427 100644 --- a/packages/@tailwindcss-standalone/src/types.d.ts +++ b/packages/@tailwindcss-standalone/src/types.d.ts @@ -6,5 +6,5 @@ declare module '*.css' { declare var __tw_version: string | undefined declare var __tw_resolve: undefined | ((id: string, base?: string) => string | false) declare var __tw_readFile: - undefined | ((path: string, encoding: BufferEncoding) => Promise) -declare var __tw_load: undefined | ((path: string) => Promise) + | undefined + | ((path: string, encoding: BufferEncoding) => Promise) diff --git a/packages/@tailwindcss-turbopack/README.md b/packages/@tailwindcss-turbopack/README.md deleted file mode 100644 index ee54b4d03..000000000 --- a/packages/@tailwindcss-turbopack/README.md +++ /dev/null @@ -1,99 +0,0 @@ -

- - - - - Tailwind CSS - - -

- -

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

- -

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

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

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

--- @@ -27,10 +27,14 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ## Community -For help, discussion about best practices, or feature ideas: +For help, discussion about best practices, or any other conversation that would benefit from being searchable: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[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) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json index e960d7677..7e0425be4 100644 --- a/packages/@tailwindcss-upgrade/package.json +++ b/packages/@tailwindcss-upgrade/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/upgrade", - "version": "4.3.3", + "version": "4.0.0-alpha.26", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -27,26 +27,23 @@ "access": "public" }, "dependencies": { - "@tailwindcss/node": "workspace:*", - "@tailwindcss/oxide": "workspace:*", - "dedent": "catalog:", - "enhanced-resolve": "catalog:", - "globby": "^16.2.2", - "jiti": "^2.7.0", + "@tailwindcss/node": "workspace:^", + "@tailwindcss/oxide": "workspace:^", + "enhanced-resolve": "^5.17.1", + "globby": "^14.0.2", + "jiti": "^2.0.0-beta.3", "mri": "^1.2.0", - "picocolors": "^1.1.1", - "postcss": "catalog:", - "postcss-import": "^16.1.1", - "postcss-selector-parser": "^7.1.4", - "prettier": "catalog:", - "semver": "^7.8.5", - "tailwindcss": "workspace:*", - "tree-sitter": "^0.25.1", - "tree-sitter-typescript": "^0.23.2" + "picocolors": "^1.0.1", + "postcss": "^8.4.41", + "postcss-import": "^16.1.0", + "postcss-selector-parser": "^6.1.2", + "prettier": "^3.3.3", + "string-byte-slice": "^3.0.0", + "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", "@types/postcss-import": "^14.0.3", - "@types/semver": "^7.8.0" + "dedent": "1.5.3" } } diff --git a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts deleted file mode 100644 index 6a451333e..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts +++ /dev/null @@ -1,553 +0,0 @@ -import { Scanner } from '@tailwindcss/oxide' -import fs from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { loadModule } from '../../../../@tailwindcss-node/src/compile' -import defaultTheme from '../../../../tailwindcss/dist/default-theme' -import { atRule, toCss, type AstNode } from '../../../../tailwindcss/src/ast' -import { - keyPathToCssProperty, - themeableValues, -} from '../../../../tailwindcss/src/compat/apply-config-to-theme' -import { keyframesToRules } from '../../../../tailwindcss/src/compat/apply-keyframes-to-theme' -import { - resolveConfig, - type ConfigFile, -} from '../../../../tailwindcss/src/compat/config/resolve-config' -import type { ResolvedConfig, ThemeConfig } from '../../../../tailwindcss/src/compat/config/types' -import { buildCustomContainerUtilityRules } from '../../../../tailwindcss/src/compat/container' -import { darkModePlugin } from '../../../../tailwindcss/src/compat/dark-mode' -import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' -import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { escape } from '../../../../tailwindcss/src/utils/escape' -import { - isValidOpacityValue, - isValidSpacingMultiplier, -} from '../../../../tailwindcss/src/utils/infer-data-type' -import * as ValueParser from '../../../../tailwindcss/src/value-parser' -import { findStaticPlugins, type StaticPluginOptions } from '../../utils/extract-static-plugins' -import { highlight, info, relative } from '../../utils/renderer' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -export type JSConfigMigration = - // Could not convert the config file, need to inject it as-is in a @config directive - null | { - sources: { base: string; pattern: string }[] - plugins: { base: string; path: string; options: null | StaticPluginOptions }[] - css: string - } - -export async function migrateJsConfig( - designSystem: DesignSystem, - fullConfigPath: string, - base: string, -): Promise { - let [unresolvedConfig, source] = await Promise.all([ - loadModule(fullConfigPath, __dirname, () => {}).then((result) => result.module) as Config, - fs.readFile(fullConfigPath, 'utf-8'), - ]) - - if (!canMigrateConfig(unresolvedConfig, source)) { - info( - `The configuration file at ${highlight(relative(fullConfigPath, base))} could not be automatically migrated to the new CSS configuration format, so your CSS has been updated to load your existing configuration file.`, - { prefix: '↳ ' }, - ) - return null - } - - let sources: { base: string; pattern: string }[] = [] - let plugins: { base: string; path: string; options: null | StaticPluginOptions }[] = [] - let cssConfigs: string[] = [] - - if ('darkMode' in unresolvedConfig) { - cssConfigs.push(migrateDarkMode(unresolvedConfig as any)) - } - - if ('content' in unresolvedConfig) { - sources = await migrateContent(unresolvedConfig as any, fullConfigPath, base) - } - - if ('theme' in unresolvedConfig) { - let themeConfig = await migrateTheme(designSystem, unresolvedConfig, base) - if (themeConfig) cssConfigs.push(themeConfig) - } - - if ('corePlugins' in unresolvedConfig) { - info( - `The \`corePlugins\` option is no longer supported as of Tailwind CSS v4.0, so it's been removed from your configuration.`, - ) - } - - let simplePlugins = findStaticPlugins(source) - if (simplePlugins !== null) { - for (let [path, options] of simplePlugins) { - plugins.push({ base, path, options }) - } - } - - return { - sources, - plugins, - css: cssConfigs.join('\n'), - } -} - -async function migrateTheme( - designSystem: DesignSystem, - unresolvedConfig: Config, - base: string, -): Promise { - // Resolve the config file without applying plugins and presets, as these are - // migrated to CSS separately. - let configToResolve: ConfigFile = { - base, - config: { ...unresolvedConfig, plugins: [], presets: undefined }, - reference: false, - src: undefined, - } - let { resolvedConfig, replacedThemeKeys } = resolveConfig(designSystem, [configToResolve]) - - let resetNamespaces = new Map( - Array.from(replacedThemeKeys.entries()).map(([key]) => [key, false]), - ) - - removeUnnecessarySpacingKeys(designSystem, resolvedConfig, replacedThemeKeys) - - let css = '' - let prevSectionKey = '' - let themeSection: string[] = [] - let keyframesCss = '' - let variants = new Map() - - // Special handling of specific theme keys: - { - if ('keyframes' in resolvedConfig.theme) { - keyframesCss += keyframesToCss(resolvedConfig.theme.keyframes) - delete resolvedConfig.theme.keyframes - } - - if ('container' in resolvedConfig.theme) { - let rules = buildCustomContainerUtilityRules(resolvedConfig.theme.container, designSystem) - if (rules.length > 0) { - // Using `theme` instead of `utility` so it sits before the `@layer - // base` with compatibility CSS. While this is technically a utility, it - // makes a bit more sense to emit this closer to the `@theme` values - // since it is needed for backwards compatibility. - css += `\n@tw-bucket theme {\n` - css += toCss([atRule('@utility', 'container', rules)]) - css += '}\n' // @tw-bucket - } - delete resolvedConfig.theme.container - } - - if ('aria' in resolvedConfig.theme) { - for (let [key, value] of Object.entries(resolvedConfig.theme.aria ?? {})) { - // Will be handled by bare values if the names match. - // E.g.: `aria-foo:flex` should produce `[aria-foo="true"]` - if (new RegExp(`^${key}=(['"]?)true\\1$`).test(`${value}`)) continue - - // Create custom variant - variants.set(`aria-${key}`, `&[aria-${value}]`) - } - delete resolvedConfig.theme.aria - } - - if ('data' in resolvedConfig.theme) { - for (let [key, value] of Object.entries(resolvedConfig.theme.data ?? {})) { - // Will be handled by bare values if the names match. - // E.g.: `data-foo:flex` should produce `[data-foo]` - if (key === value) continue - - // Create custom variant - variants.set(`data-${key}`, `&[data-${value}]`) - } - delete resolvedConfig.theme.data - } - - if ('supports' in resolvedConfig.theme) { - for (let [key, value] of Object.entries(resolvedConfig.theme.supports ?? {})) { - // Will be handled by bare values if the value of the declaration is a - // CSS variable. - let parsed = ValueParser.parse(`${value}`) - - // Unwrap the parens, e.g.: `(foo: var(--bar))` → `foo: var(--bar)` - if (parsed.length === 1 && parsed[0].kind === 'function' && parsed[0].value === '') { - parsed = parsed[0].nodes - } - - // Verify structure: `foo: var(--bar)` - // ^^^ ← must match the `key` - if ( - parsed.length === 3 && - parsed[0].kind === 'word' && - parsed[0].value === key && - parsed[2].kind === 'function' && - parsed[2].value === 'var' - ) { - continue - } - - // Create custom variant - variants.set(`supports-${key}`, `{@supports(${value}){@slot;}}`) - } - delete resolvedConfig.theme.supports - } - } - - // Convert theme values to CSS custom properties - for (let [key, value] of themeableValues(resolvedConfig.theme)) { - if (typeof value !== 'string' && typeof value !== 'number') { - continue - } - - if (typeof value === 'string') { - // This is more advanced than the version in core as ideally something - // like `rgba(0 0 0 / )` becomes `rgba(0 0 0)`. Since we know - // from the `/` that it's used in an alpha channel and we can remove it. - // - // In other cases we may not know exactly how its used, so we'll just - // replace it with `1` like core does. - value = value.replace(/\s*\/\s*/, '').replace(//, '1') - } - - // Convert `opacity` namespace from decimal to percentage values. - // Additionally we can drop values that resolve to the same value as the - // named modifier with the same name. - if (key[0] === 'opacity' && (typeof value === 'number' || typeof value === 'string')) { - let numValue = typeof value === 'string' ? parseFloat(value) : value - - if (numValue >= 0 && numValue <= 1) { - value = numValue * 100 + '%' - } - - if ( - typeof value === 'string' && - key[1] === value.replace(/%$/, '') && - isValidOpacityValue(key[1]) - ) { - continue - } - } - - let sectionKey = createSectionKey(key) - if (sectionKey !== prevSectionKey) { - themeSection.push('') - prevSectionKey = sectionKey - } - - let property = keyPathToCssProperty(key) - - if (property !== null) { - if ( - !property.startsWith('default-') && - resetNamespaces.has(key[0]) && - resetNamespaces.get(key[0]) === false - ) { - resetNamespaces.set(key[0], true) - let ns = keyPathToCssProperty([key[0]]) - if (ns !== null) { - themeSection.push(` ${escape(`--${ns}`)}-*: initial;`) - } - } - - themeSection.push(` ${escape(`--${property}`)}: ${value};`) - } - } - - if (keyframesCss) { - themeSection.push('', keyframesCss) - } - - if (themeSection.length > 0) { - css += `\n@tw-bucket theme {\n` - css += `\n@theme {\n` - css += themeSection.join('\n') + '\n' - css += '}\n' // @theme - css += '}\n' // @tw-bucket - } - - if (variants.size > 0) { - css += '\n@tw-bucket custom-variant {\n' - - let previousRoot = '' - for (let [name, selector] of variants) { - let root = name.split('-')[0] - if (previousRoot !== root) css += '\n' - previousRoot = root - - if (selector.startsWith('{')) { - css += `@custom-variant ${name} ${selector}\n` - } else { - css += `@custom-variant ${name} (${selector});\n` - } - } - css += '}\n' - } - - return css -} - -function migrateDarkMode(unresolvedConfig: Config & { darkMode: any }): string { - let variant: string | string[] = '' - let addVariant = (_name: string, _variant: string) => (variant = _variant) - let config = () => unresolvedConfig.darkMode - darkModePlugin({ config, addVariant }) - - if (variant === '') { - return '' - } - - if (!Array.isArray(variant)) { - variant = [variant] - } - - if (variant.length === 1 && !variant[0].includes('{')) { - return `\n@tw-bucket custom-variant {\n@custom-variant dark (${variant[0]});\n}\n` - } - - let customVariant = '' - for (let variantName of variant) { - // Convert to the block syntax if a block is used - if (variantName.includes('{')) { - customVariant += variantName.replace('}', '{ @slot }}') + '\n' - } else { - customVariant += variantName + '{ @slot }\n' - } - } - - if (customVariant !== '') { - return `\n@tw-bucket custom-variant {\n@custom-variant dark {${customVariant}};\n}\n` - } - - return '' -} - -// Returns a string identifier used to section theme declarations -function createSectionKey(key: string[]): string { - let sectionSegments = [] - for (let i = 0; i < key.length - 1; i++) { - let segment = key[i] - // Ignore tuples - if (key[i + 1][0] === '-') { - break - } - sectionSegments.push(segment) - } - return sectionSegments.join('-') -} - -async function migrateContent( - unresolvedConfig: Config, - configPath: string, - base: string, -): Promise<{ base: string; pattern: string }[]> { - let autoContentFiles = autodetectedSourceFiles(base) - - let sources = [] - let contentIsRelative = (() => { - if (!unresolvedConfig.content) return false - if (Array.isArray(unresolvedConfig.content)) return false - if (unresolvedConfig.content.relative) return true - if (unresolvedConfig.future === 'all') return false - return unresolvedConfig.future?.relativeContentPathsByDefault ?? false - })() - - let sourceGlobs = Array.isArray(unresolvedConfig.content) - ? unresolvedConfig.content.map((pattern) => ({ base, pattern })) - : (unresolvedConfig.content?.files ?? []).map((pattern) => { - if (typeof pattern === 'string' && contentIsRelative) { - return { base: path.dirname(configPath), pattern: pattern } - } - return { base, pattern } - }) - - for (let { base, pattern } of sourceGlobs) { - if (typeof pattern !== 'string') { - throw new Error('Unsupported content value: ' + pattern) - } - - let sourceFiles = patternSourceFiles({ - base, - pattern: pattern[0] === '!' ? pattern.slice(1) : pattern, - negated: pattern[0] === '!', - }) - - let autoContentContainsAllSourceFiles = true - for (let sourceFile of sourceFiles) { - if (!autoContentFiles.includes(sourceFile)) { - autoContentContainsAllSourceFiles = false - break - } - } - - if (!autoContentContainsAllSourceFiles) { - sources.push({ base, pattern }) - } - } - return sources -} - -// Applies heuristics to determine if we can attempt to migrate the config -function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { - // The file may not contain non-serializable values - function isSimpleValue(value: unknown): boolean { - if (typeof value === 'function') return false - if (Array.isArray(value)) return value.every(isSimpleValue) - if (typeof value === 'object' && value !== null) { - return Object.values(value).every(isSimpleValue) - } - return ['string', 'number', 'boolean', 'undefined'].includes(typeof value) - } - - // `theme` and `plugins` are handled separately and allowed to be more complex - let { plugins, theme, ...remainder } = unresolvedConfig - if (!isSimpleValue(remainder)) { - return false - } - - // The file may only contain known-migratable top-level properties - let knownProperties = [ - 'darkMode', - 'content', - 'theme', - 'plugins', - 'presets', - 'prefix', // Prefix is handled in the dedicated prefix migrator - 'corePlugins', - 'future', - 'experimental', - ] - - if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) { - return false - } - - if (findStaticPlugins(source) === null) { - return false - } - - if (unresolvedConfig.presets && unresolvedConfig.presets.length > 0) { - return false - } - - // If there are unknown "future" flags we should bail - if (unresolvedConfig.future && unresolvedConfig.future !== 'all') { - let knownFlags = [ - 'hoverOnlyWhenSupported', - 'respectDefaultRingColorOpacity', - 'disableColorOpacityUtilitiesByDefault', - 'relativeContentPathsByDefault', - ] - - if (Object.keys(unresolvedConfig.future).some((key) => !knownFlags.includes(key))) { - return false - } - } - - // If there are unknown "experimental" flags we should bail - if (unresolvedConfig.experimental && unresolvedConfig.experimental !== 'all') { - let knownFlags = ['generalizedModifiers'] - - if (Object.keys(unresolvedConfig.experimental).some((key) => !knownFlags.includes(key))) { - return false - } - } - - // Only migrate the config file if all top-level theme keys are allowed to be - // migrated - if (theme && typeof theme === 'object') { - if (theme.extend && !onlyAllowedThemeValues(theme.extend)) return false - let { extend: _extend, ...themeCopy } = theme - if (!onlyAllowedThemeValues(themeCopy)) return false - } - - return true -} - -const ALLOWED_THEME_KEYS = [ - ...Object.keys(defaultTheme), - // Used by @tailwindcss/container-queries - 'containers', -] -function onlyAllowedThemeValues(theme: ThemeConfig): boolean { - for (let key of Object.keys(theme)) { - if (!ALLOWED_THEME_KEYS.includes(key)) { - return false - } - } - - if ('screens' in theme && typeof theme.screens === 'object' && theme.screens !== null) { - for (let screen of Object.values(theme.screens)) { - if (typeof screen === 'object' && screen !== null && ('max' in screen || 'raw' in screen)) { - return false - } - } - } - return true -} - -function keyframesToCss(keyframes: Record): string { - let ast: AstNode[] = keyframesToRules({ theme: { keyframes } }) - return toCss(ast).trim() + '\n' -} - -function autodetectedSourceFiles(base: string) { - let scanner = new Scanner({ - sources: [ - { - base, - pattern: '**/*', - negated: false, - }, - ], - }) - scanner.scan() - return scanner.files -} - -function patternSourceFiles(source: { base: string; pattern: string; negated: boolean }): string[] { - let scanner = new Scanner({ sources: [source] }) - scanner.scan() - return scanner.files -} - -function removeUnnecessarySpacingKeys( - designSystem: DesignSystem, - resolvedConfig: ResolvedConfig, - replacedThemeKeys: Set, -) { - // We want to keep the spacing scale as-is if the user is overwriting - if (replacedThemeKeys.has('spacing')) return - - // Ensure we have a spacing multiplier - let spacingScale = designSystem.theme.get(['--spacing']) - if (!spacingScale) return - - let [spacingMultiplier, spacingUnit] = splitNumberAndUnit(spacingScale) - if (!spacingMultiplier || !spacingUnit) return - - if (spacingScale && !replacedThemeKeys.has('spacing')) { - for (let [key, value] of Object.entries(resolvedConfig.theme.spacing ?? {})) { - let [multiplier, unit] = splitNumberAndUnit(value as string) - if (multiplier === null) continue - - if (!isValidSpacingMultiplier(key)) continue - if (unit !== spacingUnit) continue - - if (parseFloat(multiplier) === Number(key) * parseFloat(spacingMultiplier)) { - delete resolvedConfig.theme.spacing[key] - designSystem.theme.clearNamespace(escape(`--spacing-${key.replaceAll('.', '_')}`), 0) - } - } - } -} - -function splitNumberAndUnit(value: string): [string, string] | [null, null] { - let match = value.match(/^([0-9.]+)(.*)$/) - if (!match) { - return [null, null] - } - return [match[1], match[2]] -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts b/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts deleted file mode 100644 index c7058df54..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { isGitIgnored } from 'globby' -import path from 'node:path' -import postcss, { type Result } from 'postcss' -import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' -import { segment } from '../../../../tailwindcss/src/utils/segment' -import { Stylesheet, type StylesheetConnection } from '../../stylesheet' -import { gitRoot } from '../../utils/git' -import { error, highlight, relative } from '../../utils/renderer' -import { resolveCssId } from '../../utils/resolve' - -export async function analyze(stylesheets: Stylesheet[], { base }: { base: string }) { - // Use the project's git root, instead of the `base` to ensure all - // `.gitignore` rules are being used. - let isIgnored = await isGitIgnored({ cwd: gitRoot(base) ?? base }) - let processingQueue: (() => Promise)[] = [] - let stylesheetsByFile = new DefaultMap((file) => { - // We don't want to process ignored files (like node_modules) - try { - if (isIgnored(file)) { - return null - } - } catch { - // If the file is not part of the current working directory (which can - // happen if you import `tailwindcss` and it's loading a shared file from - // pnpm) then this will throw. - return null - } - - try { - let sheet = Stylesheet.loadSync(file) - - // Mutate incoming stylesheets to include the newly discovered sheet - stylesheets.push(sheet) - - // Queue up the processing of this stylesheet - processingQueue.push(() => processor.process(sheet.root, { from: sheet.file! })) - - return sheet - } catch { - return null - } - }) - - // Step 1: Record which `@import` rules point to which stylesheets - // and which stylesheets are parents/children of each other - let processor = postcss([ - { - postcssPlugin: 'mark-import-nodes', - AtRule: { - import(node) { - // Find what the import points to - let id = node.params.match(/['"](.*?)['"]/)?.[1] - if (!id) return - - let basePath = node.source?.input.file - ? path.dirname(node.source.input.file) - : process.cwd() - - // Resolve the import to a file path - let resolvedPath: string | false = false - try { - // We first try to resolve the file as relative to the current file - // to mimic the behavior of `postcss-import` since that's what was - // used to resolve imports in Tailwind CSS v3. - if (id[0] !== '.') { - try { - resolvedPath = resolveCssId(`./${id}`, basePath) - } catch {} - } - - if (!resolvedPath) { - resolvedPath = resolveCssId(id, basePath) - } - } catch (err) { - // Import is a URL, we don't want to process these, but also don't - // want to show an error message for them. - if (id.startsWith('http://') || id.startsWith('https://') || id.startsWith('//')) { - return - } - - // Something went wrong, we can't resolve the import. - error( - `Failed to resolve import: ${highlight(id)} in ${highlight(relative(node.source?.input.file!, basePath))}. Skipping.`, - { prefix: '↳ ' }, - ) - return - } - - if (!resolvedPath) return - - // Find the stylesheet pointing to the resolved path - let stylesheet = stylesheetsByFile.get(resolvedPath) - - // If it _does not_ exist in stylesheets we don't care and skip it - // this is likely because its in node_modules or a workspace package - // that we don't want to modify - if (!stylesheet) return - - // Mark the import node with the ID of the stylesheet it points to - // We will use these later to build lookup tables and modify the AST - node.raws.tailwind_destination_sheet_id = stylesheet.id - - let parent = node.source?.input.file - ? stylesheetsByFile.get(node.source.input.file) - : undefined - - let layers: string[] = [] - - for (let part of segment(node.params, ' ')) { - if (!part.startsWith('layer(')) continue - if (!part.endsWith(')')) continue - - layers.push(part.slice(6, -1).trim()) - } - - // Connect sheets together in a dependency graph - if (parent) { - let meta = { layers } - stylesheet.parents.add({ item: parent, meta }) - parent.children.add({ item: stylesheet, meta }) - } - }, - }, - }, - ]) - - // Seed the map with all the known stylesheets, and queue up the processing of - // each incoming stylesheet. - for (let sheet of stylesheets) { - if (sheet.file) { - stylesheetsByFile.set(sheet.file, sheet) - processingQueue.push(() => processor.process(sheet.root, { from: sheet.file ?? undefined })) - } - } - - // Process all the stylesheets from step 1 - while (processingQueue.length > 0) { - let task = processingQueue.shift()! - await task() - } - - // --- - - let commonPath = process.cwd() - - function pathToString(path: StylesheetConnection[]) { - let parts: string[] = [] - - for (let connection of path) { - if (!connection.item.file) continue - - let filePath = connection.item.file.replace(commonPath, '') - let layers = connection.meta.layers.join(', ') - - if (layers.length > 0) { - parts.push(`${filePath} (layers: ${layers})`) - } else { - parts.push(filePath) - } - } - - return parts.join(' <- ') - } - - let lines: string[] = [] - - for (let sheet of stylesheets) { - if (!sheet.file) continue - - let { convertiblePaths, nonConvertiblePaths } = sheet.analyzeImportPaths() - let isAmbiguous = convertiblePaths.length > 0 && nonConvertiblePaths.length > 0 - - if (!isAmbiguous) continue - - sheet.canMigrate = false - - let filePath = sheet.file.replace(commonPath, '') - - for (let path of convertiblePaths) { - lines.push(`- ${filePath} <- ${pathToString(path)}`) - } - - for (let path of nonConvertiblePaths) { - lines.push(`- ${filePath} <- ${pathToString(path)}`) - } - } - - if (lines.length === 0) { - let tailwindRootLeafs = new Set() - - for (let sheet of stylesheets) { - // If the current file already contains `@config`, then we can assume it's - // a Tailwind CSS root file. - sheet.root.walkAtRules('config', () => { - sheet.isTailwindRoot = true - return false - }) - if (sheet.isTailwindRoot) continue - - // If an `@tailwind` at-rule, or `@import "tailwindcss"` is present, - // then we can assume it's a file where Tailwind CSS might be configured. - // - // However, if 2 or more stylesheets exist with these rules that share a - // common parent, then we want to mark the common parent as the root - // stylesheet instead. - sheet.root.walkAtRules((node) => { - if ( - node.name === 'tailwind' || - (node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) || - (node.name === 'import' && node.params.match(/^["']tailwindcss\/.*?["']$/)) - ) { - sheet.isTailwindRoot = true - tailwindRootLeafs.add(sheet) - } - }) - } - - // Only a single Tailwind CSS root file exists, no need to do anything else. - if (tailwindRootLeafs.size <= 1) { - return - } - - // Mark the common parent as the root file - { - // Group each sheet from tailwindRootLeafs by their common parent - let commonParents = new DefaultMap>(() => new Set()) - - // Seed common parents with leafs - for (let sheet of tailwindRootLeafs) { - commonParents.get(sheet).add(sheet) - } - - // If any 2 common parents come from the same tree, then all children of - // parent A and parent B will be moved to the parent of parent A and - // parent B. Parent A and parent B will be removed. - let repeat = true - repeat: while (repeat) { - repeat = false - - for (let [sheetA, childrenA] of commonParents) { - for (let [sheetB, childrenB] of commonParents) { - if (sheetA === sheetB) continue - - // Ancestors from self to root. Reversed order so we find the - // nearest common parent first - // - // Including self because if you compare a sheet with its parent, - // then the parent is still the common sheet between the two. In - // this case, the parent is the root file. - let ancestorsA = [sheetA].concat(Array.from(sheetA.ancestors()).reverse()) - let ancestorsB = [sheetB].concat(Array.from(sheetB.ancestors()).reverse()) - - for (let parentA of ancestorsA) { - for (let parentB of ancestorsB) { - if (parentA !== parentB) continue - - // Found the parent - let parent = parentA - - commonParents.delete(sheetA) - commonParents.delete(sheetB) - - for (let child of childrenA) { - commonParents.get(parent).add(child) - } - - for (let child of childrenB) { - commonParents.get(parent).add(child) - } - - // Found a common parent between sheet A and sheet B. We can - // stop looking for more common parents between A and B, and - // continue with the next sheet. - repeat = true - continue repeat - } - } - } - } - } - - // Mark the common parent as the Tailwind CSS root file, and remove the - // flag from each leaf. - for (let [parent, children] of commonParents) { - parent.isTailwindRoot = true - - for (let child of children) { - if (parent === child) continue - - child.isTailwindRoot = false - } - } - return - } - } - - { - let error = `You have one or more stylesheets that are imported into a utility layer and non-utility layer.\n` - error += `We cannot convert stylesheets under these conditions. Please look at the following stylesheets:\n` - - throw new Error(error + lines.join('\n')) - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/fixtures/test.css b/packages/@tailwindcss-upgrade/src/codemods/css/fixtures/test.css deleted file mode 100644 index a15c877ac..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/fixtures/test.css +++ /dev/null @@ -1,3 +0,0 @@ -.foo { - color: red; -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.ts b/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.ts deleted file mode 100644 index 489f2d349..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.ts +++ /dev/null @@ -1,84 +0,0 @@ -import postcss, { type ChildNode, type Plugin, type Root } from 'postcss' -import { format, type Options } from 'prettier' -import { walk } from '../../utils/walk' - -const FORMAT_OPTIONS: Options = { - parser: 'css', - semi: true, - singleQuote: true, -} - -// Prettier is used to generate cleaner output, but it's only used on the nodes -// that were marked as `pretty` during the migration. -export function formatNodes(): Plugin { - async function migrate(root: Root) { - // Find the nodes to format - let nodesToFormat: ChildNode[] = [] - walk(root, (child, _idx, parent) => { - // Always print semicolons after at-rules - if (child.type === 'atrule') { - child.raws.semicolon = true - } - - if (child.type === 'atrule' && child.name === 'tw-bucket') { - nodesToFormat.push(child) - } else if (child.raws.tailwind_pretty) { - // @ts-expect-error We might not have a parent - child.parent ??= parent - nodesToFormat.unshift(child) - } - }) - - let output: string[] = [] - - // Format the nodes - for (let node of nodesToFormat) { - let contents = (() => { - if (node.type === 'atrule' && node.name === 'tw-bucket') { - // Remove the `@tw-bucket` wrapping, and use the contents directly. - return node - .toString() - .trim() - .replace(/@tw-bucket(.*?){([\s\S]*)}/, '$2') - } - - return node.toString() - })() - - // Do not format the user bucket to ensure we keep the user's formatting - // intact. - if (node.type === 'atrule' && node.name === 'tw-bucket' && node.params === 'user') { - output.push(contents) - continue - } - - // Format buckets - if (node.type === 'atrule' && node.name === 'tw-bucket') { - output.push(await format(contents, FORMAT_OPTIONS)) - continue - } - - // Format any other nodes - node.replaceWith( - postcss.parse( - `${node.raws.before ?? ''}${(await format(contents, FORMAT_OPTIONS)).trim()}`, - ), - ) - } - - root.removeAll() - root.append( - postcss.parse( - output - .map((bucket) => bucket.trim()) - .filter(Boolean) - .join('\n\n'), - ), - ) - } - - return { - postcssPlugin: '@tailwindcss/upgrade/format-nodes', - OnceExit: migrate, - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/link.ts b/packages/@tailwindcss-upgrade/src/codemods/css/link.ts deleted file mode 100644 index 04c2dd770..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/link.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { normalizePath } from '@tailwindcss/node' -import path from 'node:path' -import postcss from 'postcss' -import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' -import { Stylesheet } from '../../stylesheet' -import { error, highlight, relative, success } from '../../utils/renderer' -import { detectConfigPath } from '../template/prepare-config' - -export async function linkConfigs( - stylesheets: Stylesheet[], - { configPath, base }: { configPath: string | null; base: string }, -) { - let rootStylesheets = stylesheets.filter((sheet) => sheet.isTailwindRoot) - if (rootStylesheets.length === 0) { - throw new Error( - `Cannot find any CSS files that reference Tailwind CSS.\nBefore your project can be upgraded you need to create a CSS file that imports Tailwind CSS or uses ${highlight('@tailwind')}.`, - ) - } - let withoutAtConfig = rootStylesheets.filter((sheet) => { - let hasConfig = false - sheet.root.walkAtRules('config', (node) => { - let configPath = path.resolve(path.dirname(sheet.file!), node.params.slice(1, -1)) - sheet.linkedConfigPath = configPath - hasConfig = true - return false - }) - return !hasConfig - }) - - // All stylesheets have a `@config` directives - if (withoutAtConfig.length === 0) return - - // Find the config file path for each stylesheet - let configPathBySheet = new Map() - let sheetByConfigPath = new DefaultMap>(() => new Set()) - for (let sheet of withoutAtConfig) { - if (!sheet.file) continue - - let localConfigPath = configPath as string - if (configPath === null) { - localConfigPath = await detectConfigPath(path.dirname(sheet.file), base) - } else if (!path.isAbsolute(localConfigPath)) { - localConfigPath = path.resolve(base, localConfigPath) - } - - configPathBySheet.set(sheet, localConfigPath) - sheetByConfigPath.get(localConfigPath).add(sheet) - } - - let problematicStylesheets = new Set() - for (let sheets of sheetByConfigPath.values()) { - if (sheets.size > 1) { - for (let sheet of sheets) { - problematicStylesheets.add(sheet) - } - } - } - - // There are multiple "root" files without `@config` directives. Manual - // intervention is needed to link to the correct Tailwind config files. - if (problematicStylesheets.size > 1) { - for (let sheet of problematicStylesheets) { - error( - `Could not determine configuration file for: ${highlight(relative(sheet.file!, base))}\nUpdate your stylesheet to use ${highlight('@config')} to specify the correct configuration file explicitly and then run the upgrade tool again.`, - { prefix: '↳ ' }, - ) - } - - process.exit(1) - } - - let relativePath = relative - for (let [sheet, configPath] of configPathBySheet) { - try { - if (!sheet || !sheet.file) return - success( - `Linked ${highlight(relativePath(configPath, base))} to ${highlight(relativePath(sheet.file, base))}`, - { prefix: '↳ ' }, - ) - - // Link the `@config` directive to the root stylesheets - - // Track the config file path on the stylesheet itself for easy access - // without traversing the CSS ast and finding the corresponding - // `@config` later. - sheet.linkedConfigPath = configPath - - // Create a relative path from the current file to the config file. - let relative = path.relative(path.dirname(sheet.file), configPath) - - // If the path points to a file in the same directory, `path.relative` will - // remove the leading `./` and we need to add it back in order to still - // consider the path relative - if (!relative.startsWith('.') && !path.isAbsolute(relative)) { - relative = './' + relative - } - - relative = normalizePath(relative) - - // Add the `@config` directive to the root stylesheet. - { - let target = sheet.root as postcss.Root | postcss.AtRule - let atConfig = postcss.atRule({ name: 'config', params: `'${relative}'` }) - - sheet.root.walkAtRules((node) => { - if (node.name === 'tailwind' || node.name === 'import') { - target = node - } - }) - - if (target.type === 'root') { - sheet.root.prepend(atConfig) - } else if (target.type === 'atrule') { - target.after(atConfig) - } - } - } catch (e: any) { - error('Could not load the configuration file: ' + e.message, { prefix: '↳ ' }) - process.exit(1) - } - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts deleted file mode 100644 index 2004d0969..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts +++ /dev/null @@ -1,110 +0,0 @@ -import path from 'node:path' -import postcss, { AtRule, type Plugin } from 'postcss' -import { normalizePath } from '../../../../@tailwindcss-node/src/normalize-path' -import type { Stylesheet } from '../../stylesheet' -import type { JSConfigMigration } from '../config/migrate-js-config' - -const ALREADY_INJECTED = new WeakMap() - -export function migrateConfig( - sheet: Stylesheet, - { - configFilePath, - jsConfigMigration, - }: { configFilePath: string | null; jsConfigMigration: JSConfigMigration | null }, -): Plugin { - function migrate() { - if (!sheet.isTailwindRoot) return - if (!configFilePath) return - - let alreadyInjected = ALREADY_INJECTED.get(sheet) - if (alreadyInjected && alreadyInjected.includes(configFilePath)) { - return - } else if (alreadyInjected) { - alreadyInjected.push(configFilePath) - } else { - ALREADY_INJECTED.set(sheet, [configFilePath]) - } - - let root = sheet.root - - // We don't have a sheet with a file path - if (!sheet.file) return - - let cssConfig = new AtRule() - - // Remove the `@config` directive if it exists and we couldn't migrate the - // config file. - if (jsConfigMigration !== null) { - root.walkAtRules('config', (node) => { - node.remove() - }) - - let css = '\n\n' - css += '\n@tw-bucket source {' - for (let source of jsConfigMigration.sources) { - let absolute = path.resolve(source.base, source.pattern) - css += `@source '${relativeToStylesheet(sheet, absolute)}';\n` - } - css += '}\n' - - css += '\n@tw-bucket plugin {\n' - for (let plugin of jsConfigMigration.plugins) { - let relative = - plugin.path[0] === '.' - ? relativeToStylesheet(sheet, path.resolve(plugin.base, plugin.path)) - : plugin.path - - if (plugin.options === null) { - css += `@plugin '${relative}';\n` - } else { - css += `@plugin '${relative}' {\n` - for (let [property, value] of Object.entries(plugin.options)) { - let cssValue = '' - if (typeof value === 'string') { - cssValue = quoteString(value) - } else if (Array.isArray(value)) { - cssValue = value - .map((v) => (typeof v === 'string' ? quoteString(v) : '' + v)) - .join(', ') - } else { - cssValue = '' + value - } - - css += ` ${property}: ${cssValue};\n` - } - css += '}\n' // @plugin - } - } - css += '}\n' // @tw-bucket - - cssConfig.append(postcss.parse(css + jsConfigMigration.css)) - } - - // Inject the `@config` directive - root.append(cssConfig.nodes) - } - - return { - postcssPlugin: '@tailwindcss/upgrade/migrate-config', - OnceExit: migrate, - } -} - -function relativeToStylesheet(sheet: Stylesheet, absolute: string) { - if (!sheet.file) throw new Error('Can not find a path for the stylesheet') - - let sheetPath = sheet.file - - let relative = path.relative(path.dirname(sheetPath), absolute) - if (relative[0] !== '.') { - relative = `./${relative}` - } - // Ensure relative is a POSIX style path since we will merge it with the - // glob. - return normalizePath(relative) -} - -function quoteString(value: string): string { - return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'` -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.test.ts deleted file mode 100644 index 4fba6de52..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import dedent from 'dedent' -import postcss from 'postcss' -import { expect, it } from 'vitest' -import { migrateImport } from './migrate-import' - -const css = dedent - -async function migrate(input: string) { - return postcss() - .use(migrateImport()) - .process(input, { from: expect.getState().testPath }) - .then((result) => result.css) -} - -it('prints relative file imports as relative paths', async () => { - expect( - await migrate(css` - @import url('https://example.com'); - - @import 'fixtures/test'; - @import 'fixtures/test.css'; - @import './fixtures/test.css'; - @import './fixtures/test'; - - @import 'fixtures/test' screen; - @import 'fixtures/test.css' screen; - @import './fixtures/test.css' screen; - @import './fixtures/test' screen; - - @import 'fixtures/test' supports(display: grid); - @import 'fixtures/test.css' supports(display: grid); - @import './fixtures/test.css' supports(display: grid); - @import './fixtures/test' supports(display: grid); - - @import 'fixtures/test' layer(utilities); - @import 'fixtures/test.css' layer(utilities); - @import './fixtures/test.css' layer(utilities); - @import './fixtures/test' layer(utilities); - - @import 'fixtures/test' theme(inline); - @import 'fixtures/test.css' theme(inline); - @import './fixtures/test.css' theme(inline); - @import './fixtures/test' theme(inline); - - @import 'fixtures/test' layer(utilities) supports(display: grid) screen and (min-width: 600px); - @import 'fixtures/test.css' layer(utilities) supports(display: grid) screen and - (min-width: 600px); - @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and - (min-width: 600px); - @import './fixtures/test' layer(utilities) supports(display: grid) screen and - (min-width: 600px); - - @import 'tailwindcss'; - @import 'tailwindcss/theme.css'; - @import 'tailwindcss/theme'; - `), - ).toMatchInlineSnapshot(` - "@import url('https://example.com'); - - @import './fixtures/test.css'; - @import './fixtures/test.css'; - @import './fixtures/test.css'; - @import './fixtures/test.css'; - - @import './fixtures/test.css' screen; - @import './fixtures/test.css' screen; - @import './fixtures/test.css' screen; - @import './fixtures/test.css' screen; - - @import './fixtures/test.css' supports(display: grid); - @import './fixtures/test.css' supports(display: grid); - @import './fixtures/test.css' supports(display: grid); - @import './fixtures/test.css' supports(display: grid); - - @import './fixtures/test.css' layer(utilities); - @import './fixtures/test.css' layer(utilities); - @import './fixtures/test.css' layer(utilities); - @import './fixtures/test.css' layer(utilities); - - @import './fixtures/test.css' theme(inline); - @import './fixtures/test.css' theme(inline); - @import './fixtures/test.css' theme(inline); - @import './fixtures/test.css' theme(inline); - - @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and (min-width: 600px); - @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and - (min-width: 600px); - @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and - (min-width: 600px); - @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and - (min-width: 600px); - - @import 'tailwindcss'; - @import 'tailwindcss/theme.css'; - @import 'tailwindcss/theme';" - `) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.ts deleted file mode 100644 index 419f5aa32..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.ts +++ /dev/null @@ -1,52 +0,0 @@ -import fs from 'node:fs/promises' -import { dirname, resolve } from 'node:path' -import { type Plugin, type Root } from 'postcss' -import { parseImportParams } from '../../../../tailwindcss/src/at-import' -import { segment } from '../../../../tailwindcss/src/utils/segment' -import * as ValueParser from '../../../../tailwindcss/src/value-parser' - -export function migrateImport(): Plugin { - async function migrate(root: Root) { - let file = root.source?.input.file - if (!file) return - - let promises: Promise[] = [] - root.walkAtRules('import', (rule) => { - try { - let [firstParam, ...rest] = segment(rule.params, ' ') - - let params = parseImportParams(ValueParser.parse(firstParam)) - if (!params) return - - let isRelative = params.uri[0] === '.' - let hasCssExtension = params.uri.endsWith('.css') - - if (isRelative && hasCssExtension) { - return - } - - let fullPath = resolve(dirname(file), params.uri) - if (!hasCssExtension) fullPath += '.css' - - promises.push( - fs.stat(fullPath).then(() => { - let ext = hasCssExtension ? '' : '.css' - let path = isRelative ? params.uri : `./${params.uri}` - rule.params = [`'${path}${ext}'`, ...rest].join(' ') - }), - ) - } catch { - // When an error occurs while parsing the `@import` statement, we skip - // the import. This will happen in cases where you import an external - // URL. - } - }) - - await Promise.allSettled(promises) - } - - return { - postcssPlugin: '@tailwindcss/upgrade/migrate-import', - OnceExit: migrate, - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts deleted file mode 100644 index c10e9ee99..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { type Plugin, type Root } from 'postcss' -import { resolveConfig } from '../../../../tailwindcss/src/compat/config/resolve-config' -import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' -import { buildMediaQuery } from '../../../../tailwindcss/src/compat/screens-config' -import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' - -export function migrateMediaScreen({ - designSystem, - userConfig, -}: { - designSystem?: DesignSystem | null - userConfig?: Config | null -} = {}): Plugin { - function migrate(root: Root) { - if (!designSystem || !userConfig) return - - let { resolvedConfig } = resolveConfig(designSystem, [ - { base: '', config: userConfig, reference: false, src: undefined }, - ]) - let screens = resolvedConfig?.theme?.screens || {} - - let mediaQueries = new DefaultMap((name) => { - let value = designSystem?.resolveThemeValue(`--breakpoint-${name}`, true) ?? screens?.[name] - if (typeof value === 'string') return `(width >= theme(--breakpoint-${name}))` - return value ? buildMediaQuery(value) : null - }) - - // First migrate `@screen md` to `@media screen(md)` - root.walkAtRules('screen', (node) => { - node.name = 'media' - node.params = `screen(${node.params})` - }) - - // Then migrate the `screen(…)` function - root.walkAtRules((rule) => { - if (rule.name !== 'media') return - - let screen = rule.params.match(/screen\(([^)]+)\)/) - if (!screen) return - - let value = mediaQueries.get(screen[1]) - if (!value) return - - rule.params = value - }) - } - - return { - postcssPlugin: '@tailwindcss/upgrade/migrate-media-screen', - OnceExit: migrate, - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts deleted file mode 100644 index 2d8b70041..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import dedent from 'dedent' -import postcss from 'postcss' -import { expect, it, vi } from 'vitest' -import * as versions from '../../utils/version' -import { formatNodes } from './format-nodes' -import { migratePreflight } from './migrate-preflight' -import { sortBuckets } from './sort-buckets' -vi.spyOn(versions, 'isMajor').mockReturnValue(true) - -const css = dedent - -async function migrate(input: string) { - let designSystem = await __unstable__loadDesignSystem( - css` - @import 'tailwindcss'; - `, - { base: __dirname }, - ) - - return postcss() - .use(migratePreflight({ designSystem })) - .use(sortBuckets()) - .use(formatNodes()) - .process(input, { from: expect.getState().testPath }) - .then((result) => result.css) -} - -it("should add compatibility CSS after the `@import 'tailwindcss'`", async () => { - expect( - await migrate(css` - @import 'tailwindcss'; - `), - ).toMatchInlineSnapshot(` - "@import 'tailwindcss'; - - /* - 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); - } - }" - `) -}) - -it('should add the compatibility CSS after the last `@import`', async () => { - expect( - await migrate(css` - @import 'tailwindcss'; - @import './foo.css'; - @import './bar.css'; - `), - ).toMatchInlineSnapshot(` - "@import 'tailwindcss'; - @import './foo.css'; - @import './bar.css'; - - /* - 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); - } - }" - `) -}) - -it('should add the compatibility CSS after the last import, even if a body-less `@layer` exists', async () => { - expect( - await migrate(css` - @charset "UTF-8"; - @layer foo, bar, baz, base; - - /**! - * License header - */ - - @import 'tailwindcss'; - @import './foo.css'; - @import './bar.css'; - `), - ).toMatchInlineSnapshot(` - "@charset "UTF-8"; - @layer foo, bar, baz, base; - - /**! - * License header - */ - - @import 'tailwindcss'; - @import './foo.css'; - @import './bar.css'; - - /* - 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); - } - }" - `) -}) - -it('should add the compatibility CSS before the first `@layer base` (if the "tailwindcss" import exists)', async () => { - expect( - await migrate(css` - @import 'tailwindcss'; - - @custom-variant foo { - } - - @utility bar { - } - - @layer base { - } - - @utility baz { - } - - @layer base { - } - `), - ).toMatchInlineSnapshot(` - "@import 'tailwindcss'; - - @custom-variant foo { - } - - /* - 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); - } - } - - @utility bar { - } - - @utility baz { - } - - @layer base { - } - - @layer base { - }" - `) -}) - -it('should add the compatibility CSS before the first `@layer base` (if the "tailwindcss/preflight" import exists)', async () => { - expect( - await migrate(css` - @import 'tailwindcss/preflight'; - - @custom-variant foo { - } - - @utility bar { - } - - @layer base { - } - - @utility baz { - } - - @layer base { - } - `), - ).toMatchInlineSnapshot(` - "@import 'tailwindcss/preflight'; - - @custom-variant foo { - } - - /* - 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); - } - } - - @utility bar { - } - - @utility baz { - } - - @layer base { - } - - @layer base { - }" - `) -}) - -it('should not add the backwards compatibility CSS when no `@import "tailwindcss"` or `@import "tailwindcss/preflight"` exists', async () => { - expect( - await migrate(css` - @custom-variant foo { - } - - @utility bar { - } - - @layer base { - } - - @utility baz { - } - - @layer base { - } - `), - ).toMatchInlineSnapshot(` - "@custom-variant foo { - } - - @utility bar { - } - - @utility baz { - } - - @layer base { - } - - @layer base { - }" - `) -}) - -it('should not add the backwards compatibility CSS when another `@import "tailwindcss"` import exists such as theme or utilities', async () => { - expect( - await migrate(css` - @import 'tailwindcss/theme'; - - @custom-variant foo { - } - - @utility bar { - } - - @layer base { - } - - @utility baz { - } - - @layer base { - } - `), - ).toMatchInlineSnapshot(` - "@import 'tailwindcss/theme'; - - @custom-variant foo { - } - - @utility bar { - } - - @utility baz { - } - - @layer base { - } - - @layer base { - }" - `) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts deleted file mode 100644 index d32d96979..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts +++ /dev/null @@ -1,184 +0,0 @@ -import dedent from 'dedent' -import postcss, { type Plugin, type Root } from 'postcss' -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 { toKeyPath } from '../../../../tailwindcss/src/utils/to-key-path' -import * as ValueParser from '../../../../tailwindcss/src/value-parser' -import { walk, WalkAction } from '../../../../tailwindcss/src/walk' -import * as version from '../../utils/version' - -// Defaults in v4 -const DEFAULT_BORDER_COLOR = 'currentcolor' - -const css = dedent -const BORDER_COLOR_COMPATIBILITY_CSS = css` - /* - 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: theme(borderColor.DEFAULT); - } - } -` - -export function migratePreflight({ - designSystem, - userConfig, -}: { - designSystem: DesignSystem | null - userConfig?: Config | null -}): Plugin { - // @ts-expect-error - let defaultBorderColor = userConfig?.theme?.borderColor?.DEFAULT - - function canResolveThemeValue(path: string) { - if (!designSystem) return false - let variable = `--${keyPathToCssProperty(toKeyPath(path))}` as const - return Boolean(designSystem.theme.get([variable])) - } - - function migrate(root: Root) { - // CSS for backwards compatibility with v3 should only injected in v3 - // projects and not v4 projects. - if (!version.isMajor(3)) return - - let isTailwindRoot = false - root.walkAtRules('import', (node) => { - if ( - /['"]tailwindcss['"]/.test(node.params) || - /['"]tailwindcss\/preflight['"]/.test(node.params) - ) { - isTailwindRoot = true - return false - } - }) - - if (!isTailwindRoot) return - - // Figure out the compatibility CSS to inject - let compatibilityCssString = '' - if (defaultBorderColor !== DEFAULT_BORDER_COLOR) { - compatibilityCssString += BORDER_COLOR_COMPATIBILITY_CSS - compatibilityCssString += '\n\n' - } - - compatibilityCssString = `\n@tw-bucket compatibility {\n${compatibilityCssString}\n}\n` - let compatibilityCss = postcss.parse(compatibilityCssString) - - // Replace the `theme(…)` with v3 values if we can't resolve the theme - // value. - compatibilityCss.walkDecls((decl) => { - if (decl.value.includes('theme(')) { - decl.value = substituteFunctionsInValue(ValueParser.parse(decl.value), (path) => { - if (canResolveThemeValue(path)) { - return defaultBorderColor - } else { - if (path === 'borderColor.DEFAULT') { - return 'var(--color-gray-200, currentcolor)' - } - } - return null - }) - } - }) - - // Cleanup `--border-color` definition in `theme(…)` - root.walkAtRules('theme', (node) => { - node.walkDecls('--border-color', (decl) => { - decl.remove() - }) - - if (node.nodes?.length === 0) { - node.remove() - } - }) - - // Inject the compatibility CSS - root.append(compatibilityCss) - } - - return { - postcssPlugin: '@tailwindcss/upgrade/migrate-preflight', - OnceExit: migrate, - } -} - -function substituteFunctionsInValue( - ast: ValueParser.ValueAstNode[], - handle: (value: string, fallback?: string) => string | null, -) { - walk(ast, (node) => { - 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 - - 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 -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.test.ts deleted file mode 100644 index f0b4844ad..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import dedent from 'dedent' -import postcss from 'postcss' -import { expect, it } from 'vitest' -import { formatNodes } from './format-nodes' -import { migrateThemeToVar } from './migrate-theme-to-var' -import { sortBuckets } from './sort-buckets' - -const css = dedent - -async function migrate(input: string) { - return postcss() - .use( - migrateThemeToVar({ - designSystem: await __unstable__loadDesignSystem(`@import 'tailwindcss';`, { - base: __dirname, - }), - }), - ) - .use(sortBuckets()) - .use(formatNodes()) - .process(input, { from: expect.getState().testPath }) - .then((result) => result.css) -} - -it('should migrate `theme(…)` to `var(…)`', async () => { - expect( - await migrate(css` - @media theme(screens.sm) { - .foo { - background-color: theme(colors.red.900); - color: theme(colors.red.900 / 75%); - border-color: theme(colors.red.200/75%); - } - } - `), - ).toMatchInlineSnapshot(` - "@media --theme(--breakpoint-sm) { - .foo { - background-color: var(--color-red-900); - color: --theme(--color-red-900 / 75%); - border-color: --theme(--color-red-200 / 75%); - } - }" - `) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts deleted file mode 100644 index 8285def1e..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { type Plugin } from 'postcss' -import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { Convert, createConverter } from '../template/migrate-theme-to-var' - -export function migrateThemeToVar({ - designSystem, -}: { - designSystem?: DesignSystem | null -} = {}): Plugin { - return { - postcssPlugin: '@tailwindcss/upgrade/migrate-theme-to-var', - OnceExit(root) { - if (!designSystem) return - let convert = createConverter(designSystem, { prettyPrint: true }) - - root.walkDecls((decl) => { - let [newValue] = convert(decl.value) - decl.value = newValue - }) - - root.walkAtRules((atRule) => { - if ( - atRule.name === 'media' || - atRule.name === 'custom-media' || - atRule.name === 'container' || - atRule.name === 'supports' - ) { - let [newValue] = convert(atRule.params, Convert.MigrateThemeOnly) - atRule.params = newValue - } - }) - }, - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.test.ts deleted file mode 100644 index 3521d34f7..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import dedent from 'dedent' -import postcss from 'postcss' -import { expect, it } from 'vitest' -import { formatNodes } from './format-nodes' -import { migrateVariantsDirective } from './migrate-variants-directive' -import { sortBuckets } from './sort-buckets' - -const css = dedent - -function migrate(input: string) { - return postcss() - .use(migrateVariantsDirective()) - .use(sortBuckets()) - .use(formatNodes()) - .process(input, { from: expect.getState().testPath }) - .then((result) => result.css) -} - -it('should replace `@variants` with `@layer utilities`', async () => { - expect( - await migrate(css` - @variants hover, focus { - .foo { - color: red; - } - } - `), - ).toMatchInlineSnapshot(` - "@layer utilities { - .foo { - color: red; - } - }" - `) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.ts deleted file mode 100644 index 1592c0059..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { type Plugin, type Root } from 'postcss' - -export function migrateVariantsDirective(): Plugin { - function migrate(root: Root) { - root.walkAtRules('variants', (node) => { - // Migrate `@variants` to `@utility` because `@variants` make the classes - // an actual utility. - // ```css - // @variants hover { - // .foo {} - // } - // ``` - // - // Means that you can do this in your HTML: - // ```html - //
- // ``` - // - // Notice the `focus:`, even though we _only_ configured the `hover` - // variant. - // - // This means that we can convert it to an `@layer utilities` rule. Later, - // this will get converted to an `@utility` rule. - if (node.name === 'variants') { - node.name = 'layer' - node.params = 'utilities' - } - }) - } - - return { - postcssPlugin: '@tailwindcss/upgrade/migrate-variants-directive', - OnceExit: migrate, - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts deleted file mode 100644 index cb9150851..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts +++ /dev/null @@ -1,57 +0,0 @@ -import postcss from 'postcss' -import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' -import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { Stylesheet } from '../../stylesheet' -import type { JSConfigMigration } from '../config/migrate-js-config' -import { migrateAtApply } from './migrate-at-apply' -import { migrateAtLayerUtilities } from './migrate-at-layer-utilities' -import { migrateConfig } from './migrate-config' -import { migrateImport } from './migrate-import' -import { migrateMediaScreen } from './migrate-media-screen' -import { migrateMissingLayers } from './migrate-missing-layers' -import { migratePreflight } from './migrate-preflight' -import { migrateTailwindDirectives } from './migrate-tailwind-directives' -import { migrateThemeToVar } from './migrate-theme-to-var' -import { migrateVariantsDirective } from './migrate-variants-directive' - -export interface MigrateOptions { - newPrefix: string | null - designSystem: DesignSystem | null - userConfig: Config | null - configFilePath: string | null - jsConfigMigration: JSConfigMigration | null -} - -export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) { - if (!stylesheet.file) { - throw new Error('Cannot migrate a stylesheet without a file path') - } - - if (!stylesheet.canMigrate) return - - await migrateContents(stylesheet, options) -} - -export async function migrateContents( - stylesheet: Stylesheet | string, - options: MigrateOptions, - file?: string, -) { - if (typeof stylesheet === 'string') { - stylesheet = await Stylesheet.fromString(stylesheet) - stylesheet.file = file ?? null - } - - return postcss() - .use(migrateImport()) - .use(migrateAtApply(options)) - .use(migrateMediaScreen(options)) - .use(migrateVariantsDirective()) - .use(migrateAtLayerUtilities(stylesheet)) - .use(migrateMissingLayers()) - .use(migrateTailwindDirectives(options)) - .use(migrateConfig(stylesheet, options)) - .use(migratePreflight(options)) - .use(migrateThemeToVar(options)) - .process(stylesheet.root, { from: stylesheet.file ?? undefined }) -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/sort-buckets.ts b/packages/@tailwindcss-upgrade/src/codemods/css/sort-buckets.ts deleted file mode 100644 index 22b0581dd..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/css/sort-buckets.ts +++ /dev/null @@ -1,161 +0,0 @@ -import postcss, { type AtRule, type ChildNode, type Comment, type Plugin, type Root } from 'postcss' -import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' -import { walk, WalkAction } from '../../utils/walk' - -const BUCKET_ORDER = [ - // Imports - 'import', // @import - - // Configuration - 'config', // @config - 'plugin', // @plugin - 'source', // @source - 'custom-variant', // @custom-variant - 'theme', // @theme - - // Styles - 'compatibility', // @layer base with compatibility CSS - 'utility', // @utility - - // User CSS - 'user', -] - -export function sortBuckets(): Plugin { - async function migrate(root: Root) { - // 1. Move items that are not in a bucket, into a bucket - { - let comments: Comment[] = [] - - let buckets = new DefaultMap((name) => { - let bucket = postcss.atRule({ name: 'tw-bucket', params: name, nodes: [] }) - root.append(bucket) - return bucket - }) - - // Seed the buckets with existing buckets - root.walkAtRules('tw-bucket', (node) => { - buckets.set(node.params, node) - }) - - let lastLayer = 'user' - function injectInto(name: string, ...nodes: ChildNode[]) { - lastLayer = name - buckets.get(name).nodes?.push(...comments.splice(0), ...nodes) - } - - walk(root, (node) => { - // Already in a bucket, skip it - if (node.type === 'atrule' && node.name === 'tw-bucket') { - return WalkAction.Skip - } - - // Comments belong to the bucket of the nearest node, which is typically - // in the "next" bucket. - if (node.type === 'comment') { - // We already have comments, which means that we already have nodes - // that belong in the next bucket, so we should move the current - // comment into the next bucket as well. - if (comments.length > 0) { - comments.push(node) - return - } - - // Figure out the closest node to the comment - let prevDistance = distance(node.prev(), node) ?? Infinity - let nextDistance = distance(node, node.next()) ?? Infinity - - if (prevDistance < nextDistance) { - buckets.get(lastLayer).nodes?.push(node) - } else { - comments.push(node) - } - } - - // Known at-rules - else if ( - node.type === 'atrule' && - ['config', 'plugin', 'source', 'theme', 'utility', 'custom-variant'].includes(node.name) - ) { - injectInto(node.name, node) - } - - // Imports bucket, which also contains the `@charset` and body-less `@layer` - else if ( - (node.type === 'atrule' && node.name === 'layer' && !node.nodes) || // @layer foo, bar; - (node.type === 'atrule' && node.name === 'import') || - (node.type === 'atrule' && node.name === 'charset') || // @charset "UTF-8"; - (node.type === 'atrule' && node.name === 'tailwind') - ) { - injectInto('import', node) - } - - // User CSS - else if (node.type === 'rule' || node.type === 'atrule') { - injectInto('user', node) - } - - // Fallback - else { - injectInto('user', node) - } - - return WalkAction.Skip - }) - - if (comments.length > 0) { - injectInto(lastLayer) - } - } - - // 2. Merge `@tw-bucket` with the same name together - let firstBuckets = new Map() - root.walkAtRules('tw-bucket', (node) => { - let firstBucket = firstBuckets.get(node.params) - if (!firstBucket) { - firstBuckets.set(node.params, node) - return - } - - if (node.nodes) { - firstBucket.append(...node.nodes) - } - }) - - // 3. Remove empty `@tw-bucket` - root.walkAtRules('tw-bucket', (node) => { - if (!node.nodes?.length) { - node.remove() - } - }) - - // 4. Sort the `@tw-bucket` themselves - { - let sorted = Array.from(firstBuckets.values()).sort((a, z) => { - let aIndex = BUCKET_ORDER.indexOf(a.params) - let zIndex = BUCKET_ORDER.indexOf(z.params) - return aIndex - zIndex - }) - - // Re-inject the sorted buckets - root.removeAll() - root.append(sorted) - } - } - - return { - postcssPlugin: '@tailwindcss/upgrade/sort-buckets', - OnceExit: migrate, - } -} - -function distance(before?: ChildNode, after?: ChildNode): number | null { - if (!before || !after) return null - if (!before.source || !after.source) return null - if (!before.source.start || !after.source.start) return null - if (!before.source.end || !after.source.end) return null - - // Compare end of Before, to start of After - let d = Math.abs(before.source.end.line - after.source.start.line) - return d -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts similarity index 52% rename from packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.test.ts rename to packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts index 9d6393a02..517c517c0 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts @@ -1,13 +1,12 @@ import postcss, { type Plugin } from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './format-nodes' -import { sortBuckets } from './sort-buckets' function markPretty(): Plugin { return { postcssPlugin: '@tailwindcss/upgrade/mark-pretty', OnceExit(root) { - root.walkAtRules('tw-format', (atRule) => { + root.walkAtRules('utility', (atRule) => { atRule.raws.tailwind_pretty = true }) }, @@ -17,14 +16,16 @@ function markPretty(): Plugin { function migrate(input: string) { return postcss() .use(markPretty()) - .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) } -it('should format PostCSS nodes', async () => { - expect(await migrate(`@utility .foo { .foo { color: red; } }`)).toMatchInlineSnapshot(` +it('should format PostCSS nodes that are marked with tailwind_pretty', async () => { + expect( + await migrate(` + @utility .foo { .foo { color: red; } }`), + ).toMatchInlineSnapshot(` "@utility .foo { .foo { color: red; @@ -32,14 +33,3 @@ it('should format PostCSS nodes', async () => { }" `) }) - -it('should format PostCSS nodes in the `user` bucket', async () => { - expect(await migrate(`@tw-bucket user { @tw-format .bar { .foo { color: red; } } }`)) - .toMatchInlineSnapshot(` - "@tw-format .bar { - .foo { - color: red; - } - }" - `) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts new file mode 100644 index 000000000..e90e0e263 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts @@ -0,0 +1,30 @@ +import { parse, type ChildNode, type Plugin, type Root } from 'postcss' +import { format } from 'prettier' +import { walk, WalkAction } from '../utils/walk' + +// Prettier is used to generate cleaner output, but it's only used on the nodes +// that were marked as `pretty` during the migration. +export function formatNodes(): Plugin { + async function migrate(root: Root) { + // Find the nodes to format + let nodesToFormat: ChildNode[] = [] + walk(root, (child) => { + if (child.raws.tailwind_pretty) { + nodesToFormat.push(child) + return WalkAction.Skip + } + }) + + // Format the nodes + await Promise.all( + nodesToFormat.map(async (node) => { + node.replaceWith(parse(await format(node.toString(), { parser: 'css', semi: true }))) + }), + ) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/format-nodes', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts similarity index 53% rename from packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts index 9f6a92008..7e8428b38 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts @@ -1,11 +1,9 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' import postcss from 'postcss' -import { expect, it, vi } from 'vitest' -import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' -import * as versions from '../../utils/version' +import type { Config } from 'tailwindcss' +import { expect, it } from 'vitest' import { migrateAtApply } from './migrate-at-apply' -vi.spyOn(versions, 'isMajor').mockReturnValue(true) const css = dedent @@ -13,11 +11,6 @@ async function migrate(input: string, config: Config = {}) { let designSystem = await __unstable__loadDesignSystem( css` @import 'tailwindcss'; - - /* TODO(perf): Only here to speed up the tests */ - @theme { - --*: initial; - } `, { base: __dirname }, ) @@ -61,6 +54,21 @@ it('should append `!` to each utility, when using `!important`', async () => { `) }) +// TODO: Handle SCSS syntax +it.skip('should append `!` to each utility, when using `#{!important}`', async () => { + expect( + await migrate(css` + .foo { + @apply flex flex-col #{!important}; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex! flex-col!; + }" + `) +}) + it('should move the legacy `!` prefix, to the new `!` postfix notation', async () => { expect( await migrate(css` @@ -75,44 +83,35 @@ it('should move the legacy `!` prefix, to the new `!` postfix notation', async ( `) }) -it( - 'should apply all candidate migration when migrating with a config', - { timeout: 10_000 }, - async () => { - async function migrateWithPrefix(input: string) { - return postcss() - .use( - migrateAtApply({ - designSystem: await __unstable__loadDesignSystem( - css` - @import 'tailwindcss' prefix(tw); +it('should apply all candidate migration when migrating with a config', async () => { + async function migrateWithPrefix(input: string) { + return postcss() + .use( + migrateAtApply({ + designSystem: await __unstable__loadDesignSystem( + css` + @import 'tailwindcss' prefix(tw); + `, + { base: __dirname }, + ), + userConfig: { + prefix: 'tw_', + }, + }), + ) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) + } - /* TODO(perf): Only here to speed up the tests */ - @theme { - --*: initial; - } - `, - { base: __dirname }, - ), - userConfig: { - prefix: 'tw_', - }, - }), - ) - .process(input, { from: expect.getState().testPath }) - .then((result) => result.css) - } - - expect( - await migrateWithPrefix(css` - .foo { - @apply !tw_flex [color:--my-color] tw_bg-gradient-to-t; - } - `), - ).toMatchInlineSnapshot(` + expect( + await migrateWithPrefix(css` + .foo { + @apply !tw_flex [color:--my-color] tw_bg-gradient-to-t; + } + `), + ).toMatchInlineSnapshot(` ".foo { - @apply tw:flex! tw:text-(--my-color) tw:bg-linear-to-t; + @apply tw:flex! tw:[color:var(--my-color)] tw:bg-linear-to-t; }" `) - }, -) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts similarity index 54% rename from packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts index 69f92688d..988203b2f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts @@ -1,15 +1,15 @@ import type { AtRule, Plugin } from 'postcss' -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 type { Config } from 'tailwindcss' +import type { DesignSystem } from '../../../tailwindcss/src/design-system' +import { segment } from '../../../tailwindcss/src/utils/segment' import { migrateCandidate } from '../template/migrate' export function migrateAtApply({ designSystem, userConfig, }: { - designSystem: DesignSystem | null - userConfig: Config | null + designSystem: DesignSystem + userConfig: Config }): Plugin { function migrate(atRule: AtRule) { let utilities = atRule.params.split(/(\s+)/) @@ -34,28 +34,17 @@ export function migrateAtApply({ return [...variants, utility].join(':') }) - return async () => { - if (!designSystem) return + // If we have a valid designSystem and config setup, we can run all + // candidate migrations on each utility + params = params.map((param) => migrateCandidate(designSystem, userConfig, param)) - // If we have a valid designSystem and config setup, we can run all - // candidate migrations on each utility - params = await Promise.all( - params.map(async (param) => await migrateCandidate(designSystem, userConfig, param)), - ) - - atRule.params = params.join('').trim() - } + atRule.params = params.join('').trim() } return { postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply', - async OnceExit(root) { - let migrations: (() => void)[] = [] - root.walkAtRules('apply', (atRule) => { - migrations.push(migrate(atRule)) - }) - - await Promise.allSettled(migrations.map((m) => m())) + OnceExit(root) { + root.walkAtRules('apply', migrate) }, } } diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts similarity index 88% rename from packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts index e115951d3..cbf96d58f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts @@ -1,12 +1,9 @@ import dedent from 'dedent' import postcss from 'postcss' -import { describe, expect, it, vi } from 'vitest' -import { Stylesheet } from '../../stylesheet' -import * as versions from '../../utils/version' +import { describe, expect, it } from 'vitest' +import { Stylesheet } from '../stylesheet' import { formatNodes } from './format-nodes' import { migrateAtLayerUtilities } from './migrate-at-layer-utilities' -import { sortBuckets } from './sort-buckets' -vi.spyOn(versions, 'isMajor').mockReturnValue(true) const css = dedent @@ -36,10 +33,9 @@ async function migrate( return postcss() .use(migrateAtLayerUtilities(stylesheet)) - .use(sortBuckets()) .use(formatNodes()) .process(stylesheet.root!, { from: expect.getState().testPath }) - .then((result) => `\n${result.css.trim()}\n`) + .then((result) => result.css) } it('should migrate simple `@layer utilities` to `@utility`', async () => { @@ -52,11 +48,9 @@ it('should migrate simple `@layer utilities` to `@utility`', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; - } - " + }" `) }) @@ -71,15 +65,13 @@ it('should split multiple selectors in separate utilities', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; } @utility bar { color: red; - } - " + }" `) }) @@ -103,16 +95,14 @@ it('should merge `@utility` with the same name', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; font-weight: bold; } .bar { color: blue; - } - " + }" `) }) @@ -155,19 +145,7 @@ it('should leave non-class utilities alone', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { - /* 2. */ - /* 2.1. */ - color: red; - /* 2.2. */ - .bar { - /* 2.2.1. */ - font-weight: bold; - } - } - - @layer utilities { + "@layer utilities { /* 1. */ #before { /* 1.1. */ @@ -190,57 +168,17 @@ it('should leave non-class utilities alone', async () => { } } } - " - `) -}) -it('should not convert empty classes into `@utility` since that would make them invalid', async () => { - expect( - await migrate(css` - @layer utilities { - .good { - color: red; - } - - .bad-empty { - } - - /* Comment above */ - .bad-with-comment-above { - } - - .bad-with-comment-inside { - /* Comment inside */ - } - - .bad-with-comment-below { - } - /* Comment below */ - } - `), - ).toMatchInlineSnapshot(` - " - @utility good { + @utility foo { + /* 2. */ + /* 2.1. */ color: red; - } - - @layer utilities { - .bad-empty { + /* 2.2. */ + .bar { + /* 2.2.1. */ + font-weight: bold; } - - /* Comment above */ - .bad-with-comment-above { - } - - .bad-with-comment-inside { - /* Comment inside */ - } - - .bad-with-comment-below { - } - /* Comment below */ - } - " + }" `) }) @@ -262,8 +200,7 @@ it('should migrate simple `@layer utilities` with nesting to `@utility`', async } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; &:hover { @@ -273,8 +210,7 @@ it('should migrate simple `@layer utilities` with nesting to `@utility`', async &:focus { color: green; } - } - " + }" `) }) @@ -292,15 +228,13 @@ it('should migrate multiple simple `@layer utilities` to `@utility`', async () = } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; } @utility bar { color: blue; - } - " + }" `) }) @@ -322,8 +256,7 @@ it('should not migrate Rules inside of Rules to a `@utility`', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { color: red; } @@ -333,8 +266,7 @@ it('should not migrate Rules inside of Rules to a `@utility`', async () => { .baz { color: green; } - } - " + }" `) }) @@ -350,13 +282,11 @@ it('should invert at-rules to make them migrate-able', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { @media (min-width: 640px) { color: red; } - } - " + }" `) }) @@ -380,8 +310,7 @@ it('should migrate at-rules with multiple utilities and invert them', async () = } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { @media (min-width: 640px) { color: red; } @@ -391,8 +320,7 @@ it('should migrate at-rules with multiple utilities and invert them', async () = @media (min-width: 640px) { color: blue; } - } - " + }" `) }) @@ -424,8 +352,7 @@ it('should migrate deeply nested at-rules with multiple utilities and invert the } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { @media (min-width: 640px) { color: red; } @@ -453,8 +380,7 @@ it('should migrate deeply nested at-rules with multiple utilities and invert the } } } - } - " + }" `) }) @@ -468,13 +394,11 @@ it('should migrate classes with pseudo elements', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { &::-webkit-scrollbar { display: none; } - } - " + }" `) }) @@ -488,13 +412,11 @@ it('should migrate classes with attribute selectors', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { - &[data-checked=''] { + "@utility no-scrollbar { + &[data-checked=""] { display: none; } - } - " + }" `) }) @@ -508,13 +430,11 @@ it('should migrate classes with element selectors', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { & main { display: none; } - } - " + }" `) }) @@ -528,13 +448,11 @@ it('should migrate classes attached to an element selector', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { &main { display: none; } - } - " + }" `) }) @@ -548,13 +466,11 @@ it('should migrate classes with id selectors', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { &#main { display: none; } - } - " + }" `) }) @@ -568,8 +484,7 @@ it('should migrate classes with another attached class', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility no-scrollbar { + "@utility no-scrollbar { &.main { display: none; } @@ -579,8 +494,7 @@ it('should migrate classes with another attached class', async () => { &.no-scrollbar { display: none; } - } - " + }" `) }) @@ -594,8 +508,7 @@ it('should migrate a selector with multiple classes to multiple @utility definit } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { & .bar:hover .baz:focus { display: none; } @@ -611,8 +524,7 @@ it('should migrate a selector with multiple classes to multiple @utility definit .foo .bar:hover &:focus { display: none; } - } - " + }" `) }) @@ -632,8 +544,7 @@ it('should merge `@utility` definitions with the same name', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility step { + "@utility step { counter-increment: step; &:before { @@ -641,8 +552,7 @@ it('should merge `@utility` definitions with the same name', async () => { @apply ml-[-41px]; content: counter(step); } - } - " + }" `) }) @@ -656,8 +566,7 @@ it('should not migrate nested classes inside a `:not(…)`', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { & .bar:not(.qux):has(.baz) { display: none; } @@ -673,8 +582,7 @@ it('should not migrate nested classes inside a `:not(…)`', async () => { .foo .bar:not(.qux):has(&) { display: none; } - } - " + }" `) }) @@ -708,8 +616,7 @@ it('should migrate advanced combinations', async () => { } `), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { @media (width >= 100px) { @supports (display: none) { & .bar:not(.qux):has(.baz) { @@ -748,8 +655,7 @@ it('should migrate advanced combinations', async () => { } } } - } - " + }" `) }) @@ -767,15 +673,12 @@ describe('comments', () => { } `), ).toMatchInlineSnapshot(` - " - /* Start of utilities: */ - + "/* Start of utilities: */ @utility foo { /* Utility #1 */ /* Declarations: */ color: red; - } - " + }" `) }) @@ -792,9 +695,7 @@ describe('comments', () => { } `), ).toMatchInlineSnapshot(` - " - /* Start of utilities: */ - + "/* Start of utilities: */ @utility foo { /* Foo & Bar */ & .bar { @@ -802,15 +703,13 @@ describe('comments', () => { color: red; } } - @utility bar { /* Foo & Bar */ .foo & { /* Declarations: */ color: red; } - } - " + }" `) }) @@ -830,9 +729,7 @@ describe('comments', () => { } `), ).toMatchInlineSnapshot(` - " - /* Start of utilities: */ - + "/* Start of utilities: */ @utility foo { /* Mobile only */ @media (width <= 640px) { @@ -840,8 +737,7 @@ describe('comments', () => { /* Declarations: */ color: red; } - } - " + }" `) }) @@ -880,9 +776,13 @@ describe('comments', () => { /* After */ `), ).toMatchInlineSnapshot(` - " - /* Tailwind Utilities: */ + "/* Above */ + .before { + /* Inside */ + } + /* After */ + /* Tailwind Utilities: */ @utility no-scrollbar { /* Chrome, Safari and Opera */ /* Second comment */ @@ -899,18 +799,11 @@ describe('comments', () => { scrollbar-width: none; /* Firefox */ } - /* Above */ - .before { - /* Inside */ - } - /* After */ - /* Above */ .after { /* Inside */ } - /* After */ - " + /* After */" `) }) }) @@ -935,8 +828,7 @@ it('should not lose attribute selectors', async () => { } `), ).toMatchInlineSnapshot(` - " - @layer components { + "@layer components { #TableOfContents { .toc a { @apply block max-w-full truncate py-1 pl-2 hover:font-medium hover:no-underline; @@ -949,8 +841,7 @@ it('should not lose attribute selectors', async () => { } } } - } - " + }" `) }) @@ -968,13 +859,11 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { /* Utility #1 */ /* Declarations: */ color: red; - } - " + }" `) }) @@ -991,13 +880,11 @@ describe('layered stylesheets', () => { layers: ['components'], }), ).toMatchInlineSnapshot(` - " - @utility foo { + "@utility foo { /* Utility #1 */ /* Declarations: */ color: red; - } - " + }" `) }) @@ -1014,13 +901,11 @@ describe('layered stylesheets', () => { layers: ['foo'], }), ).toMatchInlineSnapshot(` - " - /* Utility #1 */ + "/* Utility #1 */ .foo { /* Declarations: */ color: red; - } - " + }" `) }) @@ -1041,16 +926,15 @@ describe('layered stylesheets', () => { }), ).toMatchInlineSnapshot(` " + #main { + color: red; + } + @utility foo { /* Utility #1 */ /* Declarations: */ color: red; - } - - #main { - color: red; - } - " + }" `) }) @@ -1091,7 +975,17 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " + "@layer utilities { + + #main { + color: red; + } + } + + #secondary { + color: red; + } + @utility foo { @layer utilities { @layer utilities { @@ -1114,19 +1008,7 @@ describe('layered stylesheets', () => { /* Utility #3 */ /* Declarations: */ color: red; - } - - @layer utilities { - - #main { - color: red; - } - } - - #secondary { - color: red; - } - " + }" `) }) @@ -1143,13 +1025,11 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " - @import 'thing'; + "@import 'thing'; @utility foo { color: red; - } - " + }" `) }) @@ -1166,13 +1046,11 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " - @charset "utf-8"; + "@charset "utf-8"; @utility foo { color: red; - } - " + }" `) }) }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts similarity index 83% rename from packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts index 6bcb72d47..e274dc202 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts @@ -1,17 +1,11 @@ -import { type AtRule, type Comment, type Container, type Plugin, type Rule } from 'postcss' +import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss' import SelectorParser from 'postcss-selector-parser' -import { segment } from '../../../../tailwindcss/src/utils/segment' -import { Stylesheet } from '../../stylesheet' -import * as version from '../../utils/version' -import { walk, WalkAction, walkDepth } from '../../utils/walk' +import { segment } from '../../../tailwindcss/src/utils/segment' +import { Stylesheet } from '../stylesheet' +import { walk, WalkAction, walkDepth } from '../utils/walk' export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { function migrate(atRule: AtRule) { - // Migrating `@layer utilities` to `@utility` is only supported in Tailwind - // CSS v3 projects. Tailwind CSS v4 projects could also have `@layer - // utilities` but those aren't actual utilities. - if (!version.isMajor(3)) return - // Only migrate `@layer utilities` and `@layer components`. if (atRule.params !== 'utilities' && atRule.params !== 'components') return @@ -43,7 +37,6 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { walk(atRule, (node) => { if (node.type !== 'rule') return - if (isEmpty(node)) return // Find all the classes in the selector SelectorParser((selectors) => { @@ -185,18 +178,6 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { toRemove.push(node.parent?.nodes.at(i) as Comment) } } - - let commentsAfter: Comment[] = [] - for (let i = idx + 1; i < (node.parent?.nodes.length ?? 0); i++) { - if (node.parent?.nodes.at(i)?.type === 'rule') { - commentsAfter = [] - break - } - if (node.parent?.nodes.at(i)?.type === 'comment') { - commentsAfter.push(node.parent?.nodes.at(i) as Comment) - } - } - toRemove.push(...commentsAfter) } for (let node of toRemove) { node.remove() @@ -210,6 +191,8 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { clone.name = 'utility' clone.params = cls + // Mark the node as pretty so that it gets formatted by Prettier later. + clone.raws.tailwind_pretty = true clone.raws.before = `${clone.raws.before ?? ''}\n\n` } @@ -244,12 +227,8 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { } } - // Remove empty rules from `@utility` clones and empty wrapper at-rules - // from the default `@layer` clone. - if ( - (node.type === 'atrule' && node.nodes?.length === 0) || - (clone !== defaultsAtRule && node.type === 'rule' && node.nodes?.length === 0) - ) { + // Remove empty rules + if ((node.type === 'rule' || node.type === 'atrule') && node.nodes?.length === 0) { node.remove() } @@ -278,11 +257,6 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { // Remove empty clones entirely if (clone.nodes?.length === 0) { clones.splice(idx, 1) - } else if (clone === defaultsAtRule) { - let first = clone.nodes?.[0] - if (first) { - first.raws.before = first.raws.before?.replace(/\n\s*\n/g, '\n') - } } } @@ -338,17 +312,3 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { }, } } - -function isEmpty(node: Container): boolean { - // We have no body - if (node.nodes === undefined) return false - - // We have a body, but no nodes - if (node.nodes.length === 0) return true - - // We have nodes, but they are only comments which we consider empty - if (node.nodes.every((child) => child.type === 'comment')) return true - - // Not empty - return false -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts new file mode 100644 index 000000000..398f612e2 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts @@ -0,0 +1,140 @@ +import path from 'node:path' +import postcss, { AtRule, type Plugin, Root } from 'postcss' +import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path' +import type { JSConfigMigration } from '../migrate-js-config' +import type { Stylesheet } from '../stylesheet' +import { walk, WalkAction } from '../utils/walk' + +const ALREADY_INJECTED = new WeakMap() + +export function migrateConfig( + sheet: Stylesheet, + { + configFilePath, + jsConfigMigration, + }: { configFilePath: string; jsConfigMigration: JSConfigMigration }, +): Plugin { + function injectInto(sheet: Stylesheet) { + let alreadyInjected = ALREADY_INJECTED.get(sheet) + if (alreadyInjected && alreadyInjected.includes(configFilePath)) { + return + } else if (alreadyInjected) { + alreadyInjected.push(configFilePath) + } else { + ALREADY_INJECTED.set(sheet, [configFilePath]) + } + + let root = sheet.root + + // We don't have a sheet with a file path + if (!sheet.file) return + + let cssConfig = new AtRule() + cssConfig.raws.tailwind_pretty = true + + if (jsConfigMigration === null) { + // Skip if there is already a `@config` directive + { + let hasConfig = false + root.walkAtRules('config', () => { + hasConfig = true + return false + }) + if (hasConfig) return + } + + cssConfig.append( + new AtRule({ + name: 'config', + params: `'${relativeToStylesheet(sheet, configFilePath)}'`, + }), + ) + } else { + let css = '\n\n' + for (let source of jsConfigMigration.sources) { + let absolute = path.resolve(source.base, source.pattern) + css += `@source '${relativeToStylesheet(sheet, absolute)}';\n` + } + + if (jsConfigMigration.sources.length > 0) { + css = css + '\n' + } + + cssConfig.append(postcss.parse(css + jsConfigMigration.css)) + } + + // Inject the `@config` in a sensible place + // 1. Below the last `@import` + // 2. At the top of the file + let locationNode = null as AtRule | null + + walk(root, (node) => { + if (node.type === 'atrule' && node.name === 'import') { + locationNode = node + } + + return WalkAction.Skip + }) + + if (!locationNode) { + root.prepend(cssConfig.nodes) + } else if (locationNode.name === 'import') { + locationNode.after(cssConfig.nodes) + } + } + + function migrate(root: Root) { + // We can only migrate if there is an `@import "tailwindcss"` (or sub-import) + let hasTailwindImport = false + let hasFullTailwindImport = false + root.walkAtRules('import', (node) => { + if (node.params.match(/['"]tailwindcss['"]/)) { + hasTailwindImport = true + hasFullTailwindImport = true + return false + } else if (node.params.match(/['"]tailwindcss\/.*?['"]/)) { + hasTailwindImport = true + } + }) + + if (!hasTailwindImport) return + + // - If a full `@import "tailwindcss"` is present, we can inject the + // `@config` directive directly into this stylesheet. + // - If we are the root file (no parents), then we can inject the `@config` + // directive directly into this file as well. + if (hasFullTailwindImport || sheet.parents.size <= 0) { + injectInto(sheet) + return + } + + // Otherwise, if we are not the root file, we need to inject the `@config` + // into the root file. + if (sheet.parents.size > 0) { + for (let parent of sheet.ancestors()) { + if (parent.parents.size === 0) { + injectInto(parent) + } + } + } + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-config', + OnceExit: migrate, + } +} + +function relativeToStylesheet(sheet: Stylesheet, absolute: string) { + if (!sheet.file) throw new Error('Can not find a path for the stylesheet') + + let sheetPath = sheet.file + + let relative = path.relative(path.dirname(sheetPath), absolute) + if (relative[0] !== '.') { + relative = `./${relative}` + } + // Ensure relative is a posix style path since we will merge it with the + // glob. + return normalizePath(relative) +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts similarity index 87% rename from packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.test.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts index 18ce72e8b..b7370d4d1 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts @@ -2,10 +2,9 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' import postcss from 'postcss' import { expect, it } from 'vitest' -import type { UserConfig } from '../../../../tailwindcss/src/compat/config/types' +import type { UserConfig } from '../../../tailwindcss/src/compat/config/types' import { formatNodes } from './format-nodes' import { migrateMediaScreen } from './migrate-media-screen' -import { sortBuckets } from './sort-buckets' const css = dedent @@ -19,7 +18,6 @@ async function migrate(input: string, userConfig: UserConfig = {}) { userConfig, }), ) - .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) @@ -43,24 +41,6 @@ it('should migrate a built-in breakpoint', async () => { `) }) -it('should migrate `@screen` with a built-in breakpoint', async () => { - expect( - await migrate(css` - @screen md { - .foo { - color: red; - } - } - `), - ).toMatchInlineSnapshot(` - "@media (width >= theme(--breakpoint-md)) { - .foo { - color: red; - } - }" - `) -}) - it('should migrate a custom min-width screen (string)', async () => { expect( await migrate( diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts new file mode 100644 index 000000000..d8343984a --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts @@ -0,0 +1,44 @@ +import { type Plugin, type Root } from 'postcss' +import type { Config } from 'tailwindcss' +import { resolveConfig } from '../../../tailwindcss/src/compat/config/resolve-config' +import { buildMediaQuery } from '../../../tailwindcss/src/compat/screens-config' +import type { DesignSystem } from '../../../tailwindcss/src/design-system' +import { DefaultMap } from '../../../tailwindcss/src/utils/default-map' + +export function migrateMediaScreen({ + designSystem, + userConfig, +}: { + designSystem?: DesignSystem + userConfig?: Config +} = {}): Plugin { + function migrate(root: Root) { + if (!designSystem || !userConfig) return + + let resolvedUserConfig = resolveConfig(designSystem, [{ base: '', config: userConfig }]) + let screens = resolvedUserConfig?.theme?.screens || {} + + let mediaQueries = new DefaultMap((name) => { + let value = designSystem?.resolveThemeValue(`--breakpoint-${name}`) ?? screens?.[name] + if (typeof value === 'string') return `(width >= theme(--breakpoint-${name}))` + return value ? buildMediaQuery(value) : null + }) + + root.walkAtRules((rule) => { + if (rule.name !== 'media') return + + let screen = rule.params.match(/screen\(([^)]+)\)/) + if (!screen) return + + let value = mediaQueries.get(screen[1]) + if (!value) return + + rule.params = value + }) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-media-screen', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts similarity index 65% rename from packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.test.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts index 595275874..7dec9bcda 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts @@ -3,14 +3,12 @@ import postcss from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './format-nodes' import { migrateMissingLayers } from './migrate-missing-layers' -import { sortBuckets } from './sort-buckets' const css = dedent function migrate(input: string) { return postcss() .use(migrateMissingLayers()) - .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) @@ -24,38 +22,6 @@ it('should not migrate already migrated `@import` at-rules', async () => { ).toMatchInlineSnapshot(`"@import 'tailwindcss';"`) }) -it('should add missing `layer(…)` to imported files', async () => { - expect( - await migrate(css` - @import 'tailwindcss/utilities'; /* Expected no layer */ - @import './foo.css'; /* Expected layer(utilities) */ - @import './bar.css'; /* Expected layer(utilities) */ - @import 'tailwindcss/components'; /* Expected no layer */ - `), - ).toMatchInlineSnapshot(` - "@import 'tailwindcss/utilities'; /* Expected no layer */ - @import './foo.css' layer(utilities); /* Expected layer(utilities) */ - @import './bar.css' layer(utilities); /* Expected layer(utilities) */ - @import 'tailwindcss/components'; /* Expected no layer */" - `) -}) - -it('should add missing `layer(…)` as the first param after the import itself', async () => { - expect( - await migrate(css` - @import 'tailwindcss/utilities' supports(--foo); /* Expected no layer */ - @import './foo.css' supports(--foo); /* Expected layer(utilities) supports(--foo) */ - @import './bar.css' supports(--foo); /* Expected layer(utilities) supports(--foo) */ - @import 'tailwindcss/components' supports(--foo); /* Expected no layer */ - `), - ).toMatchInlineSnapshot(` - "@import 'tailwindcss/utilities' supports(--foo); /* Expected no layer */ - @import './foo.css' layer(utilities) supports(--foo); /* Expected layer(utilities) supports(--foo) */ - @import './bar.css' layer(utilities) supports(--foo); /* Expected layer(utilities) supports(--foo) */ - @import 'tailwindcss/components' supports(--foo); /* Expected no layer */" - `) -}) - it('should not migrate anything if no `@tailwind` directives (or imports) are found', async () => { expect( await migrate(css` @@ -140,15 +106,15 @@ it('should migrate rules above the `@tailwind base` directive in an `@layer base * License header */ - @tailwind base; - @tailwind components; - @tailwind utilities; - @layer base { html { color: red; } - }" + } + + @tailwind base; + @tailwind components; + @tailwind utilities;" `) }) @@ -177,15 +143,13 @@ it('should migrate rules between tailwind directives', async () => { ).toMatchInlineSnapshot(` "@tailwind base; - @tailwind components; - - @tailwind utilities; - @layer base { .base { } } + @tailwind components; + @layer components { .component-a { } @@ -193,6 +157,8 @@ it('should migrate rules between tailwind directives', async () => { } } + @tailwind utilities; + .utility-a { } .utility-b { diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts similarity index 82% rename from packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts index 10c9d55de..35050b932 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts @@ -1,5 +1,4 @@ import { AtRule, type ChildNode, type Plugin, type Root } from 'postcss' -import { segment } from '../../../../tailwindcss/src/utils/segment' export function migrateMissingLayers(): Plugin { function migrate(root: Root) { @@ -16,7 +15,6 @@ export function migrateMissingLayers(): Plugin { node.name === 'source' || node.name === 'theme' || node.name === 'utility' || - node.name === 'custom-variant' || node.name === 'variant' ) { if (bucket.length > 0) { @@ -77,14 +75,14 @@ export function migrateMissingLayers(): Plugin { // Add layer to `@import` at-rules if (node.name === 'import') { + if (lastLayer !== '' && !node.params.includes('layer(')) { + node.params += ` layer(${lastLayer})` + node.raws.tailwind_injected_layer = true + } + if (bucket.length > 0) { buckets.push([lastLayer, bucket.splice(0)]) } - - // Create new bucket just for the import. This way every import exists - // in its own layer which allows us to add the `layer(…)` parameter - // later on. - buckets.push([lastLayer, [node]]) return } } @@ -104,6 +102,7 @@ export function migrateMissingLayers(): Plugin { bucket.push(node) }) + // Wrap each bucket in an `@layer` at-rule for (let [layerName, nodes] of buckets) { let targetLayerName = layerName || firstLayerName || '' if (targetLayerName === '') { @@ -115,22 +114,6 @@ export function migrateMissingLayers(): Plugin { continue } - // Add `layer(…)` to `@import` at-rules - if (nodes.every((node) => node.type === 'atrule' && node.name === 'import')) { - for (let node of nodes) { - if (node.type !== 'atrule' || node.name !== 'import') continue - - if (!node.params.includes('layer(')) { - let params = segment(node.params, ' ') - params.splice(1, 0, `layer(${targetLayerName})`) - node.params = params.join(' ') - node.raws.tailwind_injected_layer = true - } - } - continue - } - - // Wrap each bucket in an `@layer` at-rule let target = nodes[0] let layerNode = new AtRule({ name: 'layer', diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts similarity index 96% rename from packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.test.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts index fdf480b3e..ba2d9ea9d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts @@ -3,14 +3,12 @@ import postcss from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './format-nodes' import { migrateTailwindDirectives } from './migrate-tailwind-directives' -import { sortBuckets } from './sort-buckets' const css = dedent function migrate(input: string, options: { newPrefix: string | null } = { newPrefix: null }) { return postcss() .use(migrateTailwindDirectives(options)) - .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) @@ -402,19 +400,3 @@ it('should drop `@tailwind variants;`', async () => { `), ).toEqual('') }) - -it('should replace `@responsive` with its children', async () => { - expect( - await migrate(css` - @responsive { - .foo { - color: red; - } - } - `), - ).toMatchInlineSnapshot(` - ".foo { - color: red; - }" - `) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts similarity index 94% rename from packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts index d3d74a65e..0f7143017 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts @@ -54,18 +54,6 @@ export function migrateTailwindDirectives(options: { newPrefix: string | null }) ) { node.remove() } - - // Replace Tailwind CSS v2 directives that still worked in v3. - else if (node.name === 'responsive') { - if (node.nodes) { - for (let child of node.nodes) { - child.raws.tailwind_pretty = true - } - node.replaceWith(node.nodes) - } else { - node.remove() - } - } }) // Insert default import if all directives are present diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/candidates.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/candidates.test.ts deleted file mode 100644 index 2e4d150aa..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/candidates.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' -import { spliceChangesIntoString } from '../../utils/splice-changes-into-string' -import { extractRawCandidates } from './candidates' - -let html = String.raw - -test('extracts candidates with positions from a template', async () => { - let content = html` -
- -
- ` - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { - base: __dirname, - }) - - let candidates = await extractRawCandidates(content, 'html') - let validCandidates = candidates.filter( - ({ rawCandidate }) => designSystem.parseCandidate(rawCandidate).length > 0, - ) - - expect(validCandidates).toMatchInlineSnapshot(` - [ - { - "end": 28, - "rawCandidate": "bg-blue-500", - "start": 17, - }, - { - "end": 51, - "rawCandidate": "hover:focus:text-white", - "start": 29, - }, - { - "end": 63, - "rawCandidate": "[color:red]", - "start": 52, - }, - { - "end": 98, - "rawCandidate": "bg-blue-500", - "start": 87, - }, - { - "end": 109, - "rawCandidate": "text-white", - "start": 99, - }, - ] - `) -}) - -test('replaces the right positions for a candidate', async () => { - let content = html` -

🤠👋

-
- ` - - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { - base: __dirname, - }) - - let candidates = await extractRawCandidates(content, 'html') - - let candidate = candidates.find( - ({ rawCandidate }) => designSystem.parseCandidate(rawCandidate).length > 0, - )! - - let migrated = spliceChangesIntoString(content, [ - { - start: candidate.start, - end: candidate.end, - replacement: 'flex', - }, - ]) - - expect(migrated).toMatchInlineSnapshot(` - " -

🤠👋

-
- " - `) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts b/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts deleted file mode 100644 index 88b1e4e03..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Scanner } from '@tailwindcss/oxide' -import { cloneCandidate, type Candidate } from '../../../../tailwindcss/src/candidate' -import type { DesignSystem } from '../../../../tailwindcss/src/design-system' - -export async function extractRawCandidates( - content: string, - extension: string = 'html', -): Promise<{ rawCandidate: string; start: number; end: number }[]> { - let scanner = new Scanner({}) - let result = scanner.getCandidatesWithPositions({ content, extension }) - - let candidates: { rawCandidate: string; start: number; end: number }[] = [] - for (let { candidate: rawCandidate, position: start } of result) { - candidates.push({ rawCandidate, start, end: start + rawCandidate.length }) - } - return candidates -} - -// Create a basic stripped candidate without variants or important flag -export function baseCandidate(candidate: T) { - let base = cloneCandidate(candidate) - - base.important = false - base.variants = [] - - return base -} - -export function parseCandidate(designSystem: DesignSystem, input: string) { - return designSystem.parseCandidate( - designSystem.theme.prefix && !input.startsWith(`${designSystem.theme.prefix}:`) - ? `${designSystem.theme.prefix}:${input}` - : input, - ) -} - -export 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 -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts deleted file mode 100644 index 7cfb16c75..000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { describe, expect, test, vi } from 'vitest' -import * as versions from '../../utils/version' -import { migrateCandidate } from './migrate' -vi.spyOn(versions, 'isMajor').mockReturnValue(true) - -const css = String.raw - -describe('is-safe-migration', async () => { - let designSystem = await __unstable__loadDesignSystem( - css` - @import 'tailwindcss'; - - /* TODO(perf): Only here to speed up the tests */ - @theme { - --*: initial; - --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - } - `, - { base: __dirname }, - ) - - test.each([ - [`let notBorder = !border \n`, '!border'], - [`{ "foo": !border.something + ""}\n`, '!border'], - [`
\n`, '!border'], - [`
\n`, '!border'], - [`
\n`, '!border'], - [`
\n`, '!border'], - [`
\n`, '!border'], - [`
\n`, '!border'], - [`
\n`, '!border'], - [`
\n`, '!border'], - [`
\n`, '!border'], - [`
\n`, '!border'], - - [`let notShadow = shadow \n`, 'shadow'], - [`{ "foo": shadow.something + ""}\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'shadow'], - [`
\n`, 'flex-grow'], - [`
\n`, 'flex-shrink'], - [`
\n`, 'flex-shrink'], - [`
\n`, 'flex-shrink'], - - // Next.js Image placeholder cases - [``, 'blur'], - [``, 'blur'], - [``, 'blur'], - - // https://github.com/tailwindlabs/tailwindcss/issues/17974 - ['
', '!duration'], - ['
', '!duration'], - ['
', '!visible'], - - // Alpine/Livewire wire:… - ['', 'blur'], - - // Vue 3 events - [`emit('blur', props.modelValue)\n`, 'blur'], - [`$emit('blur', props.modelValue)\n`, 'blur'], - - // JavaScript / TypeScript - [`document.addEventListener('blur',handleBlur)`, 'blur'], - [`document.addEventListener('blur', handleBlur)`, 'blur'], - - [`function foo({ outline = true })`, 'outline'], - [`function foo({ before = false, outline = true })`, 'outline'], - [`function foo({before=false,outline=true })`, 'outline'], - [`function foo({outline=true })`, 'outline'], - // https://github.com/tailwindlabs/tailwindcss/issues/18675 - [ - // With default value - `function foo({ size = "1.25rem", digit, outline = true, textClass = "", className = "" })`, - 'outline', - ], - [ - // Without default value - `function foo({ size = "1.25rem", digit, outline, textClass = "", className = "" })`, - 'outline', - ], - [ - // As the last argument - `function foo({ size = "1.25rem", digit, outline })`, - 'outline', - ], - [ - // As the last argument, but there is technically another `"` on the same line - `function foo({ size = "1.25rem", digit, outline }): { return "foo" }`, - 'outline', - ], - [ - // Tricky quote balancing - `function foo({ before = "'", outline, after = "'" }): { return "foo" }`, - 'outline', - ], - - [`function foo(blur, foo)`, 'blur'], - [`function foo(blur,foo)`, 'blur'], - - // shadcn/ui variants - [` +
+ ` + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + let candidates = await extractRawCandidates(content, 'html') + let validCandidates = candidates.filter( + ({ rawCandidate }) => designSystem.parseCandidate(rawCandidate).length > 0, + ) + + expect(validCandidates).toMatchInlineSnapshot(` + [ + { + "end": 28, + "rawCandidate": "bg-blue-500", + "start": 17, + }, + { + "end": 51, + "rawCandidate": "hover:focus:text-white", + "start": 29, + }, + { + "end": 63, + "rawCandidate": "[color:red]", + "start": 52, + }, + { + "end": 98, + "rawCandidate": "bg-blue-500", + "start": 87, + }, + { + "end": 109, + "rawCandidate": "text-white", + "start": 99, + }, + ] + `) +}) + +test('replaces the right positions for a candidate', async () => { + let content = html` +

🤠👋

+
+ ` + + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + let candidates = await extractRawCandidates(content, 'html') + + let candidate = candidates.find( + ({ rawCandidate }) => designSystem.parseCandidate(rawCandidate).length > 0, + )! + + expect(replaceCandidateInContent(content, 'flex', candidate.start, candidate.end)) + .toMatchInlineSnapshot(` + " +

🤠👋

+
+ " + `) +}) + +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]!'], + ['w-1/2', 'w-1/2'], +] + +const variants = [ + '', // no variant + '*:', + 'focus:', + 'group-focus:', + + 'hover:focus:', + 'hover:group-focus:', + 'group-hover:focus:', + 'group-hover:group-focus:', + + 'min-[10px]:', + // TODO: This currently expands `calc(1000px+12em)` to `calc(1000px_+_12em)` + 'min-[calc(1000px_+_12em)]:', + + 'peer-[&_p]:', + 'peer-[&_p]:hover:', + 'hover:peer-[&_p]:', + 'hover:peer-[&_p]:focus:', + 'peer-[&:hover]:peer-[&_p]:', +] + +let combinations: [string, string][] = [] +for (let variant of variants) { + for (let [input, output] of candidates) { + combinations.push([`${variant}${input}`, `${variant}${output}`]) + } +} + +describe('printCandidate()', () => { + test.each(combinations)('%s', async (candidate: string, result: string) => { + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + let candidates = designSystem.parseCandidate(candidate) + + // Sometimes we will have a functional and a static candidate for the same + // raw input string (e.g. `-inset-full`). Dedupe in this case. + let cleaned = new Set([...candidates].map((c) => printCandidate(designSystem, c))) + + expect([...cleaned]).toEqual([result]) + }) +}) diff --git a/packages/@tailwindcss-upgrade/src/template/candidates.ts b/packages/@tailwindcss-upgrade/src/template/candidates.ts new file mode 100644 index 000000000..be08477ee --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/candidates.ts @@ -0,0 +1,150 @@ +import { Scanner } from '@tailwindcss/oxide' +import stringByteSlice from 'string-byte-slice' +import type { Candidate, Variant } from '../../../tailwindcss/src/candidate' +import type { DesignSystem } from '../../../tailwindcss/src/design-system' + +export async function extractRawCandidates( + content: string, + extension: string = 'html', +): Promise<{ rawCandidate: string; start: number; end: number }[]> { + let scanner = new Scanner({}) + let result = scanner.getCandidatesWithPositions({ content, extension }) + + let candidates: { rawCandidate: string; start: number; end: number }[] = [] + for (let { candidate: rawCandidate, position: start } of result) { + candidates.push({ rawCandidate, start, end: start + rawCandidate.length }) + } + return candidates +} + +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 negative + if (candidate.kind === 'static' || candidate.kind === 'functional') { + if (candidate.negative) { + base += '-' + } + } + + // 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) { + base += '' + } else if (candidate.value.dataType) { + base += `-[${candidate.value.dataType}:${escapeArbitrary(candidate.value.value)}]` + } else { + base += `-[${escapeArbitrary(candidate.value.value)}]` + } + } else if (candidate.value.kind === 'named') { + base += `-${candidate.value.value}` + } + } + } + + // Handle arbitrary + if (candidate.kind === 'arbitrary') { + base += `[${candidate.property}:${escapeArbitrary(candidate.value)}]` + } + + // Handle modifier + if (candidate.kind === 'arbitrary' || candidate.kind === 'functional') { + if (candidate.modifier) { + if (candidate.modifier.kind === 'arbitrary') { + base += `/[${escapeArbitrary(candidate.modifier.value)}]` + } else if (candidate.modifier.kind === 'named') { + base += `/${candidate.modifier.value}` + } + } + } + + // Handle important + if (candidate.important) { + base += '!' + } + + parts.push(base) + + return parts.join(':') +} + +function printVariant(variant: Variant) { + // Handle static variants + if (variant.kind === 'static') { + return variant.root + } + + // Handle arbitrary variants + if (variant.kind === 'arbitrary') { + return `[${escapeArbitrary(variant.selector)}]` + } + + let base: string = '' + + // Handle functional variants + if (variant.kind === 'functional') { + base += variant.root + if (variant.value) { + if (variant.value.kind === 'arbitrary') { + base += `-[${escapeArbitrary(variant.value.value)}]` + } else if (variant.value.kind === 'named') { + base += `-${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') { + if (variant.modifier) { + if (variant.modifier.kind === 'arbitrary') { + base += `/[${escapeArbitrary(variant.modifier.value)}]` + } else if (variant.modifier.kind === 'named') { + base += `/${variant.modifier.value}` + } + } + } + + return base +} + +function escapeArbitrary(input: string) { + return input + .replaceAll('_', String.raw`\_`) // Escape underscores to keep them as-is + .replaceAll(' ', '_') // Replace spaces with underscores +} + +export function replaceCandidateInContent( + content: string, + replacement: string, + startByte: number, + endByte: number, +) { + return stringByteSlice(content, 0, startByte) + replacement + stringByteSlice(content, endByte) +} diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts new file mode 100644 index 000000000..2f29c130f --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts @@ -0,0 +1,31 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { expect, test } from 'vitest' +import { arbitraryValueToBareValue } from './arbitrary-value-to-bare-value' + +test.each([ + ['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!'], +])('%s => %s', async (candidate, result) => { + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + expect(arbitraryValueToBareValue(designSystem, {}, candidate)).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts new file mode 100644 index 000000000..065f10807 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts @@ -0,0 +1,94 @@ +import type { Config } from 'tailwindcss' +import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { segment } from '../../../../tailwindcss/src/utils/segment' +import { printCandidate } from '../candidates' + +export function arbitraryValueToBareValue( + designSystem: DesignSystem, + _userConfig: Config, + rawCandidate: string, +): string { + for (let candidate of parseCandidate(rawCandidate, designSystem)) { + let clone = structuredClone(candidate) + let 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-automatic-var-injection.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts similarity index 62% rename from packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.test.ts rename to packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts index 5da651d51..b3ddb06d3 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts @@ -1,6 +1,6 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import { expect, test } from 'vitest' -import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection' +import { automaticVarInjection } from './automatic-var-injection' test.each([ // Arbitrary candidates @@ -9,10 +9,10 @@ test.each([ ['[--my-color:--my-other-color]', '[--my-color:var(--my-other-color)]'], // Arbitrary values for functional candidates - ['bg-[--my-color]', 'bg-(--my-color)'], - ['bg-[color:--my-color]', 'bg-(color:--my-color)'], - ['border-[length:--my-length]', 'border-(length:--my-length)'], - ['border-[line-width:--my-width]', 'border-(line-width:--my-width)'], + ['bg-[--my-color]', 'bg-[var(--my-color)]'], + ['bg-[color:--my-color]', 'bg-[color:var(--my-color)]'], + ['border-[length:--my-length]', 'border-[length:var(--my-length)]'], + ['border-[line-width:--my-width]', 'border-[line-width:var(--my-width)]'], // Can clean up the workaround for opting out of automatic var injection ['bg-[_--my-color]', 'bg-[--my-color]'], @@ -21,30 +21,21 @@ test.each([ ['border-[line-width:_--my-width]', 'border-[line-width:--my-width]'], // Modifiers - ['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/(--my-opacity)'], - ['bg-red-500/[--my-opacity]', 'bg-red-500/(--my-opacity)'], - ['bg-[--my-color]/[--my-opacity]', 'bg-(--my-color)/(--my-opacity)'], - ['bg-[color:--my-color]/[--my-opacity]', 'bg-(color:--my-color)/(--my-opacity)'], + ['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/[var(--my-opacity)]'], + ['bg-red-500/[--my-opacity]', 'bg-red-500/[var(--my-opacity)]'], + ['bg-[--my-color]/[--my-opacity]', 'bg-[var(--my-color)]/[var(--my-opacity)]'], + ['bg-[color:--my-color]/[--my-opacity]', 'bg-[color:var(--my-color)]/[var(--my-opacity)]'], // Can clean up the workaround for opting out of automatic var injection ['[color:--my-color]/[_--my-opacity]', '[color:var(--my-color)]/[--my-opacity]'], ['bg-red-500/[_--my-opacity]', 'bg-red-500/[--my-opacity]'], - ['bg-[--my-color]/[_--my-opacity]', 'bg-(--my-color)/[--my-opacity]'], - ['bg-[color:--my-color]/[_--my-opacity]', 'bg-(color:--my-color)/[--my-opacity]'], + ['bg-[--my-color]/[_--my-opacity]', 'bg-[var(--my-color)]/[--my-opacity]'], + ['bg-[color:--my-color]/[_--my-opacity]', 'bg-[color:var(--my-color)]/[--my-opacity]'], // Variants - ['supports-[--test]:flex', 'supports-(--test):flex'], + ['supports-[--test]:flex', 'supports-[var(--test)]:flex'], ['supports-[_--test]:flex', 'supports-[--test]:flex'], - // Custom CSS functions that look like variables should not be converted - ['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]'], @@ -63,6 +54,6 @@ test.each([ base: __dirname, }) - let migrated = migrateAutomaticVarInjection(designSystem, {}, candidate) + let migrated = automaticVarInjection(designSystem, {}, candidate) expect(migrated).toEqual(result) }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts similarity index 78% rename from packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts rename to packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts index 0ea3bd691..858a80fd9 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts @@ -1,19 +1,18 @@ -import { cloneCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate' -import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import type { Config } from 'tailwindcss' +import { walk, WalkAction } from '../../../../tailwindcss/src/ast' +import { type Candidate, type Variant } from '../../../../tailwindcss/src/candidate' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import type { Writable } from '../../../../tailwindcss/src/types' -import * as ValueParser from '../../../../tailwindcss/src/value-parser' -import { walk, WalkAction } from '../../../../tailwindcss/src/walk' +import { printCandidate } from '../candidates' -export function migrateAutomaticVarInjection( +export function automaticVarInjection( designSystem: DesignSystem, - _userConfig: Config | null, + _userConfig: Config, 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 = cloneCandidate(readonlyCandidate) as Writable + let candidate = structuredClone(readonlyCandidate) as Candidate let didChange = false @@ -67,7 +66,7 @@ export function migrateAutomaticVarInjection( } if (didChange) { - return designSystem.printCandidate(candidate) + return printCandidate(designSystem, candidate) } } return rawCandidate @@ -76,27 +75,12 @@ export function migrateAutomaticVarInjection( function injectVar(value: string): { value: string; didChange: boolean } { let didChange = false 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 - } + value = `var(${value})` + didChange = true } else if (value.startsWith(' --')) { value = value.slice(1) didChange = true } - return { value, didChange } } diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.test.ts new file mode 100644 index 000000000..3c42903c3 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.test.ts @@ -0,0 +1,22 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { expect, test } from 'vitest' +import { bgGradient } from './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(bgGradient(designSystem, {}, candidate)).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.ts b/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.ts new file mode 100644 index 000000000..d85350ccd --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.ts @@ -0,0 +1,27 @@ +import type { Config } from 'tailwindcss' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { printCandidate } from '../candidates' + +const DIRECTIONS = ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] + +export function bgGradient( + 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/template/codemods/important.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts new file mode 100644 index 000000000..dfbce064c --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts @@ -0,0 +1,19 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { expect, test } from 'vitest' +import { important } from './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(important(designSystem, {}, candidate)).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/important.ts b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts new file mode 100644 index 000000000..69dbdba85 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts @@ -0,0 +1,33 @@ +import type { Config } from 'tailwindcss' +import { parseCandidate } from '../../../../tailwindcss/src/candidate' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { printCandidate } from '../candidates' + +// 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 important( + designSystem: DesignSystem, + _userConfig: Config, + rawCandidate: string, +): string { + for (let candidate of parseCandidate(rawCandidate, designSystem)) { + if (candidate.important && candidate.raw[candidate.raw.length - 1] !== '!') { + // 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-prefix.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/prefix.test.ts similarity index 66% rename from packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts rename to packages/@tailwindcss-upgrade/src/template/codemods/prefix.test.ts index f47e181af..b50dbf64b 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/prefix.test.ts @@ -1,8 +1,6 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { describe, expect, test, vi } from 'vitest' -import * as versions from '../../utils/version' -import { migratePrefix } from './migrate-prefix' -vi.spyOn(versions, 'isMajor').mockReturnValue(true) +import { describe, expect, test } from 'vitest' +import { prefix } from './prefix' describe('for projects with configured prefix', () => { test.each([ @@ -22,18 +20,12 @@ describe('for projects with configured prefix', () => { // Adds prefix to arbitrary candidates ['[color:red]', 'tw:[color:red]'], - - // `.group` and `.peer` classes - ['tw-group', 'tw:group'], - ['tw-group/foo', 'tw:group/foo'], - ['tw-peer', 'tw:peer'], - ['tw-peer/foo', 'tw:peer/foo'], ])('%s => %s', async (candidate, result) => { let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss" prefix(tw);', { base: __dirname, }) - expect(migratePrefix(designSystem, { prefix: 'tw-' }, candidate)).toEqual(result) + expect(prefix(designSystem, { prefix: 'tw-' }, candidate)).toEqual(result) }) }) @@ -42,7 +34,7 @@ test('can handle complex prefix separators', async () => { base: __dirname, }) - expect(migratePrefix(designSystem, { prefix: 'tw__' }, 'tw__flex')).toEqual('tw:flex') + expect(prefix(designSystem, { prefix: 'tw__' }, 'tw__flex')).toEqual('tw:flex') }) describe('for projects without configured prefix', () => { @@ -51,6 +43,6 @@ describe('for projects without configured prefix', () => { base: __dirname, }) - expect(migratePrefix(designSystem, {}, 'tw-flex')).toEqual('tw-flex') + expect(prefix(designSystem, {}, 'tw-flex')).toEqual('tw-flex') }) }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts b/packages/@tailwindcss-upgrade/src/template/codemods/prefix.ts similarity index 73% rename from packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts rename to packages/@tailwindcss-upgrade/src/template/codemods/prefix.ts index 776032850..8f5979326 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/prefix.ts @@ -1,36 +1,15 @@ -import { decl } from '../../../../tailwindcss/src/ast' +import type { Config } from 'tailwindcss' 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 * as version from '../../utils/version' +import { printCandidate } from '../candidates' -let seenDesignSystems = new WeakSet() - -export function migratePrefix( +export function prefix( designSystem: DesignSystem, - userConfig: Config | null, + userConfig: Config, 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', (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) - } let v3Base = extractV3Base(designSystem, userConfig, rawCandidate) @@ -58,7 +37,7 @@ export function migratePrefix( if (!candidate) return rawCandidate - return designSystem.printCandidate(candidate) + return printCandidate(designSystem, candidate) } // Parses a raw candidate with v3 compatible prefix syntax. This won't match if @@ -80,7 +59,7 @@ function extractV3Base( // ^^^^^^^^^ -> Base let rawVariants = segment(rawCandidate, ':') - // SAFETY: At this point it is safe to use TypeScript's non-null assertion + // Safety: At this point it is safe to use TypeScript's non-null assertion // operator because even if the `input` was an empty string, splitting an // empty string by `:` will always result in an array with at least one // element. @@ -128,7 +107,7 @@ function extractV3Base( } const VALID_PREFIX = /([a-z]+)/ -export function migratePrefixValue(prefix: string): string { +export function migratePrefix(prefix: string): string { let result = VALID_PREFIX.exec(prefix.toLocaleLowerCase()) if (!result) { console.warn( diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/simple-legacy-classes.test.ts similarity index 57% rename from packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts rename to packages/@tailwindcss-upgrade/src/template/codemods/simple-legacy-classes.test.ts index 456671dd7..d0a89da58 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/simple-legacy-classes.test.ts @@ -1,14 +1,11 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -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) +import { expect, test } from 'vitest' +import { simpleLegacyClasses } from './simple-legacy-classes' test.each([ + ['overflow-clip', 'text-clip'], ['overflow-ellipsis', 'text-ellipsis'], - ['flex-grow', 'grow'], ['flex-grow-0', 'grow-0'], - ['flex-shrink', 'shrink'], ['flex-shrink-0', 'shrink-0'], ['decoration-clone', 'box-decoration-clone'], ['decoration-slice', 'box-decoration-slice'], @@ -16,15 +13,10 @@ test.each([ ['max-lg:hover:decoration-slice', 'max-lg:hover:box-decoration-slice'], ['max-lg:hover:decoration-slice!', 'max-lg:hover:box-decoration-slice!'], ['max-lg:hover:!decoration-slice', 'max-lg:hover:box-decoration-slice!'], - - ['focus:outline-none', 'focus:outline-hidden'], - - // Should not convert v2 utilities - ['overflow-clip', 'overflow-clip'], ])('%s => %s', async (candidate, result) => { let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { base: __dirname, }) - expect(migrateSimpleLegacyClasses(designSystem, {}, candidate)).toEqual(result) + expect(simpleLegacyClasses(designSystem, {}, candidate)).toEqual(result) }) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/simple-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/template/codemods/simple-legacy-classes.ts new file mode 100644 index 000000000..94a4121f8 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/simple-legacy-classes.ts @@ -0,0 +1,41 @@ +import type { Config } from 'tailwindcss' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { printCandidate } from '../candidates' + +// Classes that used to exist in Tailwind CSS v3, but do not exist in Tailwind +// CSS v4 anymore. +const LEGACY_CLASS_MAP = { + 'overflow-clip': 'text-clip', + 'overflow-ellipsis': 'text-ellipsis', + 'flex-grow-0': 'grow-0', + 'flex-shrink-0': 'shrink-0', + 'decoration-clone': 'box-decoration-clone', + 'decoration-slice': 'box-decoration-slice', +} + +const SEEDED = new WeakSet() + +export function simpleLegacyClasses( + designSystem: DesignSystem, + _userConfig: Config, + rawCandidate: string, +): string { + // Prepare design system with the legacy classes + if (!SEEDED.has(designSystem)) { + for (let old in LEGACY_CLASS_MAP) { + designSystem.utilities.static(old, () => []) + } + SEEDED.add(designSystem) + } + + for (let candidate of designSystem.parseCandidate(rawCandidate)) { + if (candidate.kind === 'static' && Object.hasOwn(LEGACY_CLASS_MAP, candidate.root)) { + return printCandidate(designSystem, { + ...candidate, + root: LEGACY_CLASS_MAP[candidate.root as keyof typeof LEGACY_CLASS_MAP], + }) + } + } + + return rawCandidate +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/variant-order.test.ts similarity index 84% rename from packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts rename to packages/@tailwindcss-upgrade/src/template/codemods/variant-order.test.ts index 1b14d533f..e42595645 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/variant-order.test.ts @@ -1,9 +1,7 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' -import { expect, test, vi } from 'vitest' -import * as versions from '../../utils/version' -import { migrateVariantOrder } from './migrate-variant-order' -vi.spyOn(versions, 'isMajor').mockReturnValue(true) +import { expect, test } from 'vitest' +import { variantOrder } from './variant-order' let css = dedent @@ -55,26 +53,26 @@ test.each([ base: __dirname, }) - expect(migrateVariantOrder(designSystem, {}, candidate)).toEqual(result) + expect(variantOrder(designSystem, {}, candidate)).toEqual(result) }) test('it works with custom variants', async () => { let designSystem = await __unstable__loadDesignSystem( css` @import 'tailwindcss'; - @custom-variant atrule { + @variant atrule { @media (print) { @slot; } } - @custom-variant combinator { + @variant combinator { > * { @slot; } } - @custom-variant pseudo { + @variant pseudo { &::before { @slot; } @@ -85,7 +83,7 @@ test('it works with custom variants', async () => { }, ) - expect(migrateVariantOrder(designSystem, {}, 'combinator:pseudo:atrule:underline')).toEqual( + expect(variantOrder(designSystem, {}, 'combinator:pseudo:atrule:underline')).toEqual( 'atrule:combinator:pseudo:underline', ) }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts b/packages/@tailwindcss-upgrade/src/template/codemods/variant-order.ts similarity index 81% rename from packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts rename to packages/@tailwindcss-upgrade/src/template/codemods/variant-order.ts index 3fd84af7c..5234208a5 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/variant-order.ts @@ -1,24 +1,14 @@ -import { type AstNode } from '../../../../tailwindcss/src/ast' +import type { Config } from 'tailwindcss' +import { walk, 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 { walk } from '../../../../tailwindcss/src/walk' -import * as version from '../../utils/version' +import { printCandidate } from '../candidates' -export function migrateVariantOrder( +export function variantOrder( designSystem: DesignSystem, - _userConfig: Config | null, + _userConfig: Config, 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 @@ -56,7 +46,7 @@ export function migrateVariantOrder( continue } - return designSystem.printCandidate({ ...candidate, variants: newOrder }) + return printCandidate(designSystem, { ...candidate, variants: newOrder }) } return rawCandidate } @@ -67,7 +57,7 @@ function isAtRuleVariant(designSystem: DesignSystem, variant: Variant) { return true } let stack = getAppliedNodeStack(designSystem, variant) - return stack.every((node) => node.kind === 'at-rule') + return stack.every((node) => node.kind === 'rule' && node.selector[0] === '@') } function isCombinatorVariant(designSystem: DesignSystem, variant: Variant) { @@ -75,6 +65,8 @@ function isCombinatorVariant(designSystem: DesignSystem, variant: Variant) { return stack.some( (node) => node.kind === 'rule' && + // Ignore at-rules as they are hoisted + node.selector[0] !== '@' && // Combinators include any of the following characters (node.selector.includes(' ') || node.selector.includes('>') || diff --git a/packages/@tailwindcss-upgrade/src/template/migrate.ts b/packages/@tailwindcss-upgrade/src/template/migrate.ts new file mode 100644 index 000000000..50f2ca60e --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/migrate.ts @@ -0,0 +1,72 @@ +import fs from 'node:fs/promises' +import path, { extname } from 'node:path' +import type { Config } from 'tailwindcss' +import type { DesignSystem } from '../../../tailwindcss/src/design-system' +import { extractRawCandidates, replaceCandidateInContent } from './candidates' +import { arbitraryValueToBareValue } from './codemods/arbitrary-value-to-bare-value' +import { automaticVarInjection } from './codemods/automatic-var-injection' +import { bgGradient } from './codemods/bg-gradient' +import { important } from './codemods/important' +import { prefix } from './codemods/prefix' +import { simpleLegacyClasses } from './codemods/simple-legacy-classes' +import { variantOrder } from './codemods/variant-order' + +export type Migration = ( + designSystem: DesignSystem, + userConfig: Config, + rawCandidate: string, +) => string + +export const DEFAULT_MIGRATIONS: Migration[] = [ + prefix, + important, + automaticVarInjection, + bgGradient, + simpleLegacyClasses, + arbitraryValueToBareValue, + variantOrder, +] + +export function migrateCandidate( + designSystem: DesignSystem, + userConfig: Config, + rawCandidate: string, +): string { + for (let migration of DEFAULT_MIGRATIONS) { + rawCandidate = migration(designSystem, userConfig, rawCandidate) + } + return rawCandidate +} + +export default async function migrateContents( + designSystem: DesignSystem, + userConfig: Config, + contents: string, + extension: string, +): Promise { + let candidates = await extractRawCandidates(contents, extension) + + // Sort candidates by starting position desc + candidates.sort((a, z) => z.start - a.start) + + let output = contents + for (let { rawCandidate, start, end } of candidates) { + let migratedCandidate = migrateCandidate(designSystem, userConfig, rawCandidate) + + if (migratedCandidate !== rawCandidate) { + output = replaceCandidateInContent(output, migratedCandidate, start, end) + } + } + + return output +} + +export async function migrate(designSystem: DesignSystem, userConfig: Config, file: string) { + let fullPath = path.resolve(process.cwd(), file) + let contents = await fs.readFile(fullPath, 'utf-8') + + await fs.writeFile( + fullPath, + await migrateContents(designSystem, userConfig, contents, extname(file)), + ) +} diff --git a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts new file mode 100644 index 000000000..9cdb95d49 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts @@ -0,0 +1,108 @@ +import { __unstable__loadDesignSystem, compile } from '@tailwindcss/node' +import fs from 'node:fs/promises' +import path from 'node:path' +import { dirname } from 'path' +import type { Config } from 'tailwindcss' +import { fileURLToPath } from 'url' +import { loadModule } from '../../../@tailwindcss-node/src/compile' +import { resolveConfig } from '../../../tailwindcss/src/compat/config/resolve-config' +import type { DesignSystem } from '../../../tailwindcss/src/design-system' +import { error } from '../utils/renderer' +import { migratePrefix } from './codemods/prefix' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const css = String.raw + +export async function prepareConfig( + configPath: string | null, + options: { base: string }, +): Promise<{ + designSystem: DesignSystem + globs: { base: string; pattern: string }[] + userConfig: Config + configFilePath: string + + newPrefix: string | null +}> { + try { + if (configPath === null) { + configPath = await detectConfigPath(options.base) + } + + // We create a relative path from the current file to the config file. This is + // required so that the base for Tailwind CSS can bet inside the + // @tailwindcss-upgrade package and we can require `tailwindcss` properly. + let fullConfigPath = path.resolve(options.base, configPath) + let relative = path.relative(__dirname, fullConfigPath) + + // If the path points to a file in the same directory, `path.relative` will + // remove the leading `./` and we need to add it back in order to still + // consider the path relative + if (!relative.startsWith('.')) { + relative = './' + relative + } + + let userConfig = await createResolvedUserConfig(fullConfigPath) + + let newPrefix = userConfig.prefix ? migratePrefix(userConfig.prefix) : null + let input = css` + @import 'tailwindcss' ${newPrefix ? `prefix(${newPrefix})` : ''}; + @config './${relative}'; + ` + + let [compiler, designSystem] = await Promise.all([ + compile(input, { base: __dirname, onDependency: () => {} }), + __unstable__loadDesignSystem(input, { base: __dirname }), + ]) + + return { + designSystem, + globs: compiler.globs, + userConfig, + newPrefix, + configFilePath: fullConfigPath, + } + } catch (e: any) { + error('Could not load the configuration file: ' + e.message) + process.exit(1) + } +} + +async function createResolvedUserConfig(fullConfigPath: string): Promise { + let [noopDesignSystem, unresolvedUserConfig] = await Promise.all([ + __unstable__loadDesignSystem( + css` + @import 'tailwindcss'; + `, + { base: __dirname }, + ), + loadModule(fullConfigPath, __dirname, () => {}).then((result) => result.module) as Config, + ]) + + return resolveConfig(noopDesignSystem, [ + { base: dirname(fullConfigPath), config: unresolvedUserConfig }, + ]) as any +} + +const DEFAULT_CONFIG_FILES = [ + './tailwind.config.js', + './tailwind.config.cjs', + './tailwind.config.mjs', + './tailwind.config.ts', + './tailwind.config.cts', + './tailwind.config.mts', +] +async function detectConfigPath(base: string) { + for (let file of DEFAULT_CONFIG_FILES) { + let fullPath = path.resolve(base, file) + try { + await fs.access(fullPath) + return file + } catch {} + } + throw new Error( + 'No configuration file found. Please provide a path to the Tailwind CSS v3 config file via the `--config` option.', + ) +} diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts deleted file mode 100644 index 4ec33b410..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts +++ /dev/null @@ -1,354 +0,0 @@ -import dedent from 'dedent' -import { describe, expect, test } from 'vitest' -import { extractStaticImportMap, findStaticPlugins } from './extract-static-plugins' - -const js = dedent - -describe('findStaticPlugins', () => { - test('parses all export styles', () => { - expect( - findStaticPlugins(js` - import plugin1 from './plugin1' - - export default { - plugins: [plugin1, 'plugin2', require('./plugin3')] - } - `), - ).toEqual([ - ['./plugin1', null], - ['plugin2', null], - ['./plugin3', null], - ]) - - expect( - findStaticPlugins(js` - import plugin1 from './plugin1' - - export default { - plugins: [plugin1, 'plugin2', require('./plugin3')] - } as any - `), - ).toEqual([ - ['./plugin1', null], - ['plugin2', null], - ['./plugin3', null], - ]) - - expect( - findStaticPlugins(js` - import plugin1 from './plugin1' - - export default { - plugins: [plugin1, 'plugin2', require('./plugin3')] - } satisfies any - `), - ).toEqual([ - ['./plugin1', null], - ['plugin2', null], - ['./plugin3', null], - ]) - - expect( - findStaticPlugins(js` - const plugin1 = require('./plugin1') - - module.exports = { - plugins: [plugin1, 'plugin2', require('./plugin3')] - } as any - `), - ).toEqual([ - ['./plugin1', null], - ['plugin2', null], - ['./plugin3', null], - ]) - - expect( - findStaticPlugins(js` - const plugin1 = require('./plugin1') - - module.exports = { - plugins: [plugin1, 'plugin2', require('./plugin3')] - } satisfies any - `), - ).toEqual([ - ['./plugin1', null], - ['plugin2', null], - ['./plugin3', null], - ]) - - expect( - findStaticPlugins(js` - const plugin1 = require('./plugin1') - - module.exports = { - plugins: [plugin1, 'plugin2', require('./plugin3')] - } - `), - ).toEqual([ - ['./plugin1', null], - ['plugin2', null], - ['./plugin3', null], - ]) - }) - - test('can extract plugin options', () => { - expect( - findStaticPlugins(js` - import plugin1 from './plugin1' - import plugin2 from './plugin2' - - export default { - plugins: [ - plugin1({ - foo: 'bar', - }), - plugin2(), - require('./plugin3')({ - foo: 'bar', - }), - ] - } - `), - ).toEqual([ - ['./plugin1', { foo: 'bar' }], - ['./plugin2', null], - ['./plugin3', { foo: 'bar' }], - ]) - }) - - test('can extract all supported data types', () => { - expect( - findStaticPlugins(js` - import plugin from 'plugin' - - export default { - plugins: [ - plugin({ - 'is-arr-mixed': [null, true, false, 1234567, 1.35, 'foo', 'bar', 'true'], - 'is-arr': ['foo', 'bar'], - 'is-null': null, - 'is-true': true, - 'is-false': false, - 'is-int': 1234567, - 'is-float': 1.35, - 'is-sci': 1.35e-5, - 'is-str-null': 'null', - 'is-str-true': 'true', - 'is-str-false': 'false', - 'is-str-int': '1234567', - 'is-str-float': '1.35', - 'is-str-sci': '1.35e-5', - }), - ] - } - `), - ).toEqual([ - [ - 'plugin', - { - 'is-arr-mixed': [null, true, false, 1234567, 1.35, 'foo', 'bar', 'true'], - 'is-arr': ['foo', 'bar'], - 'is-null': null, - 'is-true': true, - 'is-false': false, - 'is-int': 1234567, - 'is-float': 1.35, - 'is-sci': 1.35e-5, - 'is-str-null': 'null', - 'is-str-true': 'true', - 'is-str-false': 'false', - 'is-str-int': '1234567', - 'is-str-float': '1.35', - 'is-str-sci': '1.35e-5', - }, - ], - ]) - }) - - test('bails out on import * as import', () => { - expect( - findStaticPlugins(js` - import * as plugin from './plugin' - - export default { - plugins: [plugin] - } - `), - ).toEqual(null) - }) - - test('bails out on inline plugins', () => { - expect( - findStaticPlugins(js` - import plugin1 from './plugin1' - - export default { - plugins: [plugin1, () => {} ] - } - `), - ).toEqual(null) - - expect( - findStaticPlugins(js` - let plugin1 = () => {} - - export default { - plugins: [plugin1] - } - `), - ).toEqual(null) - }) - - test('bails out on non `require` calls', () => { - expect( - findStaticPlugins(js` - export default { - plugins: [load('./plugin1')] - } - `), - ).toEqual(null) - }) - - test('bails out on named imports for plugins', () => { - expect( - findStaticPlugins(js` - import {plugin1} from './plugin1' - - export default { - plugins: [plugin1] - } - `), - ).toEqual(null) - }) - - test('bails on invalid plugin options', () => { - expect( - findStaticPlugins(js` - import plugin from './plugin' - - export default { - plugins: [ - plugin({ foo }), - ] - } - `), - ).toEqual(null) - - expect( - findStaticPlugins(js` - import plugin from './plugin' - - export default { - plugins: [ - plugin({ foo: { bar: 2 } }), - ] - } - `), - ).toEqual(null) - - expect( - findStaticPlugins(js` - import plugin from './plugin' - - export default { - plugins: [ - plugin({ foo: ${'`bar${""}`'} }), - ] - } - `), - ).toEqual(null) - - expect( - findStaticPlugins(js` - import plugin from './plugin' - - const OPTIONS = { foo: 1 } - - export default { - plugins: [ - plugin(OPTIONS), - ] - } - `), - ).toEqual(null) - - expect( - findStaticPlugins(js` - import plugin from './plugin' - - let something = 1 - - export default { - plugins: [ - plugin({ foo: something }), - ] - } - `), - ).toEqual(null) - }) - - test('returns no plugins if none are exported', () => { - expect( - findStaticPlugins(js` - export default { - plugins: [] - } - `), - ).toEqual([]) - - expect( - findStaticPlugins(js` - export default {} - `), - ).toEqual([]) - }) -}) - -describe('extractStaticImportMap', () => { - test('extracts different kind of imports from an ESM file', () => { - let extracted = extractStaticImportMap(js` - import plugin1 from './plugin1' - import * as plugin2 from './plugin2' - import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3' - import plugin8, * as plugin7 from './plugin7' - `) - - expect(extracted).toEqual({ - plugin1: { module: './plugin1', export: null }, - plugin2: { module: './plugin2', export: '*' }, - plugin3: { module: './plugin3', export: 'plugin3' }, - plugin4: { module: './plugin3', export: 'plugin4' }, - plugin5: { module: './plugin3', export: 'default' }, - plugin6: { module: './plugin3', export: null }, - plugin7: { module: './plugin7', export: '*' }, - plugin8: { module: './plugin7', export: null }, - }) - }) - - test('extracts different kind of imports from an CJS file', () => { - let extracted = extractStaticImportMap(js` - const plugin1 = require('./plugin1') - let plugin2 = require('./plugin2') - var plugin3 = require('./plugin3') - - const {plugin4, foo: plugin5, ...plugin6} = require('./plugin4') - let {plugin7, foo: plugin8, ...plugin9} = require('./plugin5') - var {plugin10, foo: plugin11, ...plugin12} = require('./plugin6') - `) - - expect(extracted).toEqual({ - plugin1: { module: './plugin1', export: null }, - plugin2: { module: './plugin2', export: null }, - plugin3: { module: './plugin3', export: null }, - plugin4: { module: './plugin4', export: 'plugin4' }, - plugin5: { module: './plugin4', export: 'foo' }, - plugin6: { module: './plugin4', export: '*' }, - plugin7: { module: './plugin5', export: 'plugin7' }, - plugin8: { module: './plugin5', export: 'foo' }, - plugin9: { module: './plugin5', export: '*' }, - plugin10: { module: './plugin6', export: 'plugin10' }, - plugin11: { module: './plugin6', export: 'foo' }, - plugin12: { module: './plugin6', export: '*' }, - }) - }) -}) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts deleted file mode 100644 index 4318c14ef..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts +++ /dev/null @@ -1,382 +0,0 @@ -import Parser from 'tree-sitter' -import TS from 'tree-sitter-typescript' - -let parser = new Parser() -parser.setLanguage(TS.typescript) -const treesitter = String.raw - -// Extract `plugins` property of the object export for both ESM and CJS files -const PLUGINS_QUERY = new Parser.Query( - TS.typescript, - treesitter` - ; export default {} - (export_statement - value: [ - (satisfies_expression (object - (pair - key: (property_identifier) @_name (#eq? @_name "plugins") - value: (array) @imports - ) - )) - value: (as_expression (object - (pair - key: (property_identifier) @_name (#eq? @_name "plugins") - value: (array) @imports - ) - )) - value: (object - (pair - key: (property_identifier) @_name (#eq? @_name "plugins") - value: (array) @imports - ) - ) - ] - ) - - ; module.exports = {} - (expression_statement - (assignment_expression - left: (member_expression) @left (#eq? @left "module.exports") - right: [ - (satisfies_expression (object - (pair - key: (property_identifier) @_name (#eq? @_name "plugins") - value: (array) @imports - ) - )) - (as_expression (object - (pair - key: (property_identifier) @_name (#eq? @_name "plugins") - value: (array) @imports - ) - )) - (object - (pair - key: (property_identifier) @_name (#eq? @_name "plugins") - value: (array) @imports - ) - ) - ] - ) - ) - `, -) - -// Extract require() calls, as well as identifiers with options or require() -// with options -const PLUGIN_CALL_OPTIONS_QUERY = new Parser.Query( - TS.typescript, - treesitter` - (call_expression - function: [ - (call_expression - function: (identifier) @_name (#eq? @_name "require") - arguments: (arguments - (string (string_fragment) @module_string) - ) - ) - (identifier) @module_identifier - ] - arguments: [ - (arguments - (object - (pair - key: [ - (property_identifier) @property - (string (string_fragment) @property) - ] - - value: [ - (string (string_fragment) @str_value) - (template_string - . (string_fragment) @str_value - ; If the template string has more than exactly one string - ; fragment at the top, the migration should bail. - _ @error - ) - (number) @num_value - (true) @true_value - (false) @false_value - (null) @null_value - (array [ - (string (string_fragment) @str_value) - (template_string (string_fragment) @str_value) - (number) @num_value - (true) @true_value - (false) @false_value - (null) @null_value - ]) @array_value - ] - ) - ) - ) - (arguments) @_empty_args (#eq? @_empty_args "()") - ] - ) - (call_expression - function: (identifier) @_name (#eq? @_name "require") - arguments: (arguments - (string (string_fragment) @module_string) - ) - ) - `, -) - -export type StaticPluginOptions = Record< - string, - | string - | number - | boolean - | null - | string - | number - | boolean - | null - | Array -> - -export function findStaticPlugins(source: string): [string, null | StaticPluginOptions][] | null { - try { - let tree = parser.parse(source) - let root = tree.rootNode - - let imports = extractStaticImportMap(source) - let captures = PLUGINS_QUERY.matches(root) - - let plugins: [string, null | StaticPluginOptions][] = [] - for (let match of captures) { - for (let capture of match.captures) { - if (capture.name !== 'imports') continue - - for (let pluginDefinition of capture.node.children) { - if ( - pluginDefinition.type === '[' || - pluginDefinition.type === ']' || - pluginDefinition.type === ',' - ) - continue - - switch (pluginDefinition.type) { - case 'identifier': - let source = imports[pluginDefinition.text] - if (!source || source.export !== null) { - return null - } - plugins.push([source.module, null]) - break - case 'string': - plugins.push([pluginDefinition.children[1].text, null]) - break - case 'call_expression': - let matches = PLUGIN_CALL_OPTIONS_QUERY.matches(pluginDefinition) - if (matches.length === 0) return null - - let moduleName: string | null = null - let moduleIdentifier: string | null = null - - let options: StaticPluginOptions | null = null - let lastProperty: string | null = null - - let captures = matches.flatMap((m) => m.captures) - for (let i = 0; i < captures.length; i++) { - let capture = captures[i] - switch (capture.name) { - case 'module_identifier': { - moduleIdentifier = capture.node.text - break - } - case 'module_string': { - moduleName = capture.node.text - break - } - case 'property': { - if (lastProperty !== null) return null - lastProperty = capture.node.text - break - } - case 'str_value': - case 'num_value': - case 'null_value': - case 'true_value': - case 'false_value': { - if (lastProperty === null) return null - options ??= {} - options[lastProperty] = extractValue(capture) - lastProperty = null - break - } - case 'array_value': { - if (lastProperty === null) return null - options ??= {} - - // Loop over all captures after this one that are on the - // same property (it will be one match for any array - // element) - let array: Array = [] - let lastConsumedIndex = i - arrayLoop: for (let j = i + 1; j < captures.length; j++) { - let innerCapture = captures[j] - - switch (innerCapture.name) { - case 'property': { - if (innerCapture.node.text !== lastProperty) { - break arrayLoop - } - break - } - case 'str_value': - case 'num_value': - case 'null_value': - case 'true_value': - case 'false_value': { - array.push(extractValue(innerCapture)) - lastConsumedIndex = j - } - } - } - - i = lastConsumedIndex - options[lastProperty] = array - lastProperty = null - break - } - - case '_name': - case '_empty_args': - break - default: - return null - } - } - - if (lastProperty !== null) return null - - if (moduleIdentifier !== null) { - let source = imports[moduleIdentifier] - if (!source || (source.export !== null && source.export !== '*')) { - return null - } - moduleName = source.module - } - - if (moduleName === null) { - return null - } - - plugins.push([moduleName, options]) - break - default: - return null - } - } - } - } - return plugins - } catch (error: any) { - error(`${error?.message ?? error}`, { prefix: '↳ ' }) - return null - } -} - -// Extract all top-level imports for both ESM and CJS files -const IMPORT_QUERY = new Parser.Query( - TS.typescript, - treesitter` - ; ESM import - (import_statement - (import_clause - (identifier)? @default - (named_imports - (import_specifier - name: (identifier) @imported-name - alias: (identifier)? @imported-alias - ) - )? - (namespace_import (identifier) @imported-namespace)? - ) - (string - (string_fragment) @imported-from) - ) - - ; CJS require - (variable_declarator - name: (identifier)? @default - name: (object_pattern - (shorthand_property_identifier_pattern)? @imported-name - (pair_pattern - key: (property_identifier) @imported-name - value: (identifier) @imported-alias - )? - (rest_pattern - (identifier) @imported-namespace - )? - )? - value: (call_expression - function: (identifier) @_fn (#eq? @_fn "require") - arguments: (arguments - (string - (string_fragment) @imported-from - ) - ) - ) - ) - `, -) - -export function extractStaticImportMap(source: string) { - let tree = parser.parse(source) - let root = tree.rootNode - - let captures = IMPORT_QUERY.matches(root) - - let imports: Record = {} - for (let match of captures) { - let toImport: { name: string; export: null | string }[] = [] - let from = '' - for (let i = 0; i < match.captures.length; i++) { - let capture = match.captures[i] - - switch (capture.name) { - case 'default': - toImport.push({ name: capture.node.text, export: null }) - break - case 'imported-name': - toImport.push({ name: capture.node.text, export: capture.node.text }) - break - case 'imported-from': - from = capture.node.text - break - case 'imported-namespace': - toImport.push({ name: capture.node.text, export: '*' }) - break - case 'imported-alias': - if (toImport.length < 1) { - throw new Error('Unexpected alias: ' + JSON.stringify(captures, null, 2)) - } - let prevImport = toImport[toImport.length - 1] - let name = prevImport.name - prevImport.export = name - prevImport.name = capture.node.text - break - } - } - - for (let { name, export: exportSource } of toImport) { - imports[name] = { module: from, export: exportSource } - } - } - - return imports -} - -function extractValue(capture: { name: string; node: { text: string } }) { - return capture.name === 'num_value' - ? parseFloat(capture.node.text) - : capture.name === 'null_value' - ? null - : capture.name === 'true_value' - ? true - : capture.name === 'false_value' - ? false - : capture.node.text -} diff --git a/packages/@tailwindcss-upgrade/src/utils/git.ts b/packages/@tailwindcss-upgrade/src/utils/git.ts index 38edf21cb..d716fbb8a 100644 --- a/packages/@tailwindcss-upgrade/src/utils/git.ts +++ b/packages/@tailwindcss-upgrade/src/utils/git.ts @@ -1,20 +1,8 @@ import { execSync } from 'node:child_process' -export function gitRoot(cwd?: string): string | null { +export function isRepoDirty() { try { - return execSync('git rev-parse --show-toplevel', { - encoding: 'utf-8', - cwd, - stdio: ['ignore', 'pipe', 'ignore'], - }).trim() - } catch { - return null - } -} - -export function isRepoDirty(cwd?: string) { - try { - let stdout = execSync('git status --porcelain', { encoding: 'utf-8', cwd }) + let stdout = execSync('git status --porcelain', { encoding: 'utf-8' }) return stdout.trim() !== '' } catch (error) { // If it's not a git repository we don't know if it's dirty or not. But we diff --git a/packages/@tailwindcss-upgrade/src/utils/package-version.ts b/packages/@tailwindcss-upgrade/src/utils/package-version.ts deleted file mode 100644 index 64a1e7ed1..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/package-version.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { readFileSync } from 'node:fs' -import fs from 'node:fs/promises' -import { resolveJsId } from './resolve' - -/** - * Resolves the version string of an npm dependency installed in the based - * directory. - */ -export async function getPackageVersion(pkg: string, base: string): Promise { - try { - let packageJson = resolveJsId(`${pkg}/package.json`, base) - if (!packageJson) return null - let { version } = JSON.parse(await fs.readFile(packageJson, 'utf8')) - return version - } catch { - return null - } -} - -export function getPackageVersionSync(pkg: string, base: string): string | null { - try { - let packageJson = resolveJsId(`${pkg}/package.json`, base) - if (!packageJson) return null - let { version } = JSON.parse(readFileSync(packageJson, 'utf8')) - return version - } catch { - return null - } -} diff --git a/packages/@tailwindcss-upgrade/src/utils/packages.ts b/packages/@tailwindcss-upgrade/src/utils/packages.ts index 461a6983a..837816b4c 100644 --- a/packages/@tailwindcss-upgrade/src/utils/packages.ts +++ b/packages/@tailwindcss-upgrade/src/utils/packages.ts @@ -1,80 +1,25 @@ -import { exec as execCb } from 'node:child_process' -import { readFileSync } from 'node:fs' +import { execSync } from 'node:child_process' import fs from 'node:fs/promises' import { dirname, resolve } from 'node:path' -import { promisify } from 'node:util' -import { DefaultMap } from '../../../tailwindcss/src/utils/default-map' -import { error, warn } from './renderer' - -const exec = promisify(execCb) - -const SAVE_DEV: Record = { - default: '-D', - 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() - if (location === 'devDependencies') { - 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 }) - } catch (e: any) { - error(`An error occurred while running \`${command}\`\n\n${e.stdout}\n${e.stderr}`, { - 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(' ')}` - try { - return await exec(command, { cwd: base }) - } catch (e: any) { - error(`An error occurred while running \`${command}\`\n\n${e.stdout}\n${e.stderr}`, { - prefix: '↳ ', - }) - throw e - } finally { - manifests.delete(base) - } - }, - } -} +import { warn } from './renderer' let didWarnAboutPackageManager = false -let packageManagerForBase = new DefaultMap(async (base) => { + +export async function pkg(command: string, base: string): Promise { + let packageManager = await detectPackageManager(base) + if (!packageManager) { + if (!didWarnAboutPackageManager) { + didWarnAboutPackageManager = true + warn('Could not detect a package manager. Please manually update `tailwindcss` to v4.') + } + return + } + return execSync(`${packageManager} ${command}`, { + cwd: base, + }) +} + +async function detectPackageManager(base: string): Promise { do { // 1. Check package.json for a `packageManager` field let packageJsonPath = resolve(base, 'package.json') @@ -122,17 +67,6 @@ let packageManagerForBase = new DefaultMap(async (base) => { } catch {} // 3. If no lockfile is found, we might be in a monorepo - let previousBase = base base = dirname(base) - - // Already at the root - if (previousBase === base) { - if (!didWarnAboutPackageManager) { - didWarnAboutPackageManager = true - warn('Could not detect a package manager. Please manually update `tailwindcss` to v4.') - } - - return Promise.reject('No package manager detected') - } } while (true) -}) +} diff --git a/packages/@tailwindcss-upgrade/src/utils/renderer.ts b/packages/@tailwindcss-upgrade/src/utils/renderer.ts index c9a967801..b9b9245f7 100644 --- a/packages/@tailwindcss-upgrade/src/utils/renderer.ts +++ b/packages/@tailwindcss-upgrade/src/utils/renderer.ts @@ -40,13 +40,7 @@ export function relative( /** * Wrap `text` into multiple lines based on the `width`. */ -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) => (line ? wordWrap(line, width) : [''])) - } - +export function wordWrap(text: string, width: number) { let words = text.split(' ') let lines = [] @@ -89,36 +83,32 @@ export function indent(value: string, offset = 0) { return `${' '.repeat(offset + UI.indent)}${value}` } -function log(message: string, { art = pc.gray('\u2502'), prefix = '', print = eprintln }) { - let prefixLength = prefix.length - let padding = ' ' - let paddingLength = padding.length - let artLength = stripVTControlCharacters(art).length - let availableWidth = process.stderr.columns - let totalWidth = availableWidth - prefixLength - paddingLength * 2 - artLength - - wordWrap(message, totalWidth).map((line, idx) => { - return print( - `${art}${padding}${idx === 0 ? prefix : ' '.repeat(prefixLength)}${line}${padding}`, - ) +export function success(message: string, print = eprintln) { + wordWrap(message, process.stderr.columns - 3).map((line) => { + return print(`${pc.green('\u2502')} ${line}`) }) print() } -export function success(message: string, { prefix = '', print = eprintln } = {}) { - log(message, { art: pc.green('\u2502'), prefix, print }) +export function info(message: string, print = eprintln) { + wordWrap(message, process.stderr.columns - 3).map((line) => { + return print(`${pc.blue('\u2502')} ${line}`) + }) + print() } -export function info(message: string, { prefix = '', print = eprintln } = {}) { - log(message, { art: pc.blue('\u2502'), prefix, print }) +export function error(message: string, print = eprintln) { + wordWrap(message, process.stderr.columns - 3).map((line) => { + return print(`${pc.red('\u2502')} ${line}`) + }) + print() } -export function error(message: string, { prefix = '', print = eprintln } = {}) { - log(message, { art: pc.red('\u2502'), prefix, print }) -} - -export function warn(message: string, { prefix = '', print = eprintln } = {}) { - log(message, { art: pc.yellow('\u2502'), prefix, print }) +export function warn(message: string, print = eprintln) { + wordWrap(message, process.stderr.columns - 3).map((line) => { + return print(`${pc.yellow('\u2502')} ${line}`) + }) + print() } // Rust inspired functions to print to the console: diff --git a/packages/@tailwindcss-upgrade/src/utils/resolve.ts b/packages/@tailwindcss-upgrade/src/utils/resolve.ts index 58f5834ba..e9197e874 100644 --- a/packages/@tailwindcss-upgrade/src/utils/resolve.ts +++ b/packages/@tailwindcss-upgrade/src/utils/resolve.ts @@ -13,28 +13,6 @@ export function resolve(id: string) { return localResolve(id) } -const esmResolver = EnhancedResolve.ResolverFactory.createResolver({ - fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), - useSyncFileSystemCalls: true, - extensions: ['.js', '.json', '.node', '.ts'], - conditionNames: ['node', 'import'], -}) - -const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({ - fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), - useSyncFileSystemCalls: true, - extensions: ['.js', '.json', '.node', '.ts'], - conditionNames: ['node', 'require'], -}) - -export function resolveJsId(id: string, base: string) { - try { - return esmResolver.resolveSync({}, base, id) - } catch { - return cjsResolver.resolveSync({}, base, id) - } -} - const resolver = EnhancedResolve.ResolverFactory.createResolver({ fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), useSyncFileSystemCalls: true, @@ -43,5 +21,12 @@ const resolver = EnhancedResolve.ResolverFactory.createResolver({ conditionNames: ['style'], }) export function resolveCssId(id: string, base: string) { + if (typeof globalThis.__tw_resolve === 'function') { + let resolved = globalThis.__tw_resolve(id, base) + if (resolved) { + return resolved + } + } + return resolver.resolveSync({}, base, id) } diff --git a/packages/@tailwindcss-upgrade/src/utils/splice-changes-into-string.ts b/packages/@tailwindcss-upgrade/src/utils/splice-changes-into-string.ts deleted file mode 100644 index e1e6f1685..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/splice-changes-into-string.ts +++ /dev/null @@ -1,42 +0,0 @@ -export interface StringChange { - start: number - end: number - replacement: string -} - -/** - * Apply the changes to the string such that a change in the length - * of the string does not break the indexes of the subsequent changes. - */ -export function spliceChangesIntoString(str: string, changes: StringChange[]) { - // If there are no changes, return the original string - if (!changes[0]) return str - - // Sort all changes in order to make it easier to apply them - changes.sort((a, b) => { - return a.end - b.end || a.start - b.start - }) - - // Append original string between each chunk, and then the chunk itself - // This is sort of a String Builder pattern, thus creating less memory pressure - let result = '' - - let previous = changes[0] - - result += str.slice(0, previous.start) - result += previous.replacement - - for (let i = 1; i < changes.length; ++i) { - let change = changes[i] - - result += str.slice(previous.end, change.start) - result += change.replacement - - previous = change - } - - // Add leftover string from last chunk to end - result += str.slice(previous.end) - - return result -} diff --git a/packages/@tailwindcss-upgrade/src/utils/version.ts b/packages/@tailwindcss-upgrade/src/utils/version.ts deleted file mode 100644 index 27bc44f2b..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/version.ts +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index e0dd16899..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/walk-variants.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/walk.ts b/packages/@tailwindcss-upgrade/src/utils/walk.ts index f94bdebb4..7a86b7ae5 100644 --- a/packages/@tailwindcss-upgrade/src/utils/walk.ts +++ b/packages/@tailwindcss-upgrade/src/utils/walk.ts @@ -1,4 +1,4 @@ -export const enum WalkAction { +export enum WalkAction { // Continue walking the tree. Default behavior. Continue, @@ -15,14 +15,11 @@ interface Walkable { // Custom walk implementation where we can skip going into nodes when we don't // need to process them. -export function walk( - rule: Walkable, - cb: (rule: T, idx: number, parent: Walkable) => void | WalkAction, -): undefined | false { +export function walk(rule: Walkable, cb: (rule: T) => void | WalkAction): undefined | false { let result: undefined | false = undefined - rule.each?.((node, idx) => { - let action = cb(node, idx, rule) ?? WalkAction.Continue + rule.each?.((node) => { + let action = cb(node) ?? WalkAction.Continue if (action === WalkAction.Stop) { result = false return result diff --git a/packages/@tailwindcss-upgrade/src/utils/write-file-safely.ts b/packages/@tailwindcss-upgrade/src/utils/write-file-safely.ts deleted file mode 100644 index 2ff99f9e7..000000000 --- a/packages/@tailwindcss-upgrade/src/utils/write-file-safely.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { randomUUID } from 'node:crypto' -import fs from 'node:fs/promises' -import path from 'node:path' - -export async function writeFileSafely(file: string, contents: string) { - // Resolve symlinks so the rename replaces the actual target file rather than - // replacing the symlink itself with a regular file. - let realFile = await fs.realpath(file).catch(() => file) - - // Start by creating a new file in the current directory that is guaranteed to - // be unique (via `uuid`). We can embed the `process.id` in case we need to - // debug things later. - // - // While we can write this to a more global `/tmp` folder, I want to be 100% - // sure that we are on the same file system (same drive) so the rename - // operation is atomic. Once the file is written, we will rename the file. If - // this fails, the old file is still intact, if it works we have an updated - // file. - // - // If this still causes problems (but it will slow things down): - // 1. We could make sure that we inherit the file permissions - // 2. Use an explicit fsync to force a flush to disk - let temporaryFile = path.join( - path.dirname(realFile), - `.${path.basename(realFile)}.tailwind-upgrade.${process.pid}.${randomUUID()}.tmp`, - ) - - // Write file uses the `w` flag by default, which is defined as: - // > Open file for writing. The file is created (if it does not exist) or truncated (if it exists). - // > https://nodejs.org/api/fs.html#file-system-flags - // - // Which means that if this function is actively running, and you cancel the - // process at the wrong time, then the truncated files are present. Since all - // these migrations happen in parallel, multiple files are open and available - // to be written to, it could mean in multiple truncated files. - // - // Writing to a temp file first means that if the process is cancelled at this - // point, that the old original file is still correct. - // - // The rename part should be atomic (especially because we guarantee it to be - // on the same file system) so this either succeeds or doesn't happen. - try { - await fs.writeFile(temporaryFile, contents, 'utf8') - await fs.rename(temporaryFile, realFile) - } catch (error) { - await fs.unlink(temporaryFile).catch(() => {}) - throw error - } -} diff --git a/packages/@tailwindcss-upgrade/tsconfig.json b/packages/@tailwindcss-upgrade/tsconfig.json index c4a3e047a..6ae022f65 100644 --- a/packages/@tailwindcss-upgrade/tsconfig.json +++ b/packages/@tailwindcss-upgrade/tsconfig.json @@ -1,6 +1,3 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { - "allowSyntheticDefaultImports": true, - }, } diff --git a/packages/@tailwindcss-vite/README.md b/packages/@tailwindcss-vite/README.md index 1a5d25c73..95ec9d87d 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,50 +27,14 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ## Community -For help, discussion about best practices, or feature ideas: +For help, discussion about best practices, or any other conversation that would benefit from being searchable: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[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) ## 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/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 }, - }), - ], -}) -``` +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-vite/package.json b/packages/@tailwindcss-vite/package.json index 821704a1d..4f1395660 100644 --- a/packages/@tailwindcss-vite/package.json +++ b/packages/@tailwindcss-vite/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/vite", - "version": "4.3.3", + "version": "4.0.0-alpha.26", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -24,19 +24,20 @@ "exports": { ".": { "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" + "import": "./dist/index.mjs" } }, "dependencies": { - "@tailwindcss/node": "workspace:*", - "@tailwindcss/oxide": "workspace:*", - "tailwindcss": "workspace:*" + "@tailwindcss/node": "workspace:^", + "@tailwindcss/oxide": "workspace:^", + "lightningcss": "catalog:", + "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", "vite": "catalog:" }, "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" + "vite": "^5.2.0" } } diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 8fc660eaa..abb13a613 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -1,177 +1,133 @@ -import { - compile, - env, - Features, - Instrumentation, - normalizePath, - optimize, - toSourceMap, -} from '@tailwindcss/node' +import { compile, env, normalizePath } 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 { Environment, InternalResolveOptions, Plugin, ResolvedConfig } from 'vite' -import * as vite from 'vite' +import { Features, transform } from 'lightningcss' +import path from 'path' +import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite' -const DEBUG = env.DEBUG -const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/ -const COMMON_JS_PROXY_RE = /\?commonjs-proxy/ -const INLINE_STYLE_ID_RE = /[?&]index=\d+\.css$/ - -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[] { +export default function tailwindcss(): Plugin[] { + let servers: ViteDevServer[] = [] let config: ResolvedConfig | null = null - let rootsByEnv = new DefaultMap>((env: string) => new Map()) let isSSR = false - let shouldOptimize = true - let minify = true + let minify = false - function createRoot(env: Environment | null, id: string) { - type ResolveFn = (id: string, base: string) => Promise + // A list of css plugins defined in the Vite config. We need to retain these + // so that we can rerun the right transformations in build mode where we have + // to manually rebuild the css file after the compilation is done. + let cssPlugins: readonly Plugin[] = [] - let customCssResolver: ResolveFn - let customJsResolver: ResolveFn + // The Vite extension has two types of sources for candidates: + // + // 1. The module graph: These are all modules that vite transforms and we want + // them to be automatically scanned for candidates. + // 2. Root defined `@source`s + // + // Module graph candidates are global to the Vite extension since we do not + // know which CSS roots will be used for the modules. We are using a custom + // scanner instance with auto source discovery disabled to parse these. + // + // For candidates coming from custom `@source` directives of the CSS roots, we + // create an individual scanner for each root. + // + // Note: To improve performance, we do not remove candidates from this set. + // This means a longer-ongoing dev mode session might contain candidates that + // are no longer referenced in code. + let moduleGraphCandidates = new Set() + let moduleGraphScanner = new Scanner({}) - 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 roots: DefaultMap = new DefaultMap( + (id) => new Root(id, () => moduleGraphCandidates, config!.base), + ) - 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'), - ) + function scanFile(id: string, content: string, extension: string, isSSR: boolean) { + let updated = false + for (let candidate of moduleGraphScanner.scanFiles([{ content, extension }])) { + updated = true + moduleGraphCandidates.add(candidate) } - 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, - ) + if (updated) { + invalidateAllRoots(isSSR) + } + } + + function invalidateAllRoots(isSSR: boolean) { + for (let server of servers) { + let updates: Update[] = [] + for (let id of roots.keys()) { + let module = server.moduleGraph.getModuleById(id) + if (!module) { + // Note: Removing this during SSR is not safe and will produce + // inconsistent results based on the timing of the removal and + // the order / timing of transforms. + if (!isSSR) { + // It is safe to remove the item here since we're iterating on a copy + // of the keys. + roots.delete(id) + } + continue + } + + roots.get(id).requiresRebuild = false + server.moduleGraph.invalidateModule(module) + updates.push({ + type: `${module.type}-update`, + path: module.url, + acceptedPath: module.url, + timestamp: Date.now(), + }) + } + + if (updates.length > 0) { + server.hot.send({ type: 'update', updates }) + } + } + } + + async function regenerateOptimizedCss(root: Root, addWatchFile: (file: string) => void) { + let content = root.lastContent + let generated = await root.generate(content, addWatchFile) + if (generated === false) { + return + } + env.DEBUG && console.time('[@tailwindcss/vite] Optimize CSS') + let result = optimizeCss(generated, { minify }) + env.DEBUG && console.timeEnd('[@tailwindcss/vite] Optimize CSS') + return result + } + + // Manually run the transform functions of non-Tailwind plugins on the given CSS + async function transformWithPlugins(context: Rollup.PluginContext, id: string, css: string) { + let transformPluginContext = { + ...context, + getCombinedSourcemap: () => { + throw new Error('getCombinedSourcemap not implemented') + }, + } + + for (let plugin of cssPlugins) { + if (!plugin.transform) continue + let transformHandler = + 'handler' in plugin.transform! ? plugin.transform.handler : plugin.transform! + + try { + // Directly call the plugin's transform function to process the + // generated CSS. In build mode, this updates the chunks later used to + // generate the bundle. In serve mode, the transformed source should be + // applied in transform. + let result = await transformHandler.call(transformPluginContext, css, id) + if (!result) continue + if (typeof result === 'string') { + css = result + } else if (result.code) { + css = result.code + } + } catch (e) { + console.error(`Error running ${plugin.name} on Tailwind CSS output. Skipping.`) + } + } + return css } return [ @@ -180,24 +136,38 @@ export default function tailwindcss(opts: PluginOptions = {}): 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 - } + let allowedPlugins = [ + // Apply the vite:css plugin to generated CSS for transformations like + // URL path rewriting and image inlining. + 'vite:css', - // Minification is also performed when optimizing as long as it's also - // enabled in Vite - minify = shouldOptimize && config.build.cssMinify !== false + // In build mode, since renderChunk runs after all transformations, we + // need to also apply vite:css-post. + ...(config.command === 'build' ? ['vite:css-post'] : []), + ] - // But again, the user can override that choice explicitly - if (typeof opts.optimize === 'object') { - minify = opts.optimize.minify !== false - } + cssPlugins = config.plugins.filter((plugin) => { + return allowedPlugins.includes(plugin.name) + }) + }, + + // Scan all non-CSS files for candidates + transformIndexHtml(html, { path }) { + scanFile(path, html, 'html', isSSR) + }, + transform(src, id, options) { + let extension = getExtension(id) + if (isPotentialCssRootFile(id)) return + scanFile(id, src, extension, options?.ssr ?? false) }, }, @@ -206,35 +176,29 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { name: '@tailwindcss/vite:generate:serve', apply: 'serve', enforce: 'pre', - transform: { - filter: { - id: { - exclude: [/\/\.vite\//, SPECIAL_QUERY_RE, COMMON_JS_PROXY_RE], - include: [/\.css(?:\?.*)?$/, /&lang\.css/, INLINE_STYLE_ID_RE], - }, - }, - async handler(src, id) { - if (!isPotentialCssRootFile(id)) return - using I = new Instrumentation() - DEBUG && I.start('[@tailwindcss/vite] Generate CSS (serve)') + async transform(src, id, options) { + if (!isPotentialCssRootFile(id)) return - let roots = rootsByEnv.get(this.environment?.name ?? 'default') - let root = roots.get(id) - if (!root) { - root ??= createRoot(this.environment ?? null, id) - roots.set(id, root) - } + let root = roots.get(id) - let result = await root.generate(src, (file) => this.addWatchFile(file), I) - if (!result) { - roots.delete(id) - return - } + if (!options?.ssr) { + // Wait until all other files have been processed, so we can extract + // all candidates before generating CSS. This must not be called + // during SSR or it will block the server. + // + // The reason why we can not rely on the invalidation here is that the + // users would otherwise see a flicker in the styles as the CSS might + // be loaded with an invalid set of candidates first. + await Promise.all(servers.map((server) => server.waitForRequestsIdle(id))) + } - DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)') - return result - }, + let generated = await root.generate(src, (file) => this.addWatchFile(file)) + if (!generated) { + roots.delete(id) + return src + } + return { code: generated } }, }, @@ -243,44 +207,47 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { name: '@tailwindcss/vite:generate:build', apply: 'build', enforce: 'pre', - transform: { - filter: { - id: { - exclude: [/\/\.vite\//, SPECIAL_QUERY_RE, COMMON_JS_PROXY_RE], - include: [/\.css(?:\?.*)?$/, /&lang\.css/, INLINE_STYLE_ID_RE], - }, - }, - async handler(src, id) { - if (!isPotentialCssRootFile(id)) return - using I = new Instrumentation() - DEBUG && I.start('[@tailwindcss/vite] Generate CSS (build)') + async transform(src, id) { + if (!isPotentialCssRootFile(id)) return - let roots = rootsByEnv.get(this.environment?.name ?? 'default') - let root = roots.get(id) - if (!root) { - root ??= createRoot(this.environment ?? null, id) - roots.set(id, root) - } + let root = roots.get(id) - let result = await root.generate(src, (file) => this.addWatchFile(file), I) - if (!result) { + // We do a first pass to generate valid CSS for the downstream plugins. + // However, since not all candidates are guaranteed to be extracted by + // this time, we have to re-run a transform for the root later. + let generated = await root.generate(src, (file) => this.addWatchFile(file)) + if (!generated) { + roots.delete(id) + return src + } + return { code: generated } + }, + + // `renderStart` runs in the bundle generation stage after all transforms. + // We must run before `enforce: post` so the updated chunks are picked up + // by vite:css-post. + async renderStart() { + for (let [id, root] of roots.entries()) { + let generated = await regenerateOptimizedCss( + root, + // During the renderStart phase, we can not add watch files since + // those would not be causing a refresh of the right CSS file. This + // should not be an issue since we did already process the CSS file + // before and the dependencies should not be changed (only the + // candidate list might have) + () => {}, + ) + if (!generated) { roots.delete(id) - return - } - 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') + continue } - return result - }, + // These plugins have side effects which, during build, results in CSS + // being written to the output dir. We need to run them here to ensure + // the CSS is written before the bundle is generated. + await transformWithPlugins(this, id, generated) + } }, }, ] satisfies Plugin[] @@ -292,18 +259,38 @@ function getExtension(id: string) { } function isPotentialCssRootFile(id: string) { - if (id.includes('/.vite/')) return false - - // Don't intercept special static asset resources - if (SPECIAL_QUERY_RE.test(id)) return false - if (COMMON_JS_PROXY_RE.test(id)) return false - let extension = getExtension(id) - let isCssFile = extension === 'css' || id.includes('&lang.css') || id.match(INLINE_STYLE_ID_RE) - + let isCssFile = + extension === 'css' || + (extension === 'vue' && id.includes('&lang.css')) || + (extension === 'astro' && id.includes('&lang.css')) return isCssFile } +function optimizeCss( + input: string, + { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {}, +) { + return transform({ + filename: file, + code: Buffer.from(input), + minify, + sourceMap: false, + drafts: { + customMedia: true, + }, + nonStandard: { + deepSelectorCombinator: true, + }, + include: Features.Nesting, + exclude: Features.LogicalProperties, + targets: { + safari: (16 << 16) | (4 << 8), + }, + errorRecovery: true, + }).code.toString() +} + function idToPath(id: string) { return path.resolve(id.replace(/\?.*$/, '')) } @@ -330,214 +317,103 @@ class DefaultMap extends Map { } class Root { + // Content is only used in serve mode where we need to capture the initial + // contents of the root file so that we can restore it during the + // `renderStart` hook. + public lastContent: string = '' + // The lazily-initialized Tailwind compiler components. These are persisted // throughout rebuilds but will be re-initialized if the rebuild strategy is // set to `full`. private compiler?: Awaited> - // The lazily-initialized Tailwind scanner. + public requiresRebuild: boolean = true + + // This is the compiler-specific scanner instance that is used only to scan + // files for custom @source paths. All other modules we scan for candidates + // will use the shared moduleGraphScanner instance. private scanner?: Scanner // List of all candidates that were being returned by the root scanner during // the lifetime of the root. private candidates: Set = new Set() - // List of all build dependencies (e.g. imported stylesheets or plugins) and - // their last modification timestamp. If no mtime can be found, we need to - // assume the file has always changed. - private buildDependencies = new Map() + // List of all file dependencies that were captured while generating the root. + // These are retained so we can clear the require cache when we rebuild the + // root. + private dependencies = new Set() constructor( private id: string, + private getSharedCandidates: () => Set, private base: string, - - private enableSourceMaps: boolean, - private customCssResolver: (id: string, base: string) => Promise, - private customJsResolver: (id: string, base: string) => Promise, ) {} // Generate the CSS for the root file. This can return false if the file is // not considered a Tailwind root. When this happened, the root can be GCed. public async generate( content: string, - _addWatchFile: (file: string) => void, - I: Instrumentation, - ): Promise< - | { - code: string - map: string | undefined - } - | false - > { + addWatchFile: (file: string) => void, + ): Promise { + this.lastContent = content + let inputPath = idToPath(this.id) - - function addWatchFile(file: string) { - // 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 - } - - // Scanning `.svg` file containing a `#` or `?` in the path will - // crash Vite. We work around this for now by ignoring updates to them. - // - // https://github.com/tailwindlabs/tailwindcss/issues/16877 - if (/[#?].*\.svg$/.test(file)) { - return - } - _addWatchFile(file) - } - - let requiresBuildPromise = this.requiresBuild() let inputBase = path.dirname(path.resolve(inputPath)) - if (!this.compiler || !this.scanner || (await requiresBuildPromise)) { - clearRequireCache(Array.from(this.buildDependencies.keys())) - this.buildDependencies.clear() + if (!this.compiler || !this.scanner || this.requiresRebuild) { + clearRequireCache(Array.from(this.dependencies)) + this.dependencies = new Set([idToPath(inputPath)]) - this.addBuildDependency(idToPath(inputPath)) - - DEBUG && I.start('Setup compiler') - let addBuildDependenciesPromises: Promise[] = [] + env.DEBUG && console.time('[@tailwindcss/vite] Setup compiler') this.compiler = await compile(content, { - from: this.enableSourceMaps ? this.id : undefined, base: inputBase, - shouldRewriteUrls: true, onDependency: (path) => { addWatchFile(path) - addBuildDependenciesPromises.push(this.addBuildDependency(path)) + this.dependencies.add(path) }, - - customCssResolver: this.customCssResolver, - customJsResolver: this.customJsResolver, }) - await Promise.all(addBuildDependenciesPromises) - DEBUG && I.end('Setup compiler') + env.DEBUG && console.timeEnd('[@tailwindcss/vite] Setup compiler') - DEBUG && I.start('Setup scanner') + this.scanner = new Scanner({ + sources: this.compiler.globs, + }) + } - let sources = (() => { - // Disable auto source detection - if (this.compiler.root === 'none') { - return [] - } + // This should not be here, but right now the Vite plugin is setup where we + // setup a new scanner and compiler every time we request the CSS file + // (regardless whether it actually changed or not). + env.DEBUG && console.time('[@tailwindcss/vite] Scan for candidates') + for (let candidate of this.scanner.scan()) { + this.candidates.add(candidate) + } + env.DEBUG && console.timeEnd('[@tailwindcss/vite] Scan for candidates') - // No root specified, auto-detect based on the `**/*` pattern - if (this.compiler.root === null) { - return [{ base: this.base, pattern: '**/*', negated: false }] - } + // Watch individual files found via custom `@source` paths + for (let file of this.scanner.files) { + addWatchFile(file) + } - // Use the specified root - return [{ ...this.compiler.root, negated: false }] - })().concat(this.compiler.sources) + // Watch globs found via custom `@source` paths + for (let glob of this.scanner.globs) { + if (glob.pattern[0] === '!') continue - this.scanner = new Scanner({ sources }) - DEBUG && I.end('Setup scanner') - } else { - for (let buildDependency of this.buildDependencies.keys()) { - addWatchFile(buildDependency) + let relative = path.relative(this.base, glob.base) + if (relative[0] !== '.') { + relative = './' + relative } + // Ensure relative is a posix style path since we will merge it with the + // glob. + relative = normalizePath(relative) + + addWatchFile(path.posix.join(relative, glob.pattern)) } - if ( - !( - this.compiler.features & - (Features.AtApply | - Features.JsPluginCompat | - Features.ThemeFunction | - Features.Utilities | - Features.Variants) - ) - ) { - return false - } + this.requiresRebuild = true - if (this.compiler.features & Features.Utilities) { - // This should not be here, but right now the Vite plugin is setup where we - // setup a new scanner and compiler every time we request the CSS file - // (regardless whether it actually changed or not). - DEBUG && I.start('Scan for candidates') - for (let candidate of this.scanner.scan()) { - this.candidates.add(candidate) - } - DEBUG && I.end('Scan for candidates') - } + env.DEBUG && console.time('[@tailwindcss/vite] Build CSS') + let result = this.compiler.build([...this.getSharedCandidates(), ...this.candidates]) + env.DEBUG && console.timeEnd('[@tailwindcss/vite] Build CSS') - 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) - } - - // Watch globs found via custom `@source` paths - for (let glob of this.scanner.globs) { - if (glob.pattern[0] === '!') continue - - let relative = path.relative(this.base, glob.base) - if (relative[0] !== '.') { - relative = './' + relative - } - // Ensure relative is a posix style path since we will merge it with the - // glob. - relative = normalizePath(relative) - - addWatchFile(path.posix.join(relative, glob.pattern)) - - let root = this.compiler.root - - if (root !== 'none' && root !== null) { - let basePath = normalizePath(path.resolve(root.base, root.pattern)) - - let isDir = await fs.stat(basePath).then( - (stats) => stats.isDirectory(), - () => false, - ) - - if (!isDir) { - throw new Error( - `The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`, - ) - } - } - } - DEBUG && I.end('Register dependency messages') - } - - DEBUG && I.start('Build CSS') - let code = this.compiler.build([...this.candidates]) - DEBUG && I.end('Build CSS') - - 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) { - let mtime: number | null = null - try { - mtime = (await fs.stat(path)).mtimeMs - } catch {} - this.buildDependencies.set(path, mtime) - } - - private async requiresBuild(): Promise { - for (let [path, mtime] of this.buildDependencies) { - if (mtime === null) return true - try { - let stat = await fs.stat(path) - if (stat.mtimeMs > mtime) { - return true - } - } catch { - return true - } - } - return false + return result } } diff --git a/packages/@tailwindcss-webpack/README.md b/packages/@tailwindcss-webpack/README.md deleted file mode 100644 index 56484ed7f..000000000 --- a/packages/@tailwindcss-webpack/README.md +++ /dev/null @@ -1,102 +0,0 @@ -

- - - - - Tailwind CSS - - -

- -

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

- -

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

- ---- - -## Documentation - -For full documentation, visit [tailwindcss.com](https://tailwindcss.com). - -## Community - -For help, discussion about best practices, or feature ideas: - -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) - -## Contributing - -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. - ---- - -## @tailwindcss/webpack - -A webpack loader for Tailwind CSS v4. - -## Installation - -```sh -npm install @tailwindcss/webpack -``` - -### Usage - -```javascript -// webpack.config.js -const MiniCssExtractPlugin = require('mini-css-extract-plugin') - -module.exports = { - plugins: [new MiniCssExtractPlugin()], - module: { - rules: [ - { - test: /\.css$/i, - use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], - }, - ], - }, -} -``` - -Then create a CSS file that imports Tailwind: - -```css -/* src/index.css */ -@import 'tailwindcss'; -``` - -### Options - -#### `base` - -The base directory to scan for class candidates. Defaults to the current working directory. - -```javascript -{ - loader: '@tailwindcss/webpack', - options: { - base: process.cwd(), - }, -} -``` - -#### `optimize` - -Whether to optimize and minify the output CSS. Defaults to `true` in production mode. - -```javascript -{ - loader: '@tailwindcss/webpack', - options: { - optimize: true, // or { minify: true } - }, -} -``` diff --git a/packages/@tailwindcss-webpack/package.json b/packages/@tailwindcss-webpack/package.json deleted file mode 100644 index dce09429d..000000000 --- a/packages/@tailwindcss-webpack/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@tailwindcss/webpack", - "version": "4.3.3", - "description": "A webpack loader for Tailwind CSS v4.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/tailwindlabs/tailwindcss.git", - "directory": "packages/@tailwindcss-webpack" - }, - "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", - "homepage": "https://tailwindcss.com", - "scripts": { - "build": "tsup-node", - "dev": "pnpm run build -- --watch" - }, - "files": [ - "dist/" - ], - "publishConfig": { - "provenance": true, - "access": "public" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "workspace:*", - "@tailwindcss/oxide": "workspace:*", - "tailwindcss": "workspace:*" - }, - "devDependencies": { - "@types/node": "catalog:", - "webpack": "catalog:" - }, - "peerDependencies": { - "@rspack/core": "^1.0.0 || ^2.0.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } -} diff --git a/packages/@tailwindcss-webpack/src/index.cts b/packages/@tailwindcss-webpack/src/index.cts deleted file mode 100644 index dbdded748..000000000 --- a/packages/@tailwindcss-webpack/src/index.cts +++ /dev/null @@ -1,5 +0,0 @@ -import tailwindLoader from './index.ts' - -// CommonJS export for webpack loaders - must be the function directly -// @ts-ignore -export = tailwindLoader diff --git a/packages/@tailwindcss-webpack/src/index.ts b/packages/@tailwindcss-webpack/src/index.ts deleted file mode 100644 index 6d02f1f0b..000000000 --- a/packages/@tailwindcss-webpack/src/index.ts +++ /dev/null @@ -1,287 +0,0 @@ -import QuickLRU from '@alloc/quick-lru' -import { - compile, - env, - Features, - Instrumentation, - normalizePath, - optimize, - Polyfills, -} from '@tailwindcss/node' -import { clearRequireCache } from '@tailwindcss/node/require-cache' -import { Scanner } from '@tailwindcss/oxide' -import fs from 'node:fs' -import path from 'node:path' -import type { LoaderContext } from 'webpack' - -const DEBUG = env.DEBUG - -export interface LoaderOptions { - /** - * The base directory to scan for class candidates. - * - * Defaults to the current working directory. - */ - base?: string - - /** - * Optimize and minify the output CSS. - */ - optimize?: boolean | { minify?: boolean } -} - -interface CacheEntry { - mtimes: Map - compiler: null | Awaited> - scanner: null | Scanner - candidates: Set - fullRebuildPaths: string[] -} - -const cache = new QuickLRU({ maxSize: 50 }) - -function getCacheKey(resourceId: string, opts: LoaderOptions): string { - return `${resourceId}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}` -} - -function getContextFromCache(resourceId: string, opts: LoaderOptions): CacheEntry { - let key = getCacheKey(resourceId, opts) - if (cache.has(key)) return cache.get(key)! - let entry: CacheEntry = { - mtimes: new Map(), - compiler: null, - scanner: null, - candidates: new Set(), - fullRebuildPaths: [], - } - cache.set(key, entry) - return entry -} - -export default async function tailwindLoader( - this: LoaderContext, - source: string, -): Promise { - let callback = this.async() - let options = this.getOptions() ?? {} - let inputFile = this.resourcePath - let resourceId = this.resource - let base = options.base ?? process.cwd() - let shouldOptimize = options.optimize ?? process.env.NODE_ENV === 'production' - let isCSSModuleFile = inputFile.endsWith('.module.css') - - using I = new Instrumentation() - - DEBUG && I.start(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - - // Bail out early if this is guaranteed to be a non-Tailwind CSS file. - { - DEBUG && I.start('Quick bail check') - let canBail = !/@(import|reference|theme|variant|config|plugin|apply|tailwind)\b/.test(source) - if (canBail) { - DEBUG && I.end('Quick bail check') - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(null, source) - return - } - DEBUG && I.end('Quick bail check') - } - - try { - let context = getContextFromCache(resourceId, options) - let inputBasePath = path.dirname(path.resolve(inputFile)) - - // Whether this is the first build or not - let isInitialBuild = context.compiler === null - - async function createCompiler() { - DEBUG && I.start('Setup compiler') - if (context.fullRebuildPaths.length > 0 && !isInitialBuild) { - clearRequireCache(context.fullRebuildPaths) - } - - context.fullRebuildPaths = [] - - DEBUG && I.start('Create compiler') - let compiler = await compile(source, { - from: inputFile, - base: inputBasePath, - shouldRewriteUrls: true, - onDependency: (depPath) => context.fullRebuildPaths.push(depPath), - // In CSS Module files, we have to disable the `@property` polyfill since these will - // emit global `*` rules which are considered to be non-pure and will cause builds - // to fail. - polyfills: isCSSModuleFile ? Polyfills.All ^ Polyfills.AtProperty : Polyfills.All, - }) - DEBUG && I.end('Create compiler') - - DEBUG && I.end('Setup compiler') - return compiler - } - - // Setup the compiler if it doesn't exist yet - context.compiler ??= await createCompiler() - - // Early exit if no Tailwind features are used - if (context.compiler.features === Features.None) { - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(null, source) - return - } - - let rebuildStrategy: 'full' | 'incremental' = 'incremental' - - // Track file modification times to CSS files - DEBUG && I.start('Register full rebuild paths') - { - // Report dependencies for config files, plugins, etc. - for (let file of context.fullRebuildPaths) { - this.addDependency(path.resolve(file)) - } - - let files = [...context.fullRebuildPaths, inputFile] - - for (let file of files) { - let changedTime: number | null = null - try { - changedTime = fs.statSync(file)?.mtimeMs ?? null - } catch { - // File might not exist - } - - if (changedTime === null) { - if (file === inputFile) { - rebuildStrategy = 'full' - } - continue - } - - let prevTime = context.mtimes.get(file) - if (prevTime === changedTime) continue - - rebuildStrategy = 'full' - context.mtimes.set(file, changedTime) - } - } - DEBUG && I.end('Register full rebuild paths') - - if (rebuildStrategy === 'full' && !isInitialBuild) { - context.compiler = await createCompiler() - } - - let compiler = context.compiler - - // Check if we need to process this file at all - if ( - !( - compiler.features & - (Features.AtApply | Features.JsPluginCompat | Features.ThemeFunction | Features.Utilities) - ) - ) { - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(null, source) - return - } - - // Setup or update scanner if needed - if (context.scanner === null || rebuildStrategy === 'full') { - DEBUG && I.start('Setup scanner') - let sources = (() => { - // Disable auto source detection - if (compiler.root === 'none') { - return [] - } - - // No root specified, use the base directory - if (compiler.root === null) { - return [{ base, pattern: '**/*', negated: false }] - } - - // Use the specified root - return [{ ...compiler.root, negated: false }] - })().concat(compiler.sources) - - context.scanner = new Scanner({ sources }) - DEBUG && I.end('Setup scanner') - } - - // Scan for candidates if utilities are used - if (compiler.features & Features.Utilities) { - DEBUG && I.start('Scan for candidates') - for (let candidate of context.scanner.scan()) { - context.candidates.add(candidate) - } - DEBUG && I.end('Scan for candidates') - - DEBUG && I.start('Register dependency messages') - // Add all found files as direct dependencies - let resolvedInputFile = path.resolve(base, inputFile) - for (let file of context.scanner.files) { - let absolutePath = path.resolve(file) - // The CSS file cannot be a dependency of itself - if (absolutePath === resolvedInputFile) { - continue - } - this.addDependency(absolutePath) - } - - // Register context dependencies for glob patterns - for (let glob of context.scanner.globs) { - // Skip negated patterns - if (glob.pattern[0] === '!') continue - - // Avoid adding a dependency on the base directory itself - if (glob.pattern === '*' && base === glob.base) { - continue - } - - this.addContextDependency(path.resolve(glob.base)) - } - - // Validate that source(...) paths are directories - let root = compiler.root - if (root !== 'none' && root !== null) { - let basePath = normalizePath(path.resolve(root.base, root.pattern)) - try { - let stats = fs.statSync(basePath) - if (!stats.isDirectory()) { - throw new Error( - `The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`, - ) - } - } catch (err) { - if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { - throw err - } - // Directory doesn't exist yet, which is fine - } - } - DEBUG && I.end('Register dependency messages') - } - - DEBUG && I.start('Build utilities') - let css = compiler.build([...context.candidates]) - DEBUG && I.end('Build utilities') - - // Optionally optimize the output - let result = css - if (shouldOptimize) { - DEBUG && I.start('Optimization') - let optimized = optimize(css, { - minify: typeof shouldOptimize === 'object' ? shouldOptimize.minify : true, - }) - result = optimized.code - DEBUG && I.end('Optimization') - } - - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(null, result) - } catch (error) { - // Clear the cache entry on error to force a full rebuild next time - let key = getCacheKey(resourceId, options) - cache.delete(key) - - DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`) - callback(error as Error) - } -} diff --git a/packages/@tailwindcss-webpack/tsconfig.json b/packages/@tailwindcss-webpack/tsconfig.json deleted file mode 100644 index d2131cbeb..000000000 --- a/packages/@tailwindcss-webpack/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "compilerOptions": { - "verbatimModuleSyntax": false, - }, -} diff --git a/packages/@tailwindcss-webpack/tsup.config.ts b/packages/@tailwindcss-webpack/tsup.config.ts deleted file mode 100644 index 998eebe4f..000000000 --- a/packages/@tailwindcss-webpack/tsup.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from 'tsup' - -export default defineConfig([ - { - format: ['esm'], - clean: true, - minify: true, - cjsInterop: true, - dts: true, - entry: ['src/index.ts'], - }, - { - format: ['cjs'], - minify: true, - cjsInterop: true, - dts: true, - entry: ['src/index.cts'], - }, -]) diff --git a/packages/tailwindcss/README.md b/packages/tailwindcss/README.md index 5f532607d..95ec9d87d 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,10 +27,14 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ## Community -For help, discussion about best practices, or feature ideas: +For help, discussion about best practices, or any other conversation that would benefit from being searchable: -[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions) +[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) ## Contributing -If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index d1288d6fa..3f7d9cda5 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -1,6 +1,6 @@ { "name": "tailwindcss", - "version": "4.3.3", + "version": "4.0.0-alpha.26", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { @@ -31,14 +31,6 @@ "require": "./src/compat/colors.cts", "import": "./src/compat/colors.ts" }, - "./lib/util/flattenColorPalette": { - "require": "./src/compat/flatten-color-palette.cts", - "import": "./src/compat/flatten-color-palette.ts" - }, - "./lib/util/flattenColorPalette.js": { - "require": "./src/compat/flatten-color-palette.cts", - "import": "./src/compat/flatten-color-palette.ts" - }, "./defaultTheme": { "require": "./src/compat/default-theme.cts", "import": "./src/compat/default-theme.ts" @@ -99,14 +91,6 @@ "require": "./dist/colors.js", "import": "./dist/colors.mjs" }, - "./lib/util/flattenColorPalette": { - "require": "./dist/flatten-color-palette.js", - "import": "./dist/flatten-color-palette.mjs" - }, - "./lib/util/flattenColorPalette.js": { - "require": "./dist/flatten-color-palette.js", - "import": "./dist/flatten-color-palette.mjs" - }, "./package.json": "./package.json", "./index.css": "./index.css", "./index": "./index.css", @@ -127,12 +111,9 @@ "utilities.css" ], "devDependencies": { - "@jridgewell/remapping": "^2.3.5", "@tailwindcss/oxide": "workspace:^", "@types/node": "catalog:", - "dedent": "catalog:", "lightningcss": "catalog:", - "magic-string": "^1.1.0", - "source-map-js": "^1.2.1" + "dedent": "1.5.3" } } diff --git a/packages/tailwindcss/playwright.config.ts b/packages/tailwindcss/playwright.config.ts index ba8f95d70..c0a123abf 100644 --- a/packages/tailwindcss/playwright.config.ts +++ b/packages/tailwindcss/playwright.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [['html', { open: 'never' }]], + reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ @@ -36,11 +36,10 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - // Playwright 1.60's Windows WebKit worker hangs on shutdown after all tests - // pass, causing CI to fail with a worker force-kill error. - ...(process.platform === 'win32' - ? [] - : [{ name: 'webkit', use: { ...devices['Desktop Safari'] } }]), + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, { name: 'firefox', use: { diff --git a/packages/tailwindcss/preflight.css b/packages/tailwindcss/preflight.css index 04e87905c..169f646ba 100644 --- a/packages/tailwindcss/preflight.css +++ b/packages/tailwindcss/preflight.css @@ -30,26 +30,29 @@ html, line-height: 1.5; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ tab-size: 4; /* 3 */ - font-family: --theme( + font-family: var( --default-font-family, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - Roboto, - 'Helvetica Neue', - 'Noto Sans', - Arial, + ui-sans-serif, + system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' ); /* 4 */ - font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */ - font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */ + font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */ + font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */ -webkit-tap-highlight-color: transparent; /* 7 */ } +/* + Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + line-height: inherit; +} + /* 1. Add the correct height in Firefox. 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) @@ -115,7 +118,7 @@ code, kbd, samp, pre { - font-family: --theme( + font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, @@ -125,9 +128,9 @@ pre { 'Liberation Mono', 'Courier New', monospace - ); /* 1 */ - font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */ - font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */ + ); /* 4 */ + font-feature-settings: var(--default-mono-font-feature-settings, normal); /* 5 */ + font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 6 */ font-size: 1em; /* 4 */ } @@ -171,14 +174,60 @@ table { border-collapse: collapse; /* 3 */ } +/* + 1. Inherit the font styles in all browsers. + 2. Remove the default background color. +*/ + +button, +input, +optgroup, +select, +textarea, +::file-selector-button { + font: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + letter-spacing: inherit; /* 1 */ + color: inherit; /* 1 */ + background: transparent; /* 2 */ +} + +/* + Reset the default inset border style for form controls to solid. +*/ + +input:where(:not([type='button'], [type='reset'], [type='submit'])), +select, +textarea { + border: 1px solid; +} + +/* + Correct the inability to style the border radius in iOS Safari. +*/ +button, +input:where([type='button'], [type='reset'], [type='submit']), +::file-selector-button { + appearance: button; +} + /* Use the modern Firefox focus style for all focusable elements. */ -:-moz-focusring:where(:not(iframe)) { +:-moz-focusring { outline: auto; } +/* + Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + /* Add the correct vertical alignment in Chrome and Firefox. */ @@ -187,6 +236,23 @@ progress { vertical-align: baseline; } +/* + Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* + Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + /* Add the correct display in Chrome and Safari. */ @@ -205,10 +271,28 @@ menu { list-style: none; } +/* + Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* + 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + 2. Set the default placeholder color to a semi-transparent version of the current text color. +*/ + +::placeholder { + opacity: 1; /* 1 */ + color: color-mix(in srgb, currentColor 50%, transparent); /* 2 */ +} + /* 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. + This can trigger a poorly considered lint error in some tools but is included by design. */ img, @@ -233,162 +317,6 @@ video { height: auto; } -/* - 1. Inherit font styles in all browsers. - 2. Remove border radius in all browsers. - 3. Remove background color in all browsers. - 4. Ensure consistent opacity for disabled states in all browsers. -*/ - -button, -input, -select, -optgroup, -textarea, -::file-selector-button { - font: inherit; /* 1 */ - font-feature-settings: inherit; /* 1 */ - font-variation-settings: inherit; /* 1 */ - letter-spacing: inherit; /* 1 */ - color: inherit; /* 1 */ - border-radius: 0; /* 2 */ - background-color: transparent; /* 3 */ - opacity: 1; /* 4 */ -} - -/* - Restore default font weight. -*/ - -:where(select:is([multiple], [size])) optgroup { - font-weight: bolder; -} - -/* - Restore indentation. -*/ - -:where(select:is([multiple], [size])) optgroup option { - padding-inline-start: 20px; -} - -/* - Restore space after button. -*/ - -::file-selector-button { - margin-inline-end: 4px; -} - -/* - Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -*/ - -::placeholder { - opacity: 1; -} - -/* - Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not - crash when using `color-mix(…)` with `currentcolor`. (https://github.com/tailwindlabs/tailwindcss/issues/17194) -*/ - -@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or - (contain-intrinsic-size: 1px) /* Safari 17+ */ { - ::placeholder { - color: color-mix(in oklab, currentcolor 50%, transparent); - } -} - -/* - Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* - Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* - 1. Ensure date/time inputs have the same height when empty in iOS Safari. - 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. -*/ - -::-webkit-date-and-time-value { - min-height: 1lh; /* 1 */ - text-align: inherit; /* 2 */ -} - -/* - Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`. -*/ - -::-webkit-datetime-edit { - display: inline-flex; -} - -/* - Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. -*/ - -::-webkit-datetime-edit-fields-wrapper { - padding: 0; -} - -::-webkit-datetime-edit, -::-webkit-datetime-edit-year-field, -::-webkit-datetime-edit-month-field, -::-webkit-datetime-edit-day-field, -::-webkit-datetime-edit-hour-field, -::-webkit-datetime-edit-minute-field, -::-webkit-datetime-edit-second-field, -::-webkit-datetime-edit-millisecond-field, -::-webkit-datetime-edit-meridiem-field { - 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) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* - Correct the inability to style the border radius in iOS Safari. -*/ - -button, -input:where([type='button'], [type='reset'], [type='submit']), -::file-selector-button { - appearance: button; -} - -/* - Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - /* Make elements with the HTML hidden attribute stay hidden by default. */ diff --git a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap index 9921709d7..a29cb7740 100644 --- a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap @@ -1,16 +1,436 @@ // 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 { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { +":root { + --default-transition-duration: .15s; + --default-transition-timing-function: cubic-bezier(.4, 0, .2, 1); + --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); + --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); + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + --color-black: #000; + --color-white: #fff; + --color-slate-50: #f8fafc; + --color-slate-100: #f1f5f9; + --color-slate-200: #e2e8f0; + --color-slate-300: #cbd5e1; + --color-slate-400: #94a3b8; + --color-slate-500: #64748b; + --color-slate-600: #475569; + --color-slate-700: #334155; + --color-slate-800: #1e293b; + --color-slate-900: #0f172a; + --color-slate-950: #020617; + --color-gray-50: #f9fafb; + --color-gray-100: #f3f4f6; + --color-gray-200: #e5e7eb; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; + --color-gray-500: #6b7280; + --color-gray-600: #4b5563; + --color-gray-700: #374151; + --color-gray-800: #1f2937; + --color-gray-900: #111827; + --color-gray-950: #030712; + --color-zinc-50: #fafafa; + --color-zinc-100: #f4f4f5; + --color-zinc-200: #e4e4e7; + --color-zinc-300: #d4d4d8; + --color-zinc-400: #a1a1aa; + --color-zinc-500: #71717a; + --color-zinc-600: #52525b; + --color-zinc-700: #3f3f46; + --color-zinc-800: #27272a; + --color-zinc-900: #18181b; + --color-zinc-950: #09090b; + --color-neutral-50: #fafafa; + --color-neutral-100: #f5f5f5; + --color-neutral-200: #e5e5e5; + --color-neutral-300: #d4d4d4; + --color-neutral-400: #a3a3a3; + --color-neutral-500: #737373; + --color-neutral-600: #525252; + --color-neutral-700: #404040; + --color-neutral-800: #262626; + --color-neutral-900: #171717; + --color-neutral-950: #0a0a0a; + --color-stone-50: #fafaf9; + --color-stone-100: #f5f5f4; + --color-stone-200: #e7e5e4; + --color-stone-300: #d6d3d1; + --color-stone-400: #a8a29e; + --color-stone-500: #78716c; + --color-stone-600: #57534e; + --color-stone-700: #44403c; + --color-stone-800: #292524; + --color-stone-900: #1c1917; + --color-stone-950: #0c0a09; + --color-red-50: #fef2f2; + --color-red-100: #fee2e2; + --color-red-200: #fecaca; + --color-red-300: #fca5a5; + --color-red-400: #f87171; + --color-red-500: #ef4444; + --color-red-600: #dc2626; + --color-red-700: #b91c1c; + --color-red-800: #991b1b; + --color-red-900: #7f1d1d; + --color-red-950: #450a0a; + --color-orange-50: #fff7ed; + --color-orange-100: #ffedd5; + --color-orange-200: #fed7aa; + --color-orange-300: #fdba74; + --color-orange-400: #fb923c; + --color-orange-500: #f97316; + --color-orange-600: #ea580c; + --color-orange-700: #c2410c; + --color-orange-800: #9a3412; + --color-orange-900: #7c2d12; + --color-orange-950: #431407; + --color-amber-50: #fffbeb; + --color-amber-100: #fef3c7; + --color-amber-200: #fde68a; + --color-amber-300: #fcd34d; + --color-amber-400: #fbbf24; + --color-amber-500: #f59e0b; + --color-amber-600: #d97706; + --color-amber-700: #b45309; + --color-amber-800: #92400e; + --color-amber-900: #78350f; + --color-amber-950: #451a03; + --color-yellow-50: #fefce8; + --color-yellow-100: #fef9c3; + --color-yellow-200: #fef08a; + --color-yellow-300: #fde047; + --color-yellow-400: #facc15; + --color-yellow-500: #eab308; + --color-yellow-600: #ca8a04; + --color-yellow-700: #a16207; + --color-yellow-800: #854d0e; + --color-yellow-900: #713f12; + --color-yellow-950: #422006; + --color-lime-50: #f7fee7; + --color-lime-100: #ecfccb; + --color-lime-200: #d9f99d; + --color-lime-300: #bef264; + --color-lime-400: #a3e635; + --color-lime-500: #84cc16; + --color-lime-600: #65a30d; + --color-lime-700: #4d7c0f; + --color-lime-800: #3f6212; + --color-lime-900: #365314; + --color-lime-950: #1a2e05; + --color-green-50: #f0fdf4; + --color-green-100: #dcfce7; + --color-green-200: #bbf7d0; + --color-green-300: #86efac; + --color-green-400: #4ade80; + --color-green-500: #22c55e; + --color-green-600: #16a34a; + --color-green-700: #15803d; + --color-green-800: #166534; + --color-green-900: #14532d; + --color-green-950: #052e16; + --color-emerald-50: #ecfdf5; + --color-emerald-100: #d1fae5; + --color-emerald-200: #a7f3d0; + --color-emerald-300: #6ee7b7; + --color-emerald-400: #34d399; + --color-emerald-500: #10b981; + --color-emerald-600: #059669; + --color-emerald-700: #047857; + --color-emerald-800: #065f46; + --color-emerald-900: #064e3b; + --color-emerald-950: #022c22; + --color-teal-50: #f0fdfa; + --color-teal-100: #ccfbf1; + --color-teal-200: #99f6e4; + --color-teal-300: #5eead4; + --color-teal-400: #2dd4bf; + --color-teal-500: #14b8a6; + --color-teal-600: #0d9488; + --color-teal-700: #0f766e; + --color-teal-800: #115e59; + --color-teal-900: #134e4a; + --color-teal-950: #042f2e; + --color-cyan-50: #ecfeff; + --color-cyan-100: #cffafe; + --color-cyan-200: #a5f3fc; + --color-cyan-300: #67e8f9; + --color-cyan-400: #22d3ee; + --color-cyan-500: #06b6d4; + --color-cyan-600: #0891b2; + --color-cyan-700: #0e7490; + --color-cyan-800: #155e75; + --color-cyan-900: #164e63; + --color-cyan-950: #083344; + --color-sky-50: #f0f9ff; + --color-sky-100: #e0f2fe; + --color-sky-200: #bae6fd; + --color-sky-300: #7dd3fc; + --color-sky-400: #38bdf8; + --color-sky-500: #0ea5e9; + --color-sky-600: #0284c7; + --color-sky-700: #0369a1; + --color-sky-800: #075985; + --color-sky-900: #0c4a6e; + --color-sky-950: #082f49; + --color-blue-50: #eff6ff; + --color-blue-100: #dbeafe; + --color-blue-200: #bfdbfe; + --color-blue-300: #93c5fd; + --color-blue-400: #60a5fa; + --color-blue-500: #3b82f6; + --color-blue-600: #2563eb; + --color-blue-700: #1d4ed8; + --color-blue-800: #1e40af; + --color-blue-900: #1e3a8a; + --color-blue-950: #172554; + --color-indigo-50: #eef2ff; + --color-indigo-100: #e0e7ff; + --color-indigo-200: #c7d2fe; + --color-indigo-300: #a5b4fc; + --color-indigo-400: #818cf8; + --color-indigo-500: #6366f1; + --color-indigo-600: #4f46e5; + --color-indigo-700: #4338ca; + --color-indigo-800: #3730a3; + --color-indigo-900: #312e81; + --color-indigo-950: #1e1b4b; + --color-violet-50: #f5f3ff; + --color-violet-100: #ede9fe; + --color-violet-200: #ddd6fe; + --color-violet-300: #c4b5fd; + --color-violet-400: #a78bfa; + --color-violet-500: #8b5cf6; + --color-violet-600: #7c3aed; + --color-violet-700: #6d28d9; + --color-violet-800: #5b21b6; + --color-violet-900: #4c1d95; + --color-violet-950: #2e1065; + --color-purple-50: #faf5ff; + --color-purple-100: #f3e8ff; + --color-purple-200: #e9d5ff; + --color-purple-300: #d8b4fe; + --color-purple-400: #c084fc; + --color-purple-500: #a855f7; + --color-purple-600: #9333ea; + --color-purple-700: #7e22ce; + --color-purple-800: #6b21a8; + --color-purple-900: #581c87; + --color-purple-950: #3b0764; + --color-fuchsia-50: #fdf4ff; + --color-fuchsia-100: #fae8ff; + --color-fuchsia-200: #f5d0fe; + --color-fuchsia-300: #f0abfc; + --color-fuchsia-400: #e879f9; + --color-fuchsia-500: #d946ef; + --color-fuchsia-600: #c026d3; + --color-fuchsia-700: #a21caf; + --color-fuchsia-800: #86198f; + --color-fuchsia-900: #701a75; + --color-fuchsia-950: #4a044e; + --color-pink-50: #fdf2f8; + --color-pink-100: #fce7f3; + --color-pink-200: #fbcfe8; + --color-pink-300: #f9a8d4; + --color-pink-400: #f472b6; + --color-pink-500: #ec4899; + --color-pink-600: #db2777; + --color-pink-700: #be185d; + --color-pink-800: #9d174d; + --color-pink-900: #831843; + --color-pink-950: #500724; + --color-rose-50: #fff1f2; + --color-rose-100: #ffe4e6; + --color-rose-200: #fecdd3; + --color-rose-300: #fda4af; + --color-rose-400: #fb7185; + --color-rose-500: #f43f5e; + --color-rose-600: #e11d48; + --color-rose-700: #be123c; + --color-rose-800: #9f1239; + --color-rose-900: #881337; + --color-rose-950: #4c0519; + --animate-spin: spin 1s linear infinite; + --animate-ping: ping 1s cubic-bezier(0, 0, .2, 1) infinite; + --animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite; + --animate-bounce: bounce 1s infinite; + --blur: 8px; + --blur-sm: 4px; + --blur-md: 12px; + --blur-lg: 16px; + --blur-xl: 24px; + --blur-2xl: 40px; + --blur-3xl: 64px; + --radius: .25rem; + --radius-sm: .125rem; + --radius-md: .375rem; + --radius-lg: .5rem; + --radius-xl: .75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --radius-4xl: 2rem; + --shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a; + --shadow-xs: 0 1px #0000000d; + --shadow-sm: 0 1px 2px 0 #0000000d; + --shadow-md: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a; + --shadow-lg: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a; + --shadow-xl: 0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a; + --shadow-2xl: 0 25px 50px -12px #00000040; + --shadow-inner: inset 0 2px 4px 0 #0000000d; + --inset-shadow-xs: inset 0 1px #0000000d; + --inset-shadow-sm: inset 0 1px 1px #0000000d; + --inset-shadow: inset 0 2px 4px #0000000d; + --drop-shadow: 0 1px 2px #0000001a, 0 1px 1px #0000000f; + --drop-shadow-sm: 0 1px 1px #0000000d; + --drop-shadow-md: 0 4px 3px #00000012, 0 2px 2px #0000000f; + --drop-shadow-lg: 0 10px 8px #0000000a, 0 4px 3px #0000001a; + --drop-shadow-xl: 0 20px 13px #00000008, 0 8px 5px #00000014; + --drop-shadow-2xl: 0 25px 25px #00000026; + --drop-shadow-none: 0 0 #0000; + --spacing-px: 1px; + --spacing-0: 0px; + --spacing-0_5: .125rem; + --spacing-1: .25rem; + --spacing-1_5: .375rem; + --spacing-2: .5rem; + --spacing-2_5: .625rem; + --spacing-3: .75rem; + --spacing-3_5: .875rem; + --spacing-4: 1rem; + --spacing-5: 1.25rem; + --spacing-6: 1.5rem; + --spacing-7: 1.75rem; + --spacing-8: 2rem; + --spacing-9: 2.25rem; + --spacing-10: 2.5rem; + --spacing-11: 2.75rem; + --spacing-12: 3rem; + --spacing-14: 3.5rem; + --spacing-16: 4rem; + --spacing-20: 5rem; + --spacing-24: 6rem; + --spacing-28: 7rem; + --spacing-32: 8rem; + --spacing-36: 9rem; + --spacing-40: 10rem; + --spacing-44: 11rem; + --spacing-48: 12rem; + --spacing-52: 13rem; + --spacing-56: 14rem; + --spacing-60: 15rem; + --spacing-64: 16rem; + --spacing-72: 18rem; + --spacing-80: 20rem; + --spacing-96: 24rem; + --width-3xs: 16rem; + --width-2xs: 18rem; + --width-xs: 20rem; + --width-sm: 24rem; + --width-md: 28rem; + --width-lg: 32rem; + --width-xl: 36rem; + --width-2xl: 42rem; + --width-3xl: 48rem; + --width-4xl: 56rem; + --width-5xl: 64rem; + --width-6xl: 72rem; + --width-7xl: 80rem; + --width-prose: 65ch; + --font-family-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --font-size-xs: .75rem; + --font-size-xs--line-height: 1rem; + --font-size-sm: .875rem; + --font-size-sm--line-height: 1.25rem; + --font-size-base: 1rem; + --font-size-base--line-height: 1.5rem; + --font-size-lg: 1.125rem; + --font-size-lg--line-height: 1.75rem; + --font-size-xl: 1.25rem; + --font-size-xl--line-height: 1.75rem; + --font-size-2xl: 1.5rem; + --font-size-2xl--line-height: 2rem; + --font-size-3xl: 1.875rem; + --font-size-3xl--line-height: 2.25rem; + --font-size-4xl: 2.25rem; + --font-size-4xl--line-height: 2.5rem; + --font-size-5xl: 3rem; + --font-size-5xl--line-height: 1; + --font-size-6xl: 3.75rem; + --font-size-6xl--line-height: 1; + --font-size-7xl: 4.5rem; + --font-size-7xl--line-height: 1; + --font-size-8xl: 6rem; + --font-size-8xl--line-height: 1; + --font-size-9xl: 8rem; + --font-size-9xl--line-height: 1; + --letter-spacing-tighter: -.05em; + --letter-spacing-tight: -.025em; + --letter-spacing-normal: 0em; + --letter-spacing-wide: .025em; + --letter-spacing-wider: .05em; + --letter-spacing-widest: .1em; + --line-height-none: 1; + --line-height-tight: 1.25; + --line-height-snug: 1.375; + --line-height-normal: 1.5; + --line-height-relaxed: 1.625; + --line-height-loose: 2; + --line-height-3: .75rem; + --line-height-4: 1rem; + --line-height-5: 1.25rem; + --line-height-6: 1.5rem; + --line-height-7: 1.75rem; + --line-height-8: 2rem; + --line-height-9: 2.25rem; + --line-height-10: 2.5rem; + --perspective-dramatic: 100px; + --perspective-near: 300px; + --perspective-normal: 500px; + --perspective-midrange: 800px; + --perspective-distant: 1200px; + --transition-timing-function-linear: linear; + --transition-timing-function-in: cubic-bezier(.4, 0, 1, 1); + --transition-timing-function-out: cubic-bezier(0, 0, .2, 1); + --transition-timing-function-in-out: cubic-bezier(.4, 0, .2, 1); +} + +.w-4 { + width: var(--spacing-4, 1rem); +} + +.bg-red-500 { + background-color: var(--color-red-500, #ef4444); +} + +.shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, #0000001a), 0 1px 2px -1px var(--tw-shadow-color, #0000001a); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +@media (width >= 40rem) { + .sm\\:flex { + display: flex; + } +} + +@supports (-moz-orient: inline) { + @layer base { *, :before, :after, ::backdrop { --tw-shadow: 0 0 #0000; --tw-shadow-color: initial; - --tw-shadow-alpha: 100%; --tw-inset-shadow: 0 0 #0000; --tw-inset-shadow-color: initial; - --tw-inset-shadow-alpha: 100%; --tw-ring-color: initial; --tw-ring-shadow: 0 0 #0000; --tw-inset-ring-color: initial; @@ -23,27 +443,34 @@ exports[`compiling CSS > \`@tailwind utilities\` is replaced by utilities using } } -:root, :host { - --color-red-500: oklch(63.7% .237 25.331); - --spacing: .25rem; +@keyframes spin { + to { + transform: rotate(360deg); + } } -.w-4 { - width: calc(var(--spacing) * 4); +@keyframes ping { + 75%, 100% { + opacity: 0; + transform: scale(2); + } } -.bg-red-500 { - background-color: var(--color-red-500); +@keyframes pulse { + 50% { + opacity: .5; + } } -.shadow-sm { - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, #0000001a), 0 1px 2px -1px var(--tw-shadow-color, #0000001a); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); -} +@keyframes bounce { + 0%, 100% { + animation-timing-function: cubic-bezier(.8, 0, 1, 1); + transform: translateY(-25%); + } -@media (min-width: 40rem) { - .sm\\:flex { - display: flex; + 50% { + animation-timing-function: cubic-bezier(0, 0, .2, 1); + transform: none; } } @@ -58,12 +485,6 @@ exports[`compiling CSS > \`@tailwind utilities\` is replaced by utilities using inherits: false } -@property --tw-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} - @property --tw-inset-shadow { syntax: "*"; inherits: false; @@ -75,12 +496,6 @@ exports[`compiling CSS > \`@tailwind utilities\` is replaced by utilities using inherits: false } -@property --tw-inset-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} - @property --tw-ring-color { syntax: "*"; inherits: false @@ -124,270 +539,5 @@ 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 { - :root, :host { - --tw-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, - sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --tw-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", - monospace; - --tw-default-font-family: var(--tw-font-sans); - --tw-default-mono-font-family: var(--tw-font-mono); - } -} - -@layer base { - *, :after, :before, ::backdrop { - box-sizing: border-box; - border: 0 solid; - margin: 0; - padding: 0; - } - - ::file-selector-button { - box-sizing: border-box; - border: 0 solid; - margin: 0; - padding: 0; - } - - html, :host { - -webkit-text-size-adjust: 100%; - tab-size: 4; - line-height: 1.5; - font-family: var(--tw-default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); - font-feature-settings: var(--tw-default-font-feature-settings, normal); - font-variation-settings: var(--tw-default-font-variation-settings, normal); - -webkit-tap-highlight-color: transparent; - } - - hr { - height: 0; - color: inherit; - border-top-width: 1px; - } - - abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; - } - - h1, h2, h3, h4, h5, h6 { - font-size: inherit; - font-weight: inherit; - } - - a { - color: inherit; - -webkit-text-decoration: inherit; - -webkit-text-decoration: inherit; - -webkit-text-decoration: inherit; - text-decoration: inherit; - } - - b, strong { - font-weight: bolder; - } - - code, kbd, samp, pre { - font-family: var(--tw-default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); - font-feature-settings: var(--tw-default-mono-font-feature-settings, normal); - font-variation-settings: var(--tw-default-mono-font-variation-settings, normal); - font-size: 1em; - } - - small { - font-size: 80%; - } - - sub, sup { - vertical-align: baseline; - font-size: 75%; - line-height: 0; - position: relative; - } - - sub { - bottom: -.25em; - } - - sup { - top: -.5em; - } - - table { - text-indent: 0; - border-color: inherit; - border-collapse: collapse; - } - - :-moz-focusring:where(:not(iframe)) { - outline: auto; - } - - progress { - vertical-align: baseline; - } - - summary { - display: list-item; - } - - ol, ul, menu { - list-style: none; - } - - img, svg, video, canvas, audio, iframe, embed, object { - vertical-align: middle; - display: block; - } - - img, video { - max-width: 100%; - height: auto; - } - - button, input, select, optgroup, textarea { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - opacity: 1; - background-color: #0000; - border-radius: 0; - } - - ::file-selector-button { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - opacity: 1; - background-color: #0000; - border-radius: 0; - } - - :where(select:is([multiple], [size])) optgroup { - font-weight: bolder; - } - - :where(select:is([multiple], [size])) optgroup option { - padding-inline-start: 20px; - } - - ::file-selector-button { - margin-inline-end: 4px; - } - - ::placeholder { - opacity: 1; - } - - @supports (not ((-webkit-appearance: -apple-pay-button))) or (contain-intrinsic-size: 1px) { - ::placeholder { - color: currentColor; - } - - @supports (color: color-mix(in lab, red, red)) { - ::placeholder { - color: color-mix(in oklab, currentcolor 50%, transparent); - } - } - } - - textarea { - resize: vertical; - } - - ::-webkit-search-decoration { - -webkit-appearance: none; - } - - ::-webkit-date-and-time-value { - min-height: 1lh; - text-align: inherit; - } - - ::-webkit-datetime-edit { - display: inline-flex; - } - - ::-webkit-datetime-edit-fields-wrapper { - padding: 0; - } - - ::-webkit-datetime-edit { - padding-block: 0; - } - - ::-webkit-datetime-edit-year-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-month-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-day-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-hour-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-minute-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-second-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-millisecond-field { - padding-block: 0; - } - - ::-webkit-datetime-edit-meridiem-field { - padding-block: 0; - } - - ::-webkit-calendar-picker-indicator { - line-height: 1; - } - - :-moz-ui-invalid { - box-shadow: none; - } - - button, input:where([type="button"], [type="reset"], [type="submit"]) { - appearance: button; - } - - ::file-selector-button { - appearance: button; - } - - ::-webkit-inner-spin-button { - height: auto; - } - - ::-webkit-outer-spin-button { - height: auto; - } - - [hidden]:where(:not([hidden="until-found"])) { - display: none !important; - } -} - -@layer components, utilities; -" +}" `; diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index d595e8c01..75232242c 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -2,285 +2,15 @@ exports[`getClassList 1`] = ` [ - "-bg-conic-0", - "-bg-conic-0/oklab", - "-bg-conic-0/oklch", - "-bg-conic-0/srgb", - "-bg-conic-0/hsl", - "-bg-conic-0/longer", - "-bg-conic-0/shorter", - "-bg-conic-0/increasing", - "-bg-conic-0/decreasing", - "-bg-conic-30", - "-bg-conic-30/oklab", - "-bg-conic-30/oklch", - "-bg-conic-30/srgb", - "-bg-conic-30/hsl", - "-bg-conic-30/longer", - "-bg-conic-30/shorter", - "-bg-conic-30/increasing", - "-bg-conic-30/decreasing", - "-bg-conic-60", - "-bg-conic-60/oklab", - "-bg-conic-60/oklch", - "-bg-conic-60/srgb", - "-bg-conic-60/hsl", - "-bg-conic-60/longer", - "-bg-conic-60/shorter", - "-bg-conic-60/increasing", - "-bg-conic-60/decreasing", - "-bg-conic-90", - "-bg-conic-90/oklab", - "-bg-conic-90/oklch", - "-bg-conic-90/srgb", - "-bg-conic-90/hsl", - "-bg-conic-90/longer", - "-bg-conic-90/shorter", - "-bg-conic-90/increasing", - "-bg-conic-90/decreasing", - "-bg-conic-120", - "-bg-conic-120/oklab", - "-bg-conic-120/oklch", - "-bg-conic-120/srgb", - "-bg-conic-120/hsl", - "-bg-conic-120/longer", - "-bg-conic-120/shorter", - "-bg-conic-120/increasing", - "-bg-conic-120/decreasing", - "-bg-conic-150", - "-bg-conic-150/oklab", - "-bg-conic-150/oklch", - "-bg-conic-150/srgb", - "-bg-conic-150/hsl", - "-bg-conic-150/longer", - "-bg-conic-150/shorter", - "-bg-conic-150/increasing", - "-bg-conic-150/decreasing", - "-bg-conic-180", - "-bg-conic-180/oklab", - "-bg-conic-180/oklch", - "-bg-conic-180/srgb", - "-bg-conic-180/hsl", - "-bg-conic-180/longer", - "-bg-conic-180/shorter", - "-bg-conic-180/increasing", - "-bg-conic-180/decreasing", - "-bg-conic-210", - "-bg-conic-210/oklab", - "-bg-conic-210/oklch", - "-bg-conic-210/srgb", - "-bg-conic-210/hsl", - "-bg-conic-210/longer", - "-bg-conic-210/shorter", - "-bg-conic-210/increasing", - "-bg-conic-210/decreasing", - "-bg-conic-240", - "-bg-conic-240/oklab", - "-bg-conic-240/oklch", - "-bg-conic-240/srgb", - "-bg-conic-240/hsl", - "-bg-conic-240/longer", - "-bg-conic-240/shorter", - "-bg-conic-240/increasing", - "-bg-conic-240/decreasing", - "-bg-conic-270", - "-bg-conic-270/oklab", - "-bg-conic-270/oklch", - "-bg-conic-270/srgb", - "-bg-conic-270/hsl", - "-bg-conic-270/longer", - "-bg-conic-270/shorter", - "-bg-conic-270/increasing", - "-bg-conic-270/decreasing", - "-bg-conic-300", - "-bg-conic-300/oklab", - "-bg-conic-300/oklch", - "-bg-conic-300/srgb", - "-bg-conic-300/hsl", - "-bg-conic-300/longer", - "-bg-conic-300/shorter", - "-bg-conic-300/increasing", - "-bg-conic-300/decreasing", - "-bg-conic-330", - "-bg-conic-330/oklab", - "-bg-conic-330/oklch", - "-bg-conic-330/srgb", - "-bg-conic-330/hsl", - "-bg-conic-330/longer", - "-bg-conic-330/shorter", - "-bg-conic-330/increasing", - "-bg-conic-330/decreasing", - "-bg-linear-0", - "-bg-linear-0/oklab", - "-bg-linear-0/oklch", - "-bg-linear-0/srgb", - "-bg-linear-0/hsl", - "-bg-linear-0/longer", - "-bg-linear-0/shorter", - "-bg-linear-0/increasing", - "-bg-linear-0/decreasing", - "-bg-linear-30", - "-bg-linear-30/oklab", - "-bg-linear-30/oklch", - "-bg-linear-30/srgb", - "-bg-linear-30/hsl", - "-bg-linear-30/longer", - "-bg-linear-30/shorter", - "-bg-linear-30/increasing", - "-bg-linear-30/decreasing", - "-bg-linear-60", - "-bg-linear-60/oklab", - "-bg-linear-60/oklch", - "-bg-linear-60/srgb", - "-bg-linear-60/hsl", - "-bg-linear-60/longer", - "-bg-linear-60/shorter", - "-bg-linear-60/increasing", - "-bg-linear-60/decreasing", - "-bg-linear-90", - "-bg-linear-90/oklab", - "-bg-linear-90/oklch", - "-bg-linear-90/srgb", - "-bg-linear-90/hsl", - "-bg-linear-90/longer", - "-bg-linear-90/shorter", - "-bg-linear-90/increasing", - "-bg-linear-90/decreasing", - "-bg-linear-120", - "-bg-linear-120/oklab", - "-bg-linear-120/oklch", - "-bg-linear-120/srgb", - "-bg-linear-120/hsl", - "-bg-linear-120/longer", - "-bg-linear-120/shorter", - "-bg-linear-120/increasing", - "-bg-linear-120/decreasing", - "-bg-linear-150", - "-bg-linear-150/oklab", - "-bg-linear-150/oklch", - "-bg-linear-150/srgb", - "-bg-linear-150/hsl", - "-bg-linear-150/longer", - "-bg-linear-150/shorter", - "-bg-linear-150/increasing", - "-bg-linear-150/decreasing", - "-bg-linear-180", - "-bg-linear-180/oklab", - "-bg-linear-180/oklch", - "-bg-linear-180/srgb", - "-bg-linear-180/hsl", - "-bg-linear-180/longer", - "-bg-linear-180/shorter", - "-bg-linear-180/increasing", - "-bg-linear-180/decreasing", - "-bg-linear-210", - "-bg-linear-210/oklab", - "-bg-linear-210/oklch", - "-bg-linear-210/srgb", - "-bg-linear-210/hsl", - "-bg-linear-210/longer", - "-bg-linear-210/shorter", - "-bg-linear-210/increasing", - "-bg-linear-210/decreasing", - "-bg-linear-240", - "-bg-linear-240/oklab", - "-bg-linear-240/oklch", - "-bg-linear-240/srgb", - "-bg-linear-240/hsl", - "-bg-linear-240/longer", - "-bg-linear-240/shorter", - "-bg-linear-240/increasing", - "-bg-linear-240/decreasing", - "-bg-linear-270", - "-bg-linear-270/oklab", - "-bg-linear-270/oklch", - "-bg-linear-270/srgb", - "-bg-linear-270/hsl", - "-bg-linear-270/longer", - "-bg-linear-270/shorter", - "-bg-linear-270/increasing", - "-bg-linear-270/decreasing", - "-bg-linear-300", - "-bg-linear-300/oklab", - "-bg-linear-300/oklch", - "-bg-linear-300/srgb", - "-bg-linear-300/hsl", - "-bg-linear-300/longer", - "-bg-linear-300/shorter", - "-bg-linear-300/increasing", - "-bg-linear-300/decreasing", - "-bg-linear-330", - "-bg-linear-330/oklab", - "-bg-linear-330/oklch", - "-bg-linear-330/srgb", - "-bg-linear-330/hsl", - "-bg-linear-330/longer", - "-bg-linear-330/shorter", - "-bg-linear-330/increasing", - "-bg-linear-330/decreasing", - "-bottom-0", "-bottom-0.5", "-bottom-1", - "-bottom-1.5", - "-bottom-2", - "-bottom-2.5", "-bottom-3", - "-bottom-3.5", "-bottom-4", - "-bottom-5", - "-bottom-6", - "-bottom-7", - "-bottom-8", - "-bottom-9", - "-bottom-10", - "-bottom-11", - "-bottom-12", - "-bottom-14", - "-bottom-16", - "-bottom-20", - "-bottom-24", - "-bottom-28", - "-bottom-32", - "-bottom-36", - "-bottom-40", - "-bottom-44", - "-bottom-48", - "-bottom-52", - "-bottom-56", - "-bottom-60", - "-bottom-64", - "-bottom-72", - "-bottom-80", - "-bottom-96", - "-bottom-1/2", - "-bottom-1/3", - "-bottom-1/4", - "-bottom-1/5", - "-bottom-1/6", - "-bottom-1/12", - "-bottom-2/3", - "-bottom-2/4", - "-bottom-2/5", - "-bottom-2/6", - "-bottom-2/12", - "-bottom-3/4", - "-bottom-3/5", - "-bottom-3/6", - "-bottom-3/12", - "-bottom-4/5", - "-bottom-4/6", - "-bottom-4/12", - "-bottom-5/6", - "-bottom-5/12", - "-bottom-6/12", - "-bottom-7/12", - "-bottom-8/12", - "-bottom-9/12", - "-bottom-10/12", - "-bottom-11/12", - "-bottom-full", - "-bottom-px", "-col-end-1", + "-col-end-10", + "-col-end-11", + "-col-end-12", + "-col-end-13", "-col-end-2", "-col-end-3", "-col-end-4", @@ -289,11 +19,11 @@ exports[`getClassList 1`] = ` "-col-end-7", "-col-end-8", "-col-end-9", - "-col-end-10", - "-col-end-11", - "-col-end-12", - "-col-end-13", "-col-start-1", + "-col-start-10", + "-col-start-11", + "-col-start-12", + "-col-start-13", "-col-start-2", "-col-start-3", "-col-start-4", @@ -302,947 +32,70 @@ exports[`getClassList 1`] = ` "-col-start-7", "-col-start-8", "-col-start-9", - "-col-start-10", - "-col-start-11", - "-col-start-12", - "-col-start-13", - "-end-full", - "-end-px", - "-indent-0", + "-end-0.5", + "-end-1", + "-end-3", + "-end-4", "-indent-0.5", "-indent-1", - "-indent-1.5", - "-indent-2", - "-indent-2.5", "-indent-3", - "-indent-3.5", "-indent-4", - "-indent-5", - "-indent-6", - "-indent-7", - "-indent-8", - "-indent-9", - "-indent-10", - "-indent-11", - "-indent-12", - "-indent-14", - "-indent-16", - "-indent-20", - "-indent-24", - "-indent-28", - "-indent-32", - "-indent-36", - "-indent-40", - "-indent-44", - "-indent-48", - "-indent-52", - "-indent-56", - "-indent-60", - "-indent-64", - "-indent-72", - "-indent-80", - "-indent-96", - "-indent-px", - "-inset-0", "-inset-0.5", "-inset-1", - "-inset-1.5", - "-inset-2", - "-inset-2.5", "-inset-3", - "-inset-3.5", "-inset-4", - "-inset-5", - "-inset-6", - "-inset-7", - "-inset-8", - "-inset-9", - "-inset-10", - "-inset-11", - "-inset-12", - "-inset-14", - "-inset-16", - "-inset-20", - "-inset-24", - "-inset-28", - "-inset-32", - "-inset-36", - "-inset-40", - "-inset-44", - "-inset-48", - "-inset-52", - "-inset-56", - "-inset-60", - "-inset-64", - "-inset-72", - "-inset-80", - "-inset-96", - "-inset-1/2", - "-inset-1/3", - "-inset-1/4", - "-inset-1/5", - "-inset-1/6", - "-inset-1/12", - "-inset-2/3", - "-inset-2/4", - "-inset-2/5", - "-inset-2/6", - "-inset-2/12", - "-inset-3/4", - "-inset-3/5", - "-inset-3/6", - "-inset-3/12", - "-inset-4/5", - "-inset-4/6", - "-inset-4/12", - "-inset-5/6", - "-inset-5/12", - "-inset-6/12", - "-inset-7/12", - "-inset-8/12", - "-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", - "-inset-x-1.5", - "-inset-x-2", - "-inset-x-2.5", "-inset-x-3", - "-inset-x-3.5", "-inset-x-4", - "-inset-x-5", - "-inset-x-6", - "-inset-x-7", - "-inset-x-8", - "-inset-x-9", - "-inset-x-10", - "-inset-x-11", - "-inset-x-12", - "-inset-x-14", - "-inset-x-16", - "-inset-x-20", - "-inset-x-24", - "-inset-x-28", - "-inset-x-32", - "-inset-x-36", - "-inset-x-40", - "-inset-x-44", - "-inset-x-48", - "-inset-x-52", - "-inset-x-56", - "-inset-x-60", - "-inset-x-64", - "-inset-x-72", - "-inset-x-80", - "-inset-x-96", - "-inset-x-1/2", - "-inset-x-1/3", - "-inset-x-1/4", - "-inset-x-1/5", - "-inset-x-1/6", - "-inset-x-1/12", - "-inset-x-2/3", - "-inset-x-2/4", - "-inset-x-2/5", - "-inset-x-2/6", - "-inset-x-2/12", - "-inset-x-3/4", - "-inset-x-3/5", - "-inset-x-3/6", - "-inset-x-3/12", - "-inset-x-4/5", - "-inset-x-4/6", - "-inset-x-4/12", - "-inset-x-5/6", - "-inset-x-5/12", - "-inset-x-6/12", - "-inset-x-7/12", - "-inset-x-8/12", - "-inset-x-9/12", - "-inset-x-10/12", - "-inset-x-11/12", - "-inset-x-full", - "-inset-x-px", - "-inset-y-0", "-inset-y-0.5", "-inset-y-1", - "-inset-y-1.5", - "-inset-y-2", - "-inset-y-2.5", "-inset-y-3", - "-inset-y-3.5", "-inset-y-4", - "-inset-y-5", - "-inset-y-6", - "-inset-y-7", - "-inset-y-8", - "-inset-y-9", - "-inset-y-10", - "-inset-y-11", - "-inset-y-12", - "-inset-y-14", - "-inset-y-16", - "-inset-y-20", - "-inset-y-24", - "-inset-y-28", - "-inset-y-32", - "-inset-y-36", - "-inset-y-40", - "-inset-y-44", - "-inset-y-48", - "-inset-y-52", - "-inset-y-56", - "-inset-y-60", - "-inset-y-64", - "-inset-y-72", - "-inset-y-80", - "-inset-y-96", - "-inset-y-1/2", - "-inset-y-1/3", - "-inset-y-1/4", - "-inset-y-1/5", - "-inset-y-1/6", - "-inset-y-1/12", - "-inset-y-2/3", - "-inset-y-2/4", - "-inset-y-2/5", - "-inset-y-2/6", - "-inset-y-2/12", - "-inset-y-3/4", - "-inset-y-3/5", - "-inset-y-3/6", - "-inset-y-3/12", - "-inset-y-4/5", - "-inset-y-4/6", - "-inset-y-4/12", - "-inset-y-5/6", - "-inset-y-5/12", - "-inset-y-6/12", - "-inset-y-7/12", - "-inset-y-8/12", - "-inset-y-9/12", - "-inset-y-10/12", - "-inset-y-11/12", - "-inset-y-full", - "-inset-y-px", - "-left-0", "-left-0.5", "-left-1", - "-left-1.5", - "-left-2", - "-left-2.5", "-left-3", - "-left-3.5", "-left-4", - "-left-5", - "-left-6", - "-left-7", - "-left-8", - "-left-9", - "-left-10", - "-left-11", - "-left-12", - "-left-14", - "-left-16", - "-left-20", - "-left-24", - "-left-28", - "-left-32", - "-left-36", - "-left-40", - "-left-44", - "-left-48", - "-left-52", - "-left-56", - "-left-60", - "-left-64", - "-left-72", - "-left-80", - "-left-96", - "-left-1/2", - "-left-1/3", - "-left-1/4", - "-left-1/5", - "-left-1/6", - "-left-1/12", - "-left-2/3", - "-left-2/4", - "-left-2/5", - "-left-2/6", - "-left-2/12", - "-left-3/4", - "-left-3/5", - "-left-3/6", - "-left-3/12", - "-left-4/5", - "-left-4/6", - "-left-4/12", - "-left-5/6", - "-left-5/12", - "-left-6/12", - "-left-7/12", - "-left-8/12", - "-left-9/12", - "-left-10/12", - "-left-11/12", - "-left-full", - "-left-px", - "-m-0", "-m-0.5", "-m-1", - "-m-1.5", - "-m-2", - "-m-2.5", "-m-3", - "-m-3.5", "-m-4", - "-m-5", - "-m-6", - "-m-7", - "-m-8", - "-m-9", - "-m-10", - "-m-11", - "-m-12", - "-m-14", - "-m-16", - "-m-20", - "-m-24", - "-m-28", - "-m-32", - "-m-36", - "-m-40", - "-m-44", - "-m-48", - "-m-52", - "-m-56", - "-m-60", - "-m-64", - "-m-72", - "-m-80", - "-m-96", - "-m-px", - "-mask-conic-0", - "-mask-conic-1", - "-mask-conic-2", - "-mask-conic-3", - "-mask-conic-6", - "-mask-conic-12", - "-mask-conic-45", - "-mask-conic-90", - "-mask-conic-180", - "-mask-linear-0", - "-mask-linear-1", - "-mask-linear-2", - "-mask-linear-3", - "-mask-linear-6", - "-mask-linear-12", - "-mask-linear-45", - "-mask-linear-90", - "-mask-linear-180", - "-mb-0", "-mb-0.5", "-mb-1", - "-mb-1.5", - "-mb-2", - "-mb-2.5", "-mb-3", - "-mb-3.5", "-mb-4", - "-mb-5", - "-mb-6", - "-mb-7", - "-mb-8", - "-mb-9", - "-mb-10", - "-mb-11", - "-mb-12", - "-mb-14", - "-mb-16", - "-mb-20", - "-mb-24", - "-mb-28", - "-mb-32", - "-mb-36", - "-mb-40", - "-mb-44", - "-mb-48", - "-mb-52", - "-mb-56", - "-mb-60", - "-mb-64", - "-mb-72", - "-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", - "-me-1.5", - "-me-2", - "-me-2.5", "-me-3", - "-me-3.5", "-me-4", - "-me-5", - "-me-6", - "-me-7", - "-me-8", - "-me-9", - "-me-10", - "-me-11", - "-me-12", - "-me-14", - "-me-16", - "-me-20", - "-me-24", - "-me-28", - "-me-32", - "-me-36", - "-me-40", - "-me-44", - "-me-48", - "-me-52", - "-me-56", - "-me-60", - "-me-64", - "-me-72", - "-me-80", - "-me-96", - "-me-px", - "-ml-0", "-ml-0.5", "-ml-1", - "-ml-1.5", - "-ml-2", - "-ml-2.5", "-ml-3", - "-ml-3.5", "-ml-4", - "-ml-5", - "-ml-6", - "-ml-7", - "-ml-8", - "-ml-9", - "-ml-10", - "-ml-11", - "-ml-12", - "-ml-14", - "-ml-16", - "-ml-20", - "-ml-24", - "-ml-28", - "-ml-32", - "-ml-36", - "-ml-40", - "-ml-44", - "-ml-48", - "-ml-52", - "-ml-56", - "-ml-60", - "-ml-64", - "-ml-72", - "-ml-80", - "-ml-96", - "-ml-px", - "-mr-0", "-mr-0.5", "-mr-1", - "-mr-1.5", - "-mr-2", - "-mr-2.5", "-mr-3", - "-mr-3.5", "-mr-4", - "-mr-5", - "-mr-6", - "-mr-7", - "-mr-8", - "-mr-9", - "-mr-10", - "-mr-11", - "-mr-12", - "-mr-14", - "-mr-16", - "-mr-20", - "-mr-24", - "-mr-28", - "-mr-32", - "-mr-36", - "-mr-40", - "-mr-44", - "-mr-48", - "-mr-52", - "-mr-56", - "-mr-60", - "-mr-64", - "-mr-72", - "-mr-80", - "-mr-96", - "-mr-px", - "-ms-0", "-ms-0.5", "-ms-1", - "-ms-1.5", - "-ms-2", - "-ms-2.5", "-ms-3", - "-ms-3.5", "-ms-4", - "-ms-5", - "-ms-6", - "-ms-7", - "-ms-8", - "-ms-9", - "-ms-10", - "-ms-11", - "-ms-12", - "-ms-14", - "-ms-16", - "-ms-20", - "-ms-24", - "-ms-28", - "-ms-32", - "-ms-36", - "-ms-40", - "-ms-44", - "-ms-48", - "-ms-52", - "-ms-56", - "-ms-60", - "-ms-64", - "-ms-72", - "-ms-80", - "-ms-96", - "-ms-px", - "-mt-0", "-mt-0.5", "-mt-1", - "-mt-1.5", - "-mt-2", - "-mt-2.5", "-mt-3", - "-mt-3.5", "-mt-4", - "-mt-5", - "-mt-6", - "-mt-7", - "-mt-8", - "-mt-9", - "-mt-10", - "-mt-11", - "-mt-12", - "-mt-14", - "-mt-16", - "-mt-20", - "-mt-24", - "-mt-28", - "-mt-32", - "-mt-36", - "-mt-40", - "-mt-44", - "-mt-48", - "-mt-52", - "-mt-56", - "-mt-60", - "-mt-64", - "-mt-72", - "-mt-80", - "-mt-96", - "-mt-px", - "-mx-0", "-mx-0.5", "-mx-1", - "-mx-1.5", - "-mx-2", - "-mx-2.5", "-mx-3", - "-mx-3.5", "-mx-4", - "-mx-5", - "-mx-6", - "-mx-7", - "-mx-8", - "-mx-9", - "-mx-10", - "-mx-11", - "-mx-12", - "-mx-14", - "-mx-16", - "-mx-20", - "-mx-24", - "-mx-28", - "-mx-32", - "-mx-36", - "-mx-40", - "-mx-44", - "-mx-48", - "-mx-52", - "-mx-56", - "-mx-60", - "-mx-64", - "-mx-72", - "-mx-80", - "-mx-96", - "-mx-px", - "-my-0", "-my-0.5", "-my-1", - "-my-1.5", - "-my-2", - "-my-2.5", "-my-3", - "-my-3.5", "-my-4", - "-my-5", - "-my-6", - "-my-7", - "-my-8", - "-my-9", - "-my-10", - "-my-11", - "-my-12", - "-my-14", - "-my-16", - "-my-20", - "-my-24", - "-my-28", - "-my-32", - "-my-36", - "-my-40", - "-my-44", - "-my-48", - "-my-52", - "-my-56", - "-my-60", - "-my-64", - "-my-72", - "-my-80", - "-my-96", - "-my-px", "-order-1", + "-order-10", + "-order-11", + "-order-12", "-order-2", "-order-3", "-order-4", @@ -1251,113 +104,51 @@ exports[`getClassList 1`] = ` "-order-7", "-order-8", "-order-9", - "-order-10", - "-order-11", - "-order-12", - "-outline-offset-0", - "-outline-offset-1", - "-outline-offset-2", - "-outline-offset-4", - "-outline-offset-8", - "-right-0", "-right-0.5", "-right-1", - "-right-1.5", - "-right-2", - "-right-2.5", "-right-3", - "-right-3.5", "-right-4", - "-right-5", - "-right-6", - "-right-7", - "-right-8", - "-right-9", - "-right-10", - "-right-11", - "-right-12", - "-right-14", - "-right-16", - "-right-20", - "-right-24", - "-right-28", - "-right-32", - "-right-36", - "-right-40", - "-right-44", - "-right-48", - "-right-52", - "-right-56", - "-right-60", - "-right-64", - "-right-72", - "-right-80", - "-right-96", - "-right-1/2", - "-right-1/3", - "-right-1/4", - "-right-1/5", - "-right-1/6", - "-right-1/12", - "-right-2/3", - "-right-2/4", - "-right-2/5", - "-right-2/6", - "-right-2/12", - "-right-3/4", - "-right-3/5", - "-right-3/6", - "-right-3/12", - "-right-4/5", - "-right-4/6", - "-right-4/12", - "-right-5/6", - "-right-5/12", - "-right-6/12", - "-right-7/12", - "-right-8/12", - "-right-9/12", - "-right-10/12", - "-right-11/12", - "-right-full", - "-right-px", "-rotate-0", "-rotate-1", + "-rotate-12", + "-rotate-180", "-rotate-2", "-rotate-3", - "-rotate-6", - "-rotate-12", "-rotate-45", + "-rotate-6", "-rotate-90", - "-rotate-180", "-rotate-x-0", "-rotate-x-1", + "-rotate-x-12", + "-rotate-x-180", "-rotate-x-2", "-rotate-x-3", - "-rotate-x-6", - "-rotate-x-12", "-rotate-x-45", + "-rotate-x-6", "-rotate-x-90", - "-rotate-x-180", "-rotate-y-0", "-rotate-y-1", + "-rotate-y-12", + "-rotate-y-180", "-rotate-y-2", "-rotate-y-3", - "-rotate-y-6", - "-rotate-y-12", "-rotate-y-45", + "-rotate-y-6", "-rotate-y-90", - "-rotate-y-180", "-rotate-z-0", "-rotate-z-1", + "-rotate-z-12", + "-rotate-z-180", "-rotate-z-2", "-rotate-z-3", - "-rotate-z-6", - "-rotate-z-12", "-rotate-z-45", + "-rotate-z-6", "-rotate-z-90", - "-rotate-z-180", "-row-end-1", + "-row-end-10", + "-row-end-11", + "-row-end-12", + "-row-end-13", "-row-end-2", "-row-end-3", "-row-end-4", @@ -1366,11 +157,11 @@ exports[`getClassList 1`] = ` "-row-end-7", "-row-end-8", "-row-end-9", - "-row-end-10", - "-row-end-11", - "-row-end-12", - "-row-end-13", "-row-start-1", + "-row-start-10", + "-row-start-11", + "-row-start-12", + "-row-start-13", "-row-start-2", "-row-start-3", "-row-start-4", @@ -1379,812 +170,172 @@ exports[`getClassList 1`] = ` "-row-start-7", "-row-start-8", "-row-start-9", - "-row-start-10", - "-row-start-11", - "-row-start-12", - "-row-start-13", "-scale-0", - "-scale-50", - "-scale-75", - "-scale-90", - "-scale-95", "-scale-100", "-scale-105", "-scale-110", "-scale-125", "-scale-150", "-scale-200", + "-scale-50", + "-scale-75", + "-scale-90", + "-scale-95", "-scale-x-0", - "-scale-x-50", - "-scale-x-75", - "-scale-x-90", - "-scale-x-95", "-scale-x-100", "-scale-x-105", "-scale-x-110", "-scale-x-125", "-scale-x-150", "-scale-x-200", + "-scale-x-50", + "-scale-x-75", + "-scale-x-90", + "-scale-x-95", "-scale-y-0", - "-scale-y-50", - "-scale-y-75", - "-scale-y-90", - "-scale-y-95", "-scale-y-100", "-scale-y-105", "-scale-y-110", "-scale-y-125", "-scale-y-150", "-scale-y-200", + "-scale-y-50", + "-scale-y-75", + "-scale-y-90", + "-scale-y-95", "-scale-z-0", - "-scale-z-50", - "-scale-z-75", - "-scale-z-90", - "-scale-z-95", "-scale-z-100", "-scale-z-105", "-scale-z-110", "-scale-z-125", "-scale-z-150", "-scale-z-200", - "-scroll-m-0", + "-scale-z-50", + "-scale-z-75", + "-scale-z-90", + "-scale-z-95", "-scroll-m-0.5", "-scroll-m-1", - "-scroll-m-1.5", - "-scroll-m-2", - "-scroll-m-2.5", "-scroll-m-3", - "-scroll-m-3.5", "-scroll-m-4", - "-scroll-m-5", - "-scroll-m-6", - "-scroll-m-7", - "-scroll-m-8", - "-scroll-m-9", - "-scroll-m-10", - "-scroll-m-11", - "-scroll-m-12", - "-scroll-m-14", - "-scroll-m-16", - "-scroll-m-20", - "-scroll-m-24", - "-scroll-m-28", - "-scroll-m-32", - "-scroll-m-36", - "-scroll-m-40", - "-scroll-m-44", - "-scroll-m-48", - "-scroll-m-52", - "-scroll-m-56", - "-scroll-m-60", - "-scroll-m-64", - "-scroll-m-72", - "-scroll-m-80", - "-scroll-m-96", - "-scroll-m-px", - "-scroll-mb-0", "-scroll-mb-0.5", "-scroll-mb-1", - "-scroll-mb-1.5", - "-scroll-mb-2", - "-scroll-mb-2.5", "-scroll-mb-3", - "-scroll-mb-3.5", "-scroll-mb-4", - "-scroll-mb-5", - "-scroll-mb-6", - "-scroll-mb-7", - "-scroll-mb-8", - "-scroll-mb-9", - "-scroll-mb-10", - "-scroll-mb-11", - "-scroll-mb-12", - "-scroll-mb-14", - "-scroll-mb-16", - "-scroll-mb-20", - "-scroll-mb-24", - "-scroll-mb-28", - "-scroll-mb-32", - "-scroll-mb-36", - "-scroll-mb-40", - "-scroll-mb-44", - "-scroll-mb-48", - "-scroll-mb-52", - "-scroll-mb-56", - "-scroll-mb-60", - "-scroll-mb-64", - "-scroll-mb-72", - "-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", - "-scroll-me-1.5", - "-scroll-me-2", - "-scroll-me-2.5", "-scroll-me-3", - "-scroll-me-3.5", "-scroll-me-4", - "-scroll-me-5", - "-scroll-me-6", - "-scroll-me-7", - "-scroll-me-8", - "-scroll-me-9", - "-scroll-me-10", - "-scroll-me-11", - "-scroll-me-12", - "-scroll-me-14", - "-scroll-me-16", - "-scroll-me-20", - "-scroll-me-24", - "-scroll-me-28", - "-scroll-me-32", - "-scroll-me-36", - "-scroll-me-40", - "-scroll-me-44", - "-scroll-me-48", - "-scroll-me-52", - "-scroll-me-56", - "-scroll-me-60", - "-scroll-me-64", - "-scroll-me-72", - "-scroll-me-80", - "-scroll-me-96", - "-scroll-me-px", - "-scroll-ml-0", "-scroll-ml-0.5", "-scroll-ml-1", - "-scroll-ml-1.5", - "-scroll-ml-2", - "-scroll-ml-2.5", "-scroll-ml-3", - "-scroll-ml-3.5", "-scroll-ml-4", - "-scroll-ml-5", - "-scroll-ml-6", - "-scroll-ml-7", - "-scroll-ml-8", - "-scroll-ml-9", - "-scroll-ml-10", - "-scroll-ml-11", - "-scroll-ml-12", - "-scroll-ml-14", - "-scroll-ml-16", - "-scroll-ml-20", - "-scroll-ml-24", - "-scroll-ml-28", - "-scroll-ml-32", - "-scroll-ml-36", - "-scroll-ml-40", - "-scroll-ml-44", - "-scroll-ml-48", - "-scroll-ml-52", - "-scroll-ml-56", - "-scroll-ml-60", - "-scroll-ml-64", - "-scroll-ml-72", - "-scroll-ml-80", - "-scroll-ml-96", - "-scroll-ml-px", - "-scroll-mr-0", "-scroll-mr-0.5", "-scroll-mr-1", - "-scroll-mr-1.5", - "-scroll-mr-2", - "-scroll-mr-2.5", "-scroll-mr-3", - "-scroll-mr-3.5", "-scroll-mr-4", - "-scroll-mr-5", - "-scroll-mr-6", - "-scroll-mr-7", - "-scroll-mr-8", - "-scroll-mr-9", - "-scroll-mr-10", - "-scroll-mr-11", - "-scroll-mr-12", - "-scroll-mr-14", - "-scroll-mr-16", - "-scroll-mr-20", - "-scroll-mr-24", - "-scroll-mr-28", - "-scroll-mr-32", - "-scroll-mr-36", - "-scroll-mr-40", - "-scroll-mr-44", - "-scroll-mr-48", - "-scroll-mr-52", - "-scroll-mr-56", - "-scroll-mr-60", - "-scroll-mr-64", - "-scroll-mr-72", - "-scroll-mr-80", - "-scroll-mr-96", - "-scroll-mr-px", - "-scroll-ms-0", "-scroll-ms-0.5", "-scroll-ms-1", - "-scroll-ms-1.5", - "-scroll-ms-2", - "-scroll-ms-2.5", "-scroll-ms-3", - "-scroll-ms-3.5", "-scroll-ms-4", - "-scroll-ms-5", - "-scroll-ms-6", - "-scroll-ms-7", - "-scroll-ms-8", - "-scroll-ms-9", - "-scroll-ms-10", - "-scroll-ms-11", - "-scroll-ms-12", - "-scroll-ms-14", - "-scroll-ms-16", - "-scroll-ms-20", - "-scroll-ms-24", - "-scroll-ms-28", - "-scroll-ms-32", - "-scroll-ms-36", - "-scroll-ms-40", - "-scroll-ms-44", - "-scroll-ms-48", - "-scroll-ms-52", - "-scroll-ms-56", - "-scroll-ms-60", - "-scroll-ms-64", - "-scroll-ms-72", - "-scroll-ms-80", - "-scroll-ms-96", - "-scroll-ms-px", - "-scroll-mt-0", "-scroll-mt-0.5", "-scroll-mt-1", - "-scroll-mt-1.5", - "-scroll-mt-2", - "-scroll-mt-2.5", "-scroll-mt-3", - "-scroll-mt-3.5", "-scroll-mt-4", - "-scroll-mt-5", - "-scroll-mt-6", - "-scroll-mt-7", - "-scroll-mt-8", - "-scroll-mt-9", - "-scroll-mt-10", - "-scroll-mt-11", - "-scroll-mt-12", - "-scroll-mt-14", - "-scroll-mt-16", - "-scroll-mt-20", - "-scroll-mt-24", - "-scroll-mt-28", - "-scroll-mt-32", - "-scroll-mt-36", - "-scroll-mt-40", - "-scroll-mt-44", - "-scroll-mt-48", - "-scroll-mt-52", - "-scroll-mt-56", - "-scroll-mt-60", - "-scroll-mt-64", - "-scroll-mt-72", - "-scroll-mt-80", - "-scroll-mt-96", - "-scroll-mt-px", - "-scroll-mx-0", "-scroll-mx-0.5", "-scroll-mx-1", - "-scroll-mx-1.5", - "-scroll-mx-2", - "-scroll-mx-2.5", "-scroll-mx-3", - "-scroll-mx-3.5", "-scroll-mx-4", - "-scroll-mx-5", - "-scroll-mx-6", - "-scroll-mx-7", - "-scroll-mx-8", - "-scroll-mx-9", - "-scroll-mx-10", - "-scroll-mx-11", - "-scroll-mx-12", - "-scroll-mx-14", - "-scroll-mx-16", - "-scroll-mx-20", - "-scroll-mx-24", - "-scroll-mx-28", - "-scroll-mx-32", - "-scroll-mx-36", - "-scroll-mx-40", - "-scroll-mx-44", - "-scroll-mx-48", - "-scroll-mx-52", - "-scroll-mx-56", - "-scroll-mx-60", - "-scroll-mx-64", - "-scroll-mx-72", - "-scroll-mx-80", - "-scroll-mx-96", - "-scroll-mx-px", - "-scroll-my-0", "-scroll-my-0.5", "-scroll-my-1", - "-scroll-my-1.5", - "-scroll-my-2", - "-scroll-my-2.5", "-scroll-my-3", - "-scroll-my-3.5", "-scroll-my-4", - "-scroll-my-5", - "-scroll-my-6", - "-scroll-my-7", - "-scroll-my-8", - "-scroll-my-9", - "-scroll-my-10", - "-scroll-my-11", - "-scroll-my-12", - "-scroll-my-14", - "-scroll-my-16", - "-scroll-my-20", - "-scroll-my-24", - "-scroll-my-28", - "-scroll-my-32", - "-scroll-my-36", - "-scroll-my-40", - "-scroll-my-44", - "-scroll-my-48", - "-scroll-my-52", - "-scroll-my-56", - "-scroll-my-60", - "-scroll-my-64", - "-scroll-my-72", - "-scroll-my-80", - "-scroll-my-96", - "-scroll-my-px", + "-scroll-p-0.5", + "-scroll-p-1", + "-scroll-p-3", + "-scroll-p-4", + "-scroll-pb-0.5", + "-scroll-pb-1", + "-scroll-pb-3", + "-scroll-pb-4", + "-scroll-pe-0.5", + "-scroll-pe-1", + "-scroll-pe-3", + "-scroll-pe-4", + "-scroll-pl-0.5", + "-scroll-pl-1", + "-scroll-pl-3", + "-scroll-pl-4", + "-scroll-pr-0.5", + "-scroll-pr-1", + "-scroll-pr-3", + "-scroll-pr-4", + "-scroll-ps-0.5", + "-scroll-ps-1", + "-scroll-ps-3", + "-scroll-ps-4", + "-scroll-pt-0.5", + "-scroll-pt-1", + "-scroll-pt-3", + "-scroll-pt-4", + "-scroll-px-0.5", + "-scroll-px-1", + "-scroll-px-3", + "-scroll-px-4", + "-scroll-py-0.5", + "-scroll-py-1", + "-scroll-py-3", + "-scroll-py-4", "-skew-0", "-skew-1", + "-skew-12", "-skew-2", "-skew-3", "-skew-6", - "-skew-12", "-skew-x-0", "-skew-x-1", + "-skew-x-12", "-skew-x-2", "-skew-x-3", "-skew-x-6", - "-skew-x-12", "-skew-y-0", "-skew-y-1", + "-skew-y-12", "-skew-y-2", "-skew-y-3", "-skew-y-6", - "-skew-y-12", - "-space-x-0", "-space-x-0.5", "-space-x-1", - "-space-x-1.5", - "-space-x-2", - "-space-x-2.5", "-space-x-3", - "-space-x-3.5", "-space-x-4", - "-space-x-5", - "-space-x-6", - "-space-x-7", - "-space-x-8", - "-space-x-9", - "-space-x-10", - "-space-x-11", - "-space-x-12", - "-space-x-14", - "-space-x-16", - "-space-x-20", - "-space-x-24", - "-space-x-28", - "-space-x-32", - "-space-x-36", - "-space-x-40", - "-space-x-44", - "-space-x-48", - "-space-x-52", - "-space-x-56", - "-space-x-60", - "-space-x-64", - "-space-x-72", - "-space-x-80", - "-space-x-96", - "-space-x-px", - "-space-y-0", "-space-y-0.5", "-space-y-1", - "-space-y-1.5", - "-space-y-2", - "-space-y-2.5", "-space-y-3", - "-space-y-3.5", "-space-y-4", - "-space-y-5", - "-space-y-6", - "-space-y-7", - "-space-y-8", - "-space-y-9", - "-space-y-10", - "-space-y-11", - "-space-y-12", - "-space-y-14", - "-space-y-16", - "-space-y-20", - "-space-y-24", - "-space-y-28", - "-space-y-32", - "-space-y-36", - "-space-y-40", - "-space-y-44", - "-space-y-48", - "-space-y-52", - "-space-y-56", - "-space-y-60", - "-space-y-64", - "-space-y-72", - "-space-y-80", - "-space-y-96", - "-space-y-px", - "-start-full", - "-start-px", - "-top-0", + "-start-0.5", + "-start-1", + "-start-3", + "-start-4", "-top-0.5", "-top-1", - "-top-1.5", - "-top-2", - "-top-2.5", "-top-3", - "-top-3.5", "-top-4", - "-top-5", - "-top-6", - "-top-7", - "-top-8", - "-top-9", - "-top-10", - "-top-11", - "-top-12", - "-top-14", - "-top-16", - "-top-20", - "-top-24", - "-top-28", - "-top-32", - "-top-36", - "-top-40", - "-top-44", - "-top-48", - "-top-52", - "-top-56", - "-top-60", - "-top-64", - "-top-72", - "-top-80", - "-top-96", - "-top-1/2", - "-top-1/3", - "-top-1/4", - "-top-1/5", - "-top-1/6", - "-top-1/12", - "-top-2/3", - "-top-2/4", - "-top-2/5", - "-top-2/6", - "-top-2/12", - "-top-3/4", - "-top-3/5", - "-top-3/6", - "-top-3/12", - "-top-4/5", - "-top-4/6", - "-top-4/12", - "-top-5/6", - "-top-5/12", - "-top-6/12", - "-top-7/12", - "-top-8/12", - "-top-9/12", - "-top-10/12", - "-top-11/12", - "-top-full", - "-top-px", - "-translate-0", "-translate-0.5", "-translate-1", - "-translate-1.5", - "-translate-2", - "-translate-2.5", "-translate-3", - "-translate-3.5", "-translate-4", - "-translate-5", - "-translate-6", - "-translate-7", - "-translate-8", - "-translate-9", - "-translate-10", - "-translate-11", - "-translate-12", - "-translate-14", - "-translate-16", - "-translate-20", - "-translate-24", - "-translate-28", - "-translate-32", - "-translate-36", - "-translate-40", - "-translate-44", - "-translate-48", - "-translate-52", - "-translate-56", - "-translate-60", - "-translate-64", - "-translate-72", - "-translate-80", - "-translate-96", - "-translate-1/2", - "-translate-1/3", - "-translate-1/4", - "-translate-1/5", - "-translate-1/6", - "-translate-1/12", - "-translate-2/3", - "-translate-2/4", - "-translate-2/5", - "-translate-2/6", - "-translate-2/12", - "-translate-3/4", - "-translate-3/5", - "-translate-3/6", - "-translate-3/12", - "-translate-4/5", - "-translate-4/6", - "-translate-4/12", - "-translate-5/6", - "-translate-5/12", - "-translate-6/12", - "-translate-7/12", - "-translate-8/12", - "-translate-9/12", - "-translate-10/12", - "-translate-11/12", - "-translate-full", - "-translate-px", - "-translate-x-0", "-translate-x-0.5", "-translate-x-1", - "-translate-x-1.5", - "-translate-x-2", - "-translate-x-2.5", "-translate-x-3", - "-translate-x-3.5", "-translate-x-4", - "-translate-x-5", - "-translate-x-6", - "-translate-x-7", - "-translate-x-8", - "-translate-x-9", - "-translate-x-10", - "-translate-x-11", - "-translate-x-12", - "-translate-x-14", - "-translate-x-16", - "-translate-x-20", - "-translate-x-24", - "-translate-x-28", - "-translate-x-32", - "-translate-x-36", - "-translate-x-40", - "-translate-x-44", - "-translate-x-48", - "-translate-x-52", - "-translate-x-56", - "-translate-x-60", - "-translate-x-64", - "-translate-x-72", - "-translate-x-80", - "-translate-x-96", - "-translate-x-1/2", - "-translate-x-1/3", - "-translate-x-1/4", - "-translate-x-1/5", - "-translate-x-1/6", - "-translate-x-1/12", - "-translate-x-2/3", - "-translate-x-2/4", - "-translate-x-2/5", - "-translate-x-2/6", - "-translate-x-2/12", - "-translate-x-3/4", - "-translate-x-3/5", - "-translate-x-3/6", - "-translate-x-3/12", - "-translate-x-4/5", - "-translate-x-4/6", - "-translate-x-4/12", - "-translate-x-5/6", - "-translate-x-5/12", - "-translate-x-6/12", - "-translate-x-7/12", - "-translate-x-8/12", - "-translate-x-9/12", - "-translate-x-10/12", - "-translate-x-11/12", - "-translate-x-full", - "-translate-x-px", - "-translate-y-0", "-translate-y-0.5", "-translate-y-1", - "-translate-y-1.5", - "-translate-y-2", - "-translate-y-2.5", "-translate-y-3", - "-translate-y-3.5", "-translate-y-4", - "-translate-y-5", - "-translate-y-6", - "-translate-y-7", - "-translate-y-8", - "-translate-y-9", - "-translate-y-10", - "-translate-y-11", - "-translate-y-12", - "-translate-y-14", - "-translate-y-16", - "-translate-y-20", - "-translate-y-24", - "-translate-y-28", - "-translate-y-32", - "-translate-y-36", - "-translate-y-40", - "-translate-y-44", - "-translate-y-48", - "-translate-y-52", - "-translate-y-56", - "-translate-y-60", - "-translate-y-64", - "-translate-y-72", - "-translate-y-80", - "-translate-y-96", - "-translate-y-1/2", - "-translate-y-1/3", - "-translate-y-1/4", - "-translate-y-1/5", - "-translate-y-1/6", - "-translate-y-1/12", - "-translate-y-2/3", - "-translate-y-2/4", - "-translate-y-2/5", - "-translate-y-2/6", - "-translate-y-2/12", - "-translate-y-3/4", - "-translate-y-3/5", - "-translate-y-3/6", - "-translate-y-3/12", - "-translate-y-4/5", - "-translate-y-4/6", - "-translate-y-4/12", - "-translate-y-5/6", - "-translate-y-5/12", - "-translate-y-6/12", - "-translate-y-7/12", - "-translate-y-8/12", - "-translate-y-9/12", - "-translate-y-10/12", - "-translate-y-11/12", - "-translate-y-full", - "-translate-y-px", - "-translate-z-0", "-translate-z-0.5", "-translate-z-1", - "-translate-z-1.5", - "-translate-z-2", - "-translate-z-2.5", "-translate-z-3", - "-translate-z-3.5", "-translate-z-4", - "-translate-z-5", - "-translate-z-6", - "-translate-z-7", - "-translate-z-8", - "-translate-z-9", - "-translate-z-10", - "-translate-z-11", - "-translate-z-12", - "-translate-z-14", - "-translate-z-16", - "-translate-z-20", - "-translate-z-24", - "-translate-z-28", - "-translate-z-32", - "-translate-z-36", - "-translate-z-40", - "-translate-z-44", - "-translate-z-48", - "-translate-z-52", - "-translate-z-56", - "-translate-z-60", - "-translate-z-64", - "-translate-z-72", - "-translate-z-80", - "-translate-z-96", - "-translate-z-px", "-underline-offset-0", "-underline-offset-1", "-underline-offset-2", @@ -2196,7 +347,6 @@ exports[`getClassList 1`] = ` "-z-30", "-z-40", "-z-50", - "@container", "@container-normal", "absolute", "accent-auto", @@ -2222,7 +372,6 @@ exports[`getClassList 1`] = ` "accent-current/90", "accent-current/95", "accent-current/100", - "accent-current/background", "accent-inherit", "accent-inherit/0", "accent-inherit/5", @@ -2245,7 +394,6 @@ exports[`getClassList 1`] = ` "accent-inherit/90", "accent-inherit/95", "accent-inherit/100", - "accent-inherit/background", "accent-transparent", "accent-transparent/0", "accent-transparent/5", @@ -2268,7 +416,6 @@ exports[`getClassList 1`] = ` "accent-transparent/90", "accent-transparent/95", "accent-transparent/100", - "accent-transparent/background", "align-baseline", "align-bottom", "align-middle", @@ -2294,44 +441,44 @@ exports[`getClassList 1`] = ` "auto-rows-min", "backdrop-blur-none", "backdrop-brightness-0", - "backdrop-brightness-50", - "backdrop-brightness-75", - "backdrop-brightness-90", - "backdrop-brightness-95", "backdrop-brightness-100", "backdrop-brightness-105", "backdrop-brightness-110", "backdrop-brightness-125", "backdrop-brightness-150", "backdrop-brightness-200", + "backdrop-brightness-50", + "backdrop-brightness-75", + "backdrop-brightness-90", + "backdrop-brightness-95", "backdrop-contrast-0", - "backdrop-contrast-50", - "backdrop-contrast-75", "backdrop-contrast-100", "backdrop-contrast-125", "backdrop-contrast-150", "backdrop-contrast-200", + "backdrop-contrast-50", + "backdrop-contrast-75", "backdrop-grayscale", "backdrop-grayscale-0", + "backdrop-grayscale-100", "backdrop-grayscale-25", "backdrop-grayscale-50", "backdrop-grayscale-75", - "backdrop-grayscale-100", "backdrop-hue-rotate-0", "backdrop-hue-rotate-15", + "backdrop-hue-rotate-180", "backdrop-hue-rotate-30", "backdrop-hue-rotate-60", "backdrop-hue-rotate-90", - "backdrop-hue-rotate-180", "backdrop-invert", "backdrop-invert-0", + "backdrop-invert-100", "backdrop-invert-25", "backdrop-invert-50", "backdrop-invert-75", - "backdrop-invert-100", "backdrop-opacity-0", - "backdrop-opacity-5", "backdrop-opacity-10", + "backdrop-opacity-100", "backdrop-opacity-15", "backdrop-opacity-20", "backdrop-opacity-25", @@ -2339,6 +486,7 @@ exports[`getClassList 1`] = ` "backdrop-opacity-35", "backdrop-opacity-40", "backdrop-opacity-45", + "backdrop-opacity-5", "backdrop-opacity-50", "backdrop-opacity-55", "backdrop-opacity-60", @@ -2349,84 +497,25 @@ exports[`getClassList 1`] = ` "backdrop-opacity-85", "backdrop-opacity-90", "backdrop-opacity-95", - "backdrop-opacity-100", "backdrop-opacity-background", "backdrop-saturate-0", - "backdrop-saturate-50", "backdrop-saturate-100", "backdrop-saturate-150", "backdrop-saturate-200", + "backdrop-saturate-50", "backdrop-sepia", "backdrop-sepia-0", - "backdrop-sepia-50", "backdrop-sepia-100", + "backdrop-sepia-50", "backface-hidden", "backface-visible", - "basis-0", "basis-0.5", "basis-1", - "basis-1.5", - "basis-2", - "basis-2.5", "basis-3", - "basis-3.5", "basis-4", - "basis-5", - "basis-6", - "basis-7", - "basis-8", - "basis-9", - "basis-10", - "basis-11", - "basis-12", - "basis-14", - "basis-16", - "basis-20", - "basis-24", - "basis-28", - "basis-32", - "basis-36", - "basis-40", - "basis-44", - "basis-48", - "basis-52", - "basis-56", - "basis-60", - "basis-64", - "basis-72", - "basis-80", - "basis-96", + "basis-4", "basis-auto", "basis-full", - "basis-lg", - "basis-md", - "basis-1/2", - "basis-1/3", - "basis-1/4", - "basis-1/5", - "basis-1/6", - "basis-1/12", - "basis-2/3", - "basis-2/4", - "basis-2/5", - "basis-2/6", - "basis-2/12", - "basis-3/4", - "basis-3/5", - "basis-3/6", - "basis-3/12", - "basis-4/5", - "basis-4/6", - "basis-4/12", - "basis-5/6", - "basis-5/12", - "basis-6/12", - "basis-7/12", - "basis-8/12", - "basis-9/12", - "basis-10/12", - "basis-11/12", - "basis-px", "bg-auto", "bg-blend-color", "bg-blend-color-burn", @@ -2445,130 +534,11 @@ exports[`getClassList 1`] = ` "bg-blend-screen", "bg-blend-soft-light", "bg-bottom", - "bg-bottom-left", - "bg-bottom-right", "bg-center", "bg-clip-border", "bg-clip-content", "bg-clip-padding", "bg-clip-text", - "bg-conic", - "bg-conic/oklab", - "bg-conic/oklch", - "bg-conic/srgb", - "bg-conic/hsl", - "bg-conic/longer", - "bg-conic/shorter", - "bg-conic/increasing", - "bg-conic/decreasing", - "bg-conic-0", - "bg-conic-0/oklab", - "bg-conic-0/oklch", - "bg-conic-0/srgb", - "bg-conic-0/hsl", - "bg-conic-0/longer", - "bg-conic-0/shorter", - "bg-conic-0/increasing", - "bg-conic-0/decreasing", - "bg-conic-30", - "bg-conic-30/oklab", - "bg-conic-30/oklch", - "bg-conic-30/srgb", - "bg-conic-30/hsl", - "bg-conic-30/longer", - "bg-conic-30/shorter", - "bg-conic-30/increasing", - "bg-conic-30/decreasing", - "bg-conic-60", - "bg-conic-60/oklab", - "bg-conic-60/oklch", - "bg-conic-60/srgb", - "bg-conic-60/hsl", - "bg-conic-60/longer", - "bg-conic-60/shorter", - "bg-conic-60/increasing", - "bg-conic-60/decreasing", - "bg-conic-90", - "bg-conic-90/oklab", - "bg-conic-90/oklch", - "bg-conic-90/srgb", - "bg-conic-90/hsl", - "bg-conic-90/longer", - "bg-conic-90/shorter", - "bg-conic-90/increasing", - "bg-conic-90/decreasing", - "bg-conic-120", - "bg-conic-120/oklab", - "bg-conic-120/oklch", - "bg-conic-120/srgb", - "bg-conic-120/hsl", - "bg-conic-120/longer", - "bg-conic-120/shorter", - "bg-conic-120/increasing", - "bg-conic-120/decreasing", - "bg-conic-150", - "bg-conic-150/oklab", - "bg-conic-150/oklch", - "bg-conic-150/srgb", - "bg-conic-150/hsl", - "bg-conic-150/longer", - "bg-conic-150/shorter", - "bg-conic-150/increasing", - "bg-conic-150/decreasing", - "bg-conic-180", - "bg-conic-180/oklab", - "bg-conic-180/oklch", - "bg-conic-180/srgb", - "bg-conic-180/hsl", - "bg-conic-180/longer", - "bg-conic-180/shorter", - "bg-conic-180/increasing", - "bg-conic-180/decreasing", - "bg-conic-210", - "bg-conic-210/oklab", - "bg-conic-210/oklch", - "bg-conic-210/srgb", - "bg-conic-210/hsl", - "bg-conic-210/longer", - "bg-conic-210/shorter", - "bg-conic-210/increasing", - "bg-conic-210/decreasing", - "bg-conic-240", - "bg-conic-240/oklab", - "bg-conic-240/oklch", - "bg-conic-240/srgb", - "bg-conic-240/hsl", - "bg-conic-240/longer", - "bg-conic-240/shorter", - "bg-conic-240/increasing", - "bg-conic-240/decreasing", - "bg-conic-270", - "bg-conic-270/oklab", - "bg-conic-270/oklch", - "bg-conic-270/srgb", - "bg-conic-270/hsl", - "bg-conic-270/longer", - "bg-conic-270/shorter", - "bg-conic-270/increasing", - "bg-conic-270/decreasing", - "bg-conic-300", - "bg-conic-300/oklab", - "bg-conic-300/oklch", - "bg-conic-300/srgb", - "bg-conic-300/hsl", - "bg-conic-300/longer", - "bg-conic-300/shorter", - "bg-conic-300/increasing", - "bg-conic-300/decreasing", - "bg-conic-330", - "bg-conic-330/oklab", - "bg-conic-330/oklch", - "bg-conic-330/srgb", - "bg-conic-330/hsl", - "bg-conic-330/longer", - "bg-conic-330/shorter", - "bg-conic-330/increasing", - "bg-conic-330/decreasing", "bg-contain", "bg-cover", "bg-current", @@ -2593,8 +563,15 @@ exports[`getClassList 1`] = ` "bg-current/90", "bg-current/95", "bg-current/100", - "bg-current/background", "bg-fixed", + "bg-gradient-to-b", + "bg-gradient-to-bl", + "bg-gradient-to-br", + "bg-gradient-to-l", + "bg-gradient-to-r", + "bg-gradient-to-t", + "bg-gradient-to-tl", + "bg-gradient-to-tr", "bg-inherit", "bg-inherit/0", "bg-inherit/5", @@ -2617,213 +594,33 @@ exports[`getClassList 1`] = ` "bg-inherit/90", "bg-inherit/95", "bg-inherit/100", - "bg-inherit/background", "bg-left", - "bg-linear-0", - "bg-linear-0/oklab", - "bg-linear-0/oklch", - "bg-linear-0/srgb", - "bg-linear-0/hsl", - "bg-linear-0/longer", - "bg-linear-0/shorter", - "bg-linear-0/increasing", - "bg-linear-0/decreasing", - "bg-linear-30", - "bg-linear-30/oklab", - "bg-linear-30/oklch", - "bg-linear-30/srgb", - "bg-linear-30/hsl", - "bg-linear-30/longer", - "bg-linear-30/shorter", - "bg-linear-30/increasing", - "bg-linear-30/decreasing", - "bg-linear-60", - "bg-linear-60/oklab", - "bg-linear-60/oklch", - "bg-linear-60/srgb", - "bg-linear-60/hsl", - "bg-linear-60/longer", - "bg-linear-60/shorter", - "bg-linear-60/increasing", - "bg-linear-60/decreasing", - "bg-linear-90", - "bg-linear-90/oklab", - "bg-linear-90/oklch", - "bg-linear-90/srgb", - "bg-linear-90/hsl", - "bg-linear-90/longer", - "bg-linear-90/shorter", - "bg-linear-90/increasing", - "bg-linear-90/decreasing", - "bg-linear-120", - "bg-linear-120/oklab", - "bg-linear-120/oklch", - "bg-linear-120/srgb", - "bg-linear-120/hsl", - "bg-linear-120/longer", - "bg-linear-120/shorter", - "bg-linear-120/increasing", - "bg-linear-120/decreasing", - "bg-linear-150", - "bg-linear-150/oklab", - "bg-linear-150/oklch", - "bg-linear-150/srgb", - "bg-linear-150/hsl", - "bg-linear-150/longer", - "bg-linear-150/shorter", - "bg-linear-150/increasing", - "bg-linear-150/decreasing", - "bg-linear-180", - "bg-linear-180/oklab", - "bg-linear-180/oklch", - "bg-linear-180/srgb", - "bg-linear-180/hsl", - "bg-linear-180/longer", - "bg-linear-180/shorter", - "bg-linear-180/increasing", - "bg-linear-180/decreasing", - "bg-linear-210", - "bg-linear-210/oklab", - "bg-linear-210/oklch", - "bg-linear-210/srgb", - "bg-linear-210/hsl", - "bg-linear-210/longer", - "bg-linear-210/shorter", - "bg-linear-210/increasing", - "bg-linear-210/decreasing", - "bg-linear-240", - "bg-linear-240/oklab", - "bg-linear-240/oklch", - "bg-linear-240/srgb", - "bg-linear-240/hsl", - "bg-linear-240/longer", - "bg-linear-240/shorter", - "bg-linear-240/increasing", - "bg-linear-240/decreasing", - "bg-linear-270", - "bg-linear-270/oklab", - "bg-linear-270/oklch", - "bg-linear-270/srgb", - "bg-linear-270/hsl", - "bg-linear-270/longer", - "bg-linear-270/shorter", - "bg-linear-270/increasing", - "bg-linear-270/decreasing", - "bg-linear-300", - "bg-linear-300/oklab", - "bg-linear-300/oklch", - "bg-linear-300/srgb", - "bg-linear-300/hsl", - "bg-linear-300/longer", - "bg-linear-300/shorter", - "bg-linear-300/increasing", - "bg-linear-300/decreasing", - "bg-linear-330", - "bg-linear-330/oklab", - "bg-linear-330/oklch", - "bg-linear-330/srgb", - "bg-linear-330/hsl", - "bg-linear-330/longer", - "bg-linear-330/shorter", - "bg-linear-330/increasing", - "bg-linear-330/decreasing", + "bg-left-bottom", + "bg-left-top", "bg-linear-to-b", - "bg-linear-to-b/oklab", - "bg-linear-to-b/oklch", - "bg-linear-to-b/srgb", - "bg-linear-to-b/hsl", - "bg-linear-to-b/longer", - "bg-linear-to-b/shorter", - "bg-linear-to-b/increasing", - "bg-linear-to-b/decreasing", "bg-linear-to-bl", - "bg-linear-to-bl/oklab", - "bg-linear-to-bl/oklch", - "bg-linear-to-bl/srgb", - "bg-linear-to-bl/hsl", - "bg-linear-to-bl/longer", - "bg-linear-to-bl/shorter", - "bg-linear-to-bl/increasing", - "bg-linear-to-bl/decreasing", "bg-linear-to-br", - "bg-linear-to-br/oklab", - "bg-linear-to-br/oklch", - "bg-linear-to-br/srgb", - "bg-linear-to-br/hsl", - "bg-linear-to-br/longer", - "bg-linear-to-br/shorter", - "bg-linear-to-br/increasing", - "bg-linear-to-br/decreasing", "bg-linear-to-l", - "bg-linear-to-l/oklab", - "bg-linear-to-l/oklch", - "bg-linear-to-l/srgb", - "bg-linear-to-l/hsl", - "bg-linear-to-l/longer", - "bg-linear-to-l/shorter", - "bg-linear-to-l/increasing", - "bg-linear-to-l/decreasing", "bg-linear-to-r", - "bg-linear-to-r/oklab", - "bg-linear-to-r/oklch", - "bg-linear-to-r/srgb", - "bg-linear-to-r/hsl", - "bg-linear-to-r/longer", - "bg-linear-to-r/shorter", - "bg-linear-to-r/increasing", - "bg-linear-to-r/decreasing", "bg-linear-to-t", - "bg-linear-to-t/oklab", - "bg-linear-to-t/oklch", - "bg-linear-to-t/srgb", - "bg-linear-to-t/hsl", - "bg-linear-to-t/longer", - "bg-linear-to-t/shorter", - "bg-linear-to-t/increasing", - "bg-linear-to-t/decreasing", "bg-linear-to-tl", - "bg-linear-to-tl/oklab", - "bg-linear-to-tl/oklch", - "bg-linear-to-tl/srgb", - "bg-linear-to-tl/hsl", - "bg-linear-to-tl/longer", - "bg-linear-to-tl/shorter", - "bg-linear-to-tl/increasing", - "bg-linear-to-tl/decreasing", "bg-linear-to-tr", - "bg-linear-to-tr/oklab", - "bg-linear-to-tr/oklch", - "bg-linear-to-tr/srgb", - "bg-linear-to-tr/hsl", - "bg-linear-to-tr/longer", - "bg-linear-to-tr/shorter", - "bg-linear-to-tr/increasing", - "bg-linear-to-tr/decreasing", "bg-local", "bg-no-repeat", "bg-none", "bg-origin-border", "bg-origin-content", "bg-origin-padding", - "bg-radial", - "bg-radial/oklab", - "bg-radial/oklch", - "bg-radial/srgb", - "bg-radial/hsl", - "bg-radial/longer", - "bg-radial/shorter", - "bg-radial/increasing", - "bg-radial/decreasing", "bg-repeat", - "bg-repeat-round", - "bg-repeat-space", "bg-repeat-x", "bg-repeat-y", "bg-right", + "bg-right-bottom", + "bg-right-top", + "bg-round", "bg-scroll", + "bg-space", "bg-top", - "bg-top-left", - "bg-top-right", "bg-transparent", "bg-transparent/0", "bg-transparent/5", @@ -2846,79 +643,7 @@ exports[`getClassList 1`] = ` "bg-transparent/90", "bg-transparent/95", "bg-transparent/100", - "bg-transparent/background", "block", - "block-0", - "block-0.5", - "block-1", - "block-1.5", - "block-2", - "block-2.5", - "block-3", - "block-3.5", - "block-4", - "block-5", - "block-6", - "block-7", - "block-8", - "block-9", - "block-10", - "block-11", - "block-12", - "block-14", - "block-16", - "block-20", - "block-24", - "block-28", - "block-32", - "block-36", - "block-40", - "block-44", - "block-48", - "block-52", - "block-56", - "block-60", - "block-64", - "block-72", - "block-80", - "block-96", - "block-1/2", - "block-1/3", - "block-1/4", - "block-1/5", - "block-1/6", - "block-1/12", - "block-2/3", - "block-2/4", - "block-2/5", - "block-2/6", - "block-2/12", - "block-3/4", - "block-3/5", - "block-3/6", - "block-3/12", - "block-4/5", - "block-4/6", - "block-4/12", - "block-5/6", - "block-5/12", - "block-6/12", - "block-7/12", - "block-8/12", - "block-9/12", - "block-10/12", - "block-11/12", - "block-auto", - "block-dvh", - "block-fit", - "block-full", - "block-lh", - "block-lvh", - "block-max", - "block-min", - "block-px", - "block-screen", - "block-svh", "blur-none", "border", "border/0", @@ -2942,7 +667,6 @@ exports[`getClassList 1`] = ` "border/90", "border/95", "border/100", - "border/background", "border-0", "border-2", "border-4", @@ -2969,7 +693,6 @@ exports[`getClassList 1`] = ` "border-b/90", "border-b/95", "border-b/100", - "border-b/background", "border-b-0", "border-b-2", "border-b-4", @@ -2996,7 +719,6 @@ exports[`getClassList 1`] = ` "border-b-current/90", "border-b-current/95", "border-b-current/100", - "border-b-current/background", "border-b-inherit", "border-b-inherit/0", "border-b-inherit/5", @@ -3019,7 +741,6 @@ exports[`getClassList 1`] = ` "border-b-inherit/90", "border-b-inherit/95", "border-b-inherit/100", - "border-b-inherit/background", "border-b-transparent", "border-b-transparent/0", "border-b-transparent/5", @@ -3042,199 +763,6 @@ exports[`getClassList 1`] = ` "border-b-transparent/90", "border-b-transparent/95", "border-b-transparent/100", - "border-b-transparent/background", - "border-be", - "border-be/0", - "border-be/5", - "border-be/10", - "border-be/15", - "border-be/20", - "border-be/25", - "border-be/30", - "border-be/35", - "border-be/40", - "border-be/45", - "border-be/50", - "border-be/55", - "border-be/60", - "border-be/65", - "border-be/70", - "border-be/75", - "border-be/80", - "border-be/85", - "border-be/90", - "border-be/95", - "border-be/100", - "border-be/background", - "border-be-0", - "border-be-2", - "border-be-4", - "border-be-8", - "border-be-current", - "border-be-current/0", - "border-be-current/5", - "border-be-current/10", - "border-be-current/15", - "border-be-current/20", - "border-be-current/25", - "border-be-current/30", - "border-be-current/35", - "border-be-current/40", - "border-be-current/45", - "border-be-current/50", - "border-be-current/55", - "border-be-current/60", - "border-be-current/65", - "border-be-current/70", - "border-be-current/75", - "border-be-current/80", - "border-be-current/85", - "border-be-current/90", - "border-be-current/95", - "border-be-current/100", - "border-be-current/background", - "border-be-inherit", - "border-be-inherit/0", - "border-be-inherit/5", - "border-be-inherit/10", - "border-be-inherit/15", - "border-be-inherit/20", - "border-be-inherit/25", - "border-be-inherit/30", - "border-be-inherit/35", - "border-be-inherit/40", - "border-be-inherit/45", - "border-be-inherit/50", - "border-be-inherit/55", - "border-be-inherit/60", - "border-be-inherit/65", - "border-be-inherit/70", - "border-be-inherit/75", - "border-be-inherit/80", - "border-be-inherit/85", - "border-be-inherit/90", - "border-be-inherit/95", - "border-be-inherit/100", - "border-be-inherit/background", - "border-be-transparent", - "border-be-transparent/0", - "border-be-transparent/5", - "border-be-transparent/10", - "border-be-transparent/15", - "border-be-transparent/20", - "border-be-transparent/25", - "border-be-transparent/30", - "border-be-transparent/35", - "border-be-transparent/40", - "border-be-transparent/45", - "border-be-transparent/50", - "border-be-transparent/55", - "border-be-transparent/60", - "border-be-transparent/65", - "border-be-transparent/70", - "border-be-transparent/75", - "border-be-transparent/80", - "border-be-transparent/85", - "border-be-transparent/90", - "border-be-transparent/95", - "border-be-transparent/100", - "border-be-transparent/background", - "border-bs", - "border-bs/0", - "border-bs/5", - "border-bs/10", - "border-bs/15", - "border-bs/20", - "border-bs/25", - "border-bs/30", - "border-bs/35", - "border-bs/40", - "border-bs/45", - "border-bs/50", - "border-bs/55", - "border-bs/60", - "border-bs/65", - "border-bs/70", - "border-bs/75", - "border-bs/80", - "border-bs/85", - "border-bs/90", - "border-bs/95", - "border-bs/100", - "border-bs/background", - "border-bs-0", - "border-bs-2", - "border-bs-4", - "border-bs-8", - "border-bs-current", - "border-bs-current/0", - "border-bs-current/5", - "border-bs-current/10", - "border-bs-current/15", - "border-bs-current/20", - "border-bs-current/25", - "border-bs-current/30", - "border-bs-current/35", - "border-bs-current/40", - "border-bs-current/45", - "border-bs-current/50", - "border-bs-current/55", - "border-bs-current/60", - "border-bs-current/65", - "border-bs-current/70", - "border-bs-current/75", - "border-bs-current/80", - "border-bs-current/85", - "border-bs-current/90", - "border-bs-current/95", - "border-bs-current/100", - "border-bs-current/background", - "border-bs-inherit", - "border-bs-inherit/0", - "border-bs-inherit/5", - "border-bs-inherit/10", - "border-bs-inherit/15", - "border-bs-inherit/20", - "border-bs-inherit/25", - "border-bs-inherit/30", - "border-bs-inherit/35", - "border-bs-inherit/40", - "border-bs-inherit/45", - "border-bs-inherit/50", - "border-bs-inherit/55", - "border-bs-inherit/60", - "border-bs-inherit/65", - "border-bs-inherit/70", - "border-bs-inherit/75", - "border-bs-inherit/80", - "border-bs-inherit/85", - "border-bs-inherit/90", - "border-bs-inherit/95", - "border-bs-inherit/100", - "border-bs-inherit/background", - "border-bs-transparent", - "border-bs-transparent/0", - "border-bs-transparent/5", - "border-bs-transparent/10", - "border-bs-transparent/15", - "border-bs-transparent/20", - "border-bs-transparent/25", - "border-bs-transparent/30", - "border-bs-transparent/35", - "border-bs-transparent/40", - "border-bs-transparent/45", - "border-bs-transparent/50", - "border-bs-transparent/55", - "border-bs-transparent/60", - "border-bs-transparent/65", - "border-bs-transparent/70", - "border-bs-transparent/75", - "border-bs-transparent/80", - "border-bs-transparent/85", - "border-bs-transparent/90", - "border-bs-transparent/95", - "border-bs-transparent/100", - "border-bs-transparent/background", "border-collapse", "border-current", "border-current/0", @@ -3258,7 +786,6 @@ exports[`getClassList 1`] = ` "border-current/90", "border-current/95", "border-current/100", - "border-current/background", "border-dashed", "border-dotted", "border-double", @@ -3284,7 +811,6 @@ exports[`getClassList 1`] = ` "border-e/90", "border-e/95", "border-e/100", - "border-e/background", "border-e-0", "border-e-2", "border-e-4", @@ -3311,7 +837,6 @@ exports[`getClassList 1`] = ` "border-e-current/90", "border-e-current/95", "border-e-current/100", - "border-e-current/background", "border-e-inherit", "border-e-inherit/0", "border-e-inherit/5", @@ -3334,7 +859,6 @@ exports[`getClassList 1`] = ` "border-e-inherit/90", "border-e-inherit/95", "border-e-inherit/100", - "border-e-inherit/background", "border-e-transparent", "border-e-transparent/0", "border-e-transparent/5", @@ -3357,7 +881,6 @@ exports[`getClassList 1`] = ` "border-e-transparent/90", "border-e-transparent/95", "border-e-transparent/100", - "border-e-transparent/background", "border-hidden", "border-inherit", "border-inherit/0", @@ -3381,7 +904,6 @@ exports[`getClassList 1`] = ` "border-inherit/90", "border-inherit/95", "border-inherit/100", - "border-inherit/background", "border-l", "border-l/0", "border-l/5", @@ -3404,7 +926,6 @@ exports[`getClassList 1`] = ` "border-l/90", "border-l/95", "border-l/100", - "border-l/background", "border-l-0", "border-l-2", "border-l-4", @@ -3431,7 +952,6 @@ exports[`getClassList 1`] = ` "border-l-current/90", "border-l-current/95", "border-l-current/100", - "border-l-current/background", "border-l-inherit", "border-l-inherit/0", "border-l-inherit/5", @@ -3454,7 +974,6 @@ exports[`getClassList 1`] = ` "border-l-inherit/90", "border-l-inherit/95", "border-l-inherit/100", - "border-l-inherit/background", "border-l-transparent", "border-l-transparent/0", "border-l-transparent/5", @@ -3477,7 +996,6 @@ exports[`getClassList 1`] = ` "border-l-transparent/90", "border-l-transparent/95", "border-l-transparent/100", - "border-l-transparent/background", "border-none", "border-r", "border-r/0", @@ -3501,7 +1019,6 @@ exports[`getClassList 1`] = ` "border-r/90", "border-r/95", "border-r/100", - "border-r/background", "border-r-0", "border-r-2", "border-r-4", @@ -3528,7 +1045,6 @@ exports[`getClassList 1`] = ` "border-r-current/90", "border-r-current/95", "border-r-current/100", - "border-r-current/background", "border-r-inherit", "border-r-inherit/0", "border-r-inherit/5", @@ -3551,7 +1067,6 @@ exports[`getClassList 1`] = ` "border-r-inherit/90", "border-r-inherit/95", "border-r-inherit/100", - "border-r-inherit/background", "border-r-transparent", "border-r-transparent/0", "border-r-transparent/5", @@ -3574,7 +1089,6 @@ exports[`getClassList 1`] = ` "border-r-transparent/90", "border-r-transparent/95", "border-r-transparent/100", - "border-r-transparent/background", "border-s", "border-s/0", "border-s/5", @@ -3597,7 +1111,6 @@ exports[`getClassList 1`] = ` "border-s/90", "border-s/95", "border-s/100", - "border-s/background", "border-s-0", "border-s-2", "border-s-4", @@ -3624,7 +1137,6 @@ exports[`getClassList 1`] = ` "border-s-current/90", "border-s-current/95", "border-s-current/100", - "border-s-current/background", "border-s-inherit", "border-s-inherit/0", "border-s-inherit/5", @@ -3647,7 +1159,6 @@ exports[`getClassList 1`] = ` "border-s-inherit/90", "border-s-inherit/95", "border-s-inherit/100", - "border-s-inherit/background", "border-s-transparent", "border-s-transparent/0", "border-s-transparent/5", @@ -3670,114 +1181,20 @@ 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", "border-spacing-0.5", "border-spacing-1", - "border-spacing-1.5", - "border-spacing-2", - "border-spacing-2.5", "border-spacing-3", - "border-spacing-3.5", "border-spacing-4", - "border-spacing-5", - "border-spacing-6", - "border-spacing-7", - "border-spacing-8", - "border-spacing-9", - "border-spacing-10", - "border-spacing-11", - "border-spacing-12", - "border-spacing-14", - "border-spacing-16", - "border-spacing-20", - "border-spacing-24", - "border-spacing-28", - "border-spacing-32", - "border-spacing-36", - "border-spacing-40", - "border-spacing-44", - "border-spacing-48", - "border-spacing-52", - "border-spacing-56", - "border-spacing-60", - "border-spacing-64", - "border-spacing-72", - "border-spacing-80", - "border-spacing-96", - "border-spacing-px", - "border-spacing-x-0", "border-spacing-x-0.5", "border-spacing-x-1", - "border-spacing-x-1.5", - "border-spacing-x-2", - "border-spacing-x-2.5", "border-spacing-x-3", - "border-spacing-x-3.5", "border-spacing-x-4", - "border-spacing-x-5", - "border-spacing-x-6", - "border-spacing-x-7", - "border-spacing-x-8", - "border-spacing-x-9", - "border-spacing-x-10", - "border-spacing-x-11", - "border-spacing-x-12", - "border-spacing-x-14", - "border-spacing-x-16", - "border-spacing-x-20", - "border-spacing-x-24", - "border-spacing-x-28", - "border-spacing-x-32", - "border-spacing-x-36", - "border-spacing-x-40", - "border-spacing-x-44", - "border-spacing-x-48", - "border-spacing-x-52", - "border-spacing-x-56", - "border-spacing-x-60", - "border-spacing-x-64", - "border-spacing-x-72", - "border-spacing-x-80", - "border-spacing-x-96", - "border-spacing-x-px", - "border-spacing-y-0", "border-spacing-y-0.5", "border-spacing-y-1", - "border-spacing-y-1.5", - "border-spacing-y-2", - "border-spacing-y-2.5", "border-spacing-y-3", - "border-spacing-y-3.5", "border-spacing-y-4", - "border-spacing-y-5", - "border-spacing-y-6", - "border-spacing-y-7", - "border-spacing-y-8", - "border-spacing-y-9", - "border-spacing-y-10", - "border-spacing-y-11", - "border-spacing-y-12", - "border-spacing-y-14", - "border-spacing-y-16", - "border-spacing-y-20", - "border-spacing-y-24", - "border-spacing-y-28", - "border-spacing-y-32", - "border-spacing-y-36", - "border-spacing-y-40", - "border-spacing-y-44", - "border-spacing-y-48", - "border-spacing-y-52", - "border-spacing-y-56", - "border-spacing-y-60", - "border-spacing-y-64", - "border-spacing-y-72", - "border-spacing-y-80", - "border-spacing-y-96", - "border-spacing-y-px", "border-t", "border-t/0", "border-t/5", @@ -3800,7 +1217,6 @@ exports[`getClassList 1`] = ` "border-t/90", "border-t/95", "border-t/100", - "border-t/background", "border-t-0", "border-t-2", "border-t-4", @@ -3827,7 +1243,6 @@ exports[`getClassList 1`] = ` "border-t-current/90", "border-t-current/95", "border-t-current/100", - "border-t-current/background", "border-t-inherit", "border-t-inherit/0", "border-t-inherit/5", @@ -3850,7 +1265,6 @@ exports[`getClassList 1`] = ` "border-t-inherit/90", "border-t-inherit/95", "border-t-inherit/100", - "border-t-inherit/background", "border-t-transparent", "border-t-transparent/0", "border-t-transparent/5", @@ -3873,7 +1287,6 @@ exports[`getClassList 1`] = ` "border-t-transparent/90", "border-t-transparent/95", "border-t-transparent/100", - "border-t-transparent/background", "border-transparent", "border-transparent/0", "border-transparent/5", @@ -3896,7 +1309,6 @@ exports[`getClassList 1`] = ` "border-transparent/90", "border-transparent/95", "border-transparent/100", - "border-transparent/background", "border-x", "border-x/0", "border-x/5", @@ -3919,7 +1331,6 @@ exports[`getClassList 1`] = ` "border-x/90", "border-x/95", "border-x/100", - "border-x/background", "border-x-0", "border-x-2", "border-x-4", @@ -3946,7 +1357,6 @@ exports[`getClassList 1`] = ` "border-x-current/90", "border-x-current/95", "border-x-current/100", - "border-x-current/background", "border-x-inherit", "border-x-inherit/0", "border-x-inherit/5", @@ -3969,7 +1379,6 @@ exports[`getClassList 1`] = ` "border-x-inherit/90", "border-x-inherit/95", "border-x-inherit/100", - "border-x-inherit/background", "border-x-transparent", "border-x-transparent/0", "border-x-transparent/5", @@ -3992,7 +1401,6 @@ exports[`getClassList 1`] = ` "border-x-transparent/90", "border-x-transparent/95", "border-x-transparent/100", - "border-x-transparent/background", "border-y", "border-y/0", "border-y/5", @@ -4015,7 +1423,6 @@ exports[`getClassList 1`] = ` "border-y/90", "border-y/95", "border-y/100", - "border-y/background", "border-y-0", "border-y-2", "border-y-4", @@ -4042,7 +1449,6 @@ exports[`getClassList 1`] = ` "border-y-current/90", "border-y-current/95", "border-y-current/100", - "border-y-current/background", "border-y-inherit", "border-y-inherit/0", "border-y-inherit/5", @@ -4065,7 +1471,6 @@ exports[`getClassList 1`] = ` "border-y-inherit/90", "border-y-inherit/95", "border-y-inherit/100", - "border-y-inherit/background", "border-y-transparent", "border-y-transparent/0", "border-y-transparent/5", @@ -4088,70 +1493,12 @@ 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", - "bottom-1.5", - "bottom-2", - "bottom-2.5", "bottom-3", - "bottom-3.5", "bottom-4", - "bottom-5", - "bottom-6", - "bottom-7", - "bottom-8", - "bottom-9", - "bottom-10", - "bottom-11", - "bottom-12", - "bottom-14", - "bottom-16", - "bottom-20", - "bottom-24", - "bottom-28", - "bottom-32", - "bottom-36", - "bottom-40", - "bottom-44", - "bottom-48", - "bottom-52", - "bottom-56", - "bottom-60", - "bottom-64", - "bottom-72", - "bottom-80", - "bottom-96", - "bottom-1/2", - "bottom-1/3", - "bottom-1/4", - "bottom-1/5", - "bottom-1/6", - "bottom-1/12", - "bottom-2/3", - "bottom-2/4", - "bottom-2/5", - "bottom-2/6", - "bottom-2/12", - "bottom-3/4", - "bottom-3/5", - "bottom-3/6", - "bottom-3/12", - "bottom-4/5", - "bottom-4/6", - "bottom-4/12", - "bottom-5/6", - "bottom-5/12", - "bottom-6/12", - "bottom-7/12", - "bottom-8/12", - "bottom-9/12", - "bottom-10/12", - "bottom-11/12", "bottom-auto", "bottom-full", - "bottom-px", "box-border", "box-content", "box-decoration-clone", @@ -4179,17 +1526,18 @@ exports[`getClassList 1`] = ` "break-inside-avoid-page", "break-keep", "break-normal", + "break-words", "brightness-0", - "brightness-50", - "brightness-75", - "brightness-90", - "brightness-95", "brightness-100", "brightness-105", "brightness-110", "brightness-125", "brightness-150", "brightness-200", + "brightness-50", + "brightness-75", + "brightness-90", + "brightness-95", "capitalize", "caption-bottom", "caption-top", @@ -4215,7 +1563,6 @@ exports[`getClassList 1`] = ` "caret-current/90", "caret-current/95", "caret-current/100", - "caret-current/background", "caret-inherit", "caret-inherit/0", "caret-inherit/5", @@ -4238,7 +1585,6 @@ exports[`getClassList 1`] = ` "caret-inherit/90", "caret-inherit/95", "caret-inherit/100", - "caret-inherit/background", "caret-transparent", "caret-transparent/0", "caret-transparent/5", @@ -4261,7 +1607,6 @@ exports[`getClassList 1`] = ` "caret-transparent/90", "caret-transparent/95", "caret-transparent/100", - "caret-transparent/background", "clear-both", "clear-end", "clear-left", @@ -4270,6 +1615,10 @@ exports[`getClassList 1`] = ` "clear-start", "col-auto", "col-end-1", + "col-end-10", + "col-end-11", + "col-end-12", + "col-end-13", "col-end-2", "col-end-3", "col-end-4", @@ -4278,12 +1627,11 @@ exports[`getClassList 1`] = ` "col-end-7", "col-end-8", "col-end-9", - "col-end-10", - "col-end-11", - "col-end-12", - "col-end-13", "col-end-auto", "col-span-1", + "col-span-10", + "col-span-11", + "col-span-12", "col-span-2", "col-span-3", "col-span-4", @@ -4292,11 +1640,12 @@ exports[`getClassList 1`] = ` "col-span-7", "col-span-8", "col-span-9", - "col-span-10", - "col-span-11", - "col-span-12", "col-span-full", "col-start-1", + "col-start-10", + "col-start-11", + "col-start-12", + "col-start-13", "col-start-2", "col-start-3", "col-start-4", @@ -4305,27 +1654,22 @@ exports[`getClassList 1`] = ` "col-start-7", "col-start-8", "col-start-9", - "col-start-10", - "col-start-11", - "col-start-12", - "col-start-13", "col-start-auto", "collapse", "columns-1", + "columns-10", + "columns-11", + "columns-12", "columns-2", "columns-3", "columns-4", + "columns-4", "columns-5", "columns-6", "columns-7", "columns-8", "columns-9", - "columns-10", - "columns-11", - "columns-12", "columns-auto", - "columns-lg", - "columns-md", "contain-content", "contain-inline-size", "contain-layout", @@ -4334,14 +1678,11 @@ exports[`getClassList 1`] = ` "contain-size", "contain-strict", "contain-style", - "container", "content-around", "content-baseline", "content-between", "content-center", - "content-center-safe", "content-end", - "content-end-safe", "content-evenly", "content-none", "content-normal", @@ -4349,12 +1690,12 @@ exports[`getClassList 1`] = ` "content-stretch", "contents", "contrast-0", - "contrast-50", - "contrast-75", "contrast-100", "contrast-125", "contrast-150", "contrast-200", + "contrast-50", + "contrast-75", "cursor-alias", "cursor-all-scroll", "cursor-auto", @@ -4417,7 +1758,6 @@ exports[`getClassList 1`] = ` "decoration-current/90", "decoration-current/95", "decoration-current/100", - "decoration-current/background", "decoration-dashed", "decoration-dotted", "decoration-double", @@ -4444,7 +1784,6 @@ exports[`getClassList 1`] = ` "decoration-inherit/90", "decoration-inherit/95", "decoration-inherit/100", - "decoration-inherit/background", "decoration-solid", "decoration-transparent", "decoration-transparent/0", @@ -4468,16 +1807,15 @@ exports[`getClassList 1`] = ` "decoration-transparent/90", "decoration-transparent/95", "decoration-transparent/100", - "decoration-transparent/background", "decoration-wavy", - "delay-75", "delay-100", + "delay-1000", "delay-150", "delay-200", "delay-300", "delay-500", "delay-700", - "delay-1000", + "delay-75", "diagonal-fractions", "divide-current", "divide-current/0", @@ -4501,7 +1839,6 @@ exports[`getClassList 1`] = ` "divide-current/90", "divide-current/95", "divide-current/100", - "divide-current/background", "divide-dashed", "divide-dotted", "divide-double", @@ -4527,7 +1864,6 @@ exports[`getClassList 1`] = ` "divide-inherit/90", "divide-inherit/95", "divide-inherit/100", - "divide-inherit/background", "divide-none", "divide-solid", "divide-transparent", @@ -4552,7 +1888,6 @@ exports[`getClassList 1`] = ` "divide-transparent/90", "divide-transparent/95", "divide-transparent/100", - "divide-transparent/background", "divide-x", "divide-x-0", "divide-x-2", @@ -4565,91 +1900,22 @@ exports[`getClassList 1`] = ` "divide-y-4", "divide-y-8", "divide-y-reverse", - "drop-shadow-current", - "drop-shadow-current/0", - "drop-shadow-current/5", - "drop-shadow-current/10", - "drop-shadow-current/15", - "drop-shadow-current/20", - "drop-shadow-current/25", - "drop-shadow-current/30", - "drop-shadow-current/35", - "drop-shadow-current/40", - "drop-shadow-current/45", - "drop-shadow-current/50", - "drop-shadow-current/55", - "drop-shadow-current/60", - "drop-shadow-current/65", - "drop-shadow-current/70", - "drop-shadow-current/75", - "drop-shadow-current/80", - "drop-shadow-current/85", - "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", - "drop-shadow-inherit/10", - "drop-shadow-inherit/15", - "drop-shadow-inherit/20", - "drop-shadow-inherit/25", - "drop-shadow-inherit/30", - "drop-shadow-inherit/35", - "drop-shadow-inherit/40", - "drop-shadow-inherit/45", - "drop-shadow-inherit/50", - "drop-shadow-inherit/55", - "drop-shadow-inherit/60", - "drop-shadow-inherit/65", - "drop-shadow-inherit/70", - "drop-shadow-inherit/75", - "drop-shadow-inherit/80", - "drop-shadow-inherit/85", - "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", - "drop-shadow-transparent/0", - "drop-shadow-transparent/5", - "drop-shadow-transparent/10", - "drop-shadow-transparent/15", - "drop-shadow-transparent/20", - "drop-shadow-transparent/25", - "drop-shadow-transparent/30", - "drop-shadow-transparent/35", - "drop-shadow-transparent/40", - "drop-shadow-transparent/45", - "drop-shadow-transparent/50", - "drop-shadow-transparent/55", - "drop-shadow-transparent/60", - "drop-shadow-transparent/65", - "drop-shadow-transparent/70", - "drop-shadow-transparent/75", - "drop-shadow-transparent/80", - "drop-shadow-transparent/85", - "drop-shadow-transparent/90", - "drop-shadow-transparent/95", - "drop-shadow-transparent/100", - "drop-shadow-transparent/background", - "duration-75", "duration-100", + "duration-1000", "duration-150", "duration-200", "duration-300", "duration-500", "duration-700", - "duration-1000", + "duration-75", "duration-initial", "ease-initial", - "ease-linear", + "end-0.5", + "end-1", + "end-3", + "end-4", "end-auto", "end-full", - "end-px", "field-sizing-content", "field-sizing-fixed", "fill-current", @@ -4674,7 +1940,6 @@ exports[`getClassList 1`] = ` "fill-current/90", "fill-current/95", "fill-current/100", - "fill-current/background", "fill-inherit", "fill-inherit/0", "fill-inherit/5", @@ -4697,7 +1962,6 @@ exports[`getClassList 1`] = ` "fill-inherit/90", "fill-inherit/95", "fill-inherit/100", - "fill-inherit/background", "fill-none", "fill-transparent", "fill-transparent/0", @@ -4721,47 +1985,7 @@ 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", - "flex-1/5", - "flex-1/6", - "flex-1/12", - "flex-2/3", - "flex-2/4", - "flex-2/5", - "flex-2/6", - "flex-2/12", - "flex-3/4", - "flex-3/5", - "flex-3/6", - "flex-3/12", - "flex-4/5", - "flex-4/6", - "flex-4/12", - "flex-5/6", - "flex-5/12", - "flex-6/12", - "flex-7/12", - "flex-8/12", - "flex-9/12", - "flex-10/12", - "flex-11/12", "flex-auto", "flex-col", "flex-col-reverse", @@ -4778,18 +2002,24 @@ exports[`getClassList 1`] = ` "float-right", "float-start", "flow-root", + "font-black", "font-bold", - "font-sans", - "font-stretch-50%", - "font-stretch-75%", - "font-stretch-90%", - "font-stretch-95%", + "font-extrabold", + "font-extralight", + "font-light", + "font-medium", + "font-normal", + "font-semibold", "font-stretch-100%", "font-stretch-105%", "font-stretch-110%", "font-stretch-125%", "font-stretch-150%", "font-stretch-200%", + "font-stretch-50%", + "font-stretch-75%", + "font-stretch-90%", + "font-stretch-95%", "font-stretch-condensed", "font-stretch-expanded", "font-stretch-extra-condensed", @@ -4799,12 +2029,12 @@ exports[`getClassList 1`] = ` "font-stretch-semi-expanded", "font-stretch-ultra-condensed", "font-stretch-ultra-expanded", - "font-superbold", + "font-thin", "forced-color-adjust-auto", "forced-color-adjust-none", "from-0%", - "from-5%", "from-10%", + "from-100%", "from-15%", "from-20%", "from-25%", @@ -4812,6 +2042,7 @@ exports[`getClassList 1`] = ` "from-35%", "from-40%", "from-45%", + "from-5%", "from-50%", "from-55%", "from-60%", @@ -4822,7 +2053,6 @@ exports[`getClassList 1`] = ` "from-85%", "from-90%", "from-95%", - "from-100%", "from-current", "from-current/0", "from-current/5", @@ -4845,7 +2075,6 @@ exports[`getClassList 1`] = ` "from-current/90", "from-current/95", "from-current/100", - "from-current/background", "from-inherit", "from-inherit/0", "from-inherit/5", @@ -4868,7 +2097,6 @@ exports[`getClassList 1`] = ` "from-inherit/90", "from-inherit/95", "from-inherit/100", - "from-inherit/background", "from-transparent", "from-transparent/0", "from-transparent/5", @@ -4891,120 +2119,29 @@ exports[`getClassList 1`] = ` "from-transparent/90", "from-transparent/95", "from-transparent/100", - "from-transparent/background", - "gap-0", "gap-0.5", "gap-1", - "gap-1.5", - "gap-2", - "gap-2.5", "gap-3", - "gap-3.5", "gap-4", - "gap-5", - "gap-6", - "gap-7", - "gap-8", - "gap-9", - "gap-10", - "gap-11", - "gap-12", - "gap-14", - "gap-16", - "gap-20", - "gap-24", - "gap-28", - "gap-32", - "gap-36", - "gap-40", - "gap-44", - "gap-48", - "gap-52", - "gap-56", - "gap-60", - "gap-64", - "gap-72", - "gap-80", - "gap-96", - "gap-px", - "gap-x-0", "gap-x-0.5", "gap-x-1", - "gap-x-1.5", - "gap-x-2", - "gap-x-2.5", "gap-x-3", - "gap-x-3.5", "gap-x-4", - "gap-x-5", - "gap-x-6", - "gap-x-7", - "gap-x-8", - "gap-x-9", - "gap-x-10", - "gap-x-11", - "gap-x-12", - "gap-x-14", - "gap-x-16", - "gap-x-20", - "gap-x-24", - "gap-x-28", - "gap-x-32", - "gap-x-36", - "gap-x-40", - "gap-x-44", - "gap-x-48", - "gap-x-52", - "gap-x-56", - "gap-x-60", - "gap-x-64", - "gap-x-72", - "gap-x-80", - "gap-x-96", - "gap-x-px", - "gap-y-0", "gap-y-0.5", "gap-y-1", - "gap-y-1.5", - "gap-y-2", - "gap-y-2.5", "gap-y-3", - "gap-y-3.5", "gap-y-4", - "gap-y-5", - "gap-y-6", - "gap-y-7", - "gap-y-8", - "gap-y-9", - "gap-y-10", - "gap-y-11", - "gap-y-12", - "gap-y-14", - "gap-y-16", - "gap-y-20", - "gap-y-24", - "gap-y-28", - "gap-y-32", - "gap-y-36", - "gap-y-40", - "gap-y-44", - "gap-y-48", - "gap-y-52", - "gap-y-56", - "gap-y-60", - "gap-y-64", - "gap-y-72", - "gap-y-80", - "gap-y-96", - "gap-y-px", "grayscale", "grayscale-0", + "grayscale-100", "grayscale-25", "grayscale-50", "grayscale-75", - "grayscale-100", "grid", "grid-cols-1", + "grid-cols-10", + "grid-cols-11", + "grid-cols-12", "grid-cols-2", "grid-cols-3", "grid-cols-4", @@ -5013,9 +2150,6 @@ exports[`getClassList 1`] = ` "grid-cols-7", "grid-cols-8", "grid-cols-9", - "grid-cols-10", - "grid-cols-11", - "grid-cols-12", "grid-cols-none", "grid-cols-subgrid", "grid-flow-col", @@ -5024,6 +2158,9 @@ exports[`getClassList 1`] = ` "grid-flow-row", "grid-flow-row-dense", "grid-rows-1", + "grid-rows-10", + "grid-rows-11", + "grid-rows-12", "grid-rows-2", "grid-rows-3", "grid-rows-4", @@ -5032,461 +2169,48 @@ exports[`getClassList 1`] = ` "grid-rows-7", "grid-rows-8", "grid-rows-9", - "grid-rows-10", - "grid-rows-11", - "grid-rows-12", "grid-rows-none", "grid-rows-subgrid", "grow", "grow-0", - "h-0", "h-0.5", "h-1", - "h-1.5", - "h-2", - "h-2.5", "h-3", - "h-3.5", "h-4", - "h-5", - "h-6", - "h-7", - "h-8", - "h-9", - "h-10", - "h-11", - "h-12", - "h-14", - "h-16", - "h-20", - "h-24", - "h-28", - "h-32", - "h-36", - "h-40", - "h-44", - "h-48", - "h-52", - "h-56", - "h-60", - "h-64", - "h-72", - "h-80", - "h-96", - "h-1/2", - "h-1/3", - "h-1/4", - "h-1/5", - "h-1/6", - "h-1/12", - "h-2/3", - "h-2/4", - "h-2/5", - "h-2/6", - "h-2/12", - "h-3/4", - "h-3/5", - "h-3/6", - "h-3/12", - "h-4/5", - "h-4/6", - "h-4/12", - "h-5/6", - "h-5/12", - "h-6/12", - "h-7/12", - "h-8/12", - "h-9/12", - "h-10/12", - "h-11/12", "h-auto", "h-dvh", - "h-dvw", "h-fit", "h-full", - "h-lh", "h-lvh", - "h-lvw", "h-max", "h-min", - "h-px", "h-screen", "h-svh", - "h-svw", "hidden", "hue-rotate-0", "hue-rotate-15", + "hue-rotate-180", "hue-rotate-30", "hue-rotate-60", "hue-rotate-90", - "hue-rotate-180", "hyphens-auto", "hyphens-manual", "hyphens-none", - "indent-0", "indent-0.5", "indent-1", - "indent-1.5", - "indent-2", - "indent-2.5", "indent-3", - "indent-3.5", "indent-4", - "indent-5", - "indent-6", - "indent-7", - "indent-8", - "indent-9", - "indent-10", - "indent-11", - "indent-12", - "indent-14", - "indent-16", - "indent-20", - "indent-24", - "indent-28", - "indent-32", - "indent-36", - "indent-40", - "indent-44", - "indent-48", - "indent-52", - "indent-56", - "indent-60", - "indent-64", - "indent-72", - "indent-80", - "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", "inset-1", - "inset-1.5", - "inset-2", - "inset-2.5", "inset-3", - "inset-3.5", "inset-4", - "inset-5", - "inset-6", - "inset-7", - "inset-8", - "inset-9", - "inset-10", - "inset-11", - "inset-12", - "inset-14", - "inset-16", - "inset-20", - "inset-24", - "inset-28", - "inset-32", - "inset-36", - "inset-40", - "inset-44", - "inset-48", - "inset-52", - "inset-56", - "inset-60", - "inset-64", - "inset-72", - "inset-80", - "inset-96", - "inset-1/2", - "inset-1/3", - "inset-1/4", - "inset-1/5", - "inset-1/6", - "inset-1/12", - "inset-2/3", - "inset-2/4", - "inset-2/5", - "inset-2/6", - "inset-2/12", - "inset-3/4", - "inset-3/5", - "inset-3/6", - "inset-3/12", - "inset-4/5", - "inset-4/6", - "inset-4/12", - "inset-5/6", - "inset-5/12", - "inset-6/12", - "inset-7/12", - "inset-8/12", - "inset-9/12", - "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", "inset-ring-0", "inset-ring-1", @@ -5515,7 +2239,6 @@ exports[`getClassList 1`] = ` "inset-ring-current/90", "inset-ring-current/95", "inset-ring-current/100", - "inset-ring-current/background", "inset-ring-inherit", "inset-ring-inherit/0", "inset-ring-inherit/5", @@ -5538,7 +2261,6 @@ exports[`getClassList 1`] = ` "inset-ring-inherit/90", "inset-ring-inherit/95", "inset-ring-inherit/100", - "inset-ring-inherit/background", "inset-ring-transparent", "inset-ring-transparent/0", "inset-ring-transparent/5", @@ -5561,70 +2283,7 @@ exports[`getClassList 1`] = ` "inset-ring-transparent/90", "inset-ring-transparent/95", "inset-ring-transparent/100", - "inset-ring-transparent/background", - "inset-s-0", - "inset-s-0.5", - "inset-s-1", - "inset-s-1.5", - "inset-s-2", - "inset-s-2.5", - "inset-s-3", - "inset-s-3.5", - "inset-s-4", - "inset-s-5", - "inset-s-6", - "inset-s-7", - "inset-s-8", - "inset-s-9", - "inset-s-10", - "inset-s-11", - "inset-s-12", - "inset-s-14", - "inset-s-16", - "inset-s-20", - "inset-s-24", - "inset-s-28", - "inset-s-32", - "inset-s-36", - "inset-s-40", - "inset-s-44", - "inset-s-48", - "inset-s-52", - "inset-s-56", - "inset-s-60", - "inset-s-64", - "inset-s-72", - "inset-s-80", - "inset-s-96", - "inset-s-1/2", - "inset-s-1/3", - "inset-s-1/4", - "inset-s-1/5", - "inset-s-1/6", - "inset-s-1/12", - "inset-s-2/3", - "inset-s-2/4", - "inset-s-2/5", - "inset-s-2/6", - "inset-s-2/12", - "inset-s-3/4", - "inset-s-3/5", - "inset-s-3/6", - "inset-s-3/12", - "inset-s-4/5", - "inset-s-4/6", - "inset-s-4/12", - "inset-s-5/6", - "inset-s-5/12", - "inset-s-6/12", - "inset-s-7/12", - "inset-s-8/12", - "inset-s-9/12", - "inset-s-10/12", - "inset-s-11/12", - "inset-s-auto", - "inset-s-full", - "inset-s-px", + "inset-shadow", "inset-shadow-current", "inset-shadow-current/0", "inset-shadow-current/5", @@ -5647,7 +2306,6 @@ exports[`getClassList 1`] = ` "inset-shadow-current/90", "inset-shadow-current/95", "inset-shadow-current/100", - "inset-shadow-current/background", "inset-shadow-inherit", "inset-shadow-inherit/0", "inset-shadow-inherit/5", @@ -5670,31 +2328,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", - "inset-shadow-sm/0", - "inset-shadow-sm/5", - "inset-shadow-sm/10", - "inset-shadow-sm/15", - "inset-shadow-sm/20", - "inset-shadow-sm/25", - "inset-shadow-sm/30", - "inset-shadow-sm/35", - "inset-shadow-sm/40", - "inset-shadow-sm/45", - "inset-shadow-sm/50", - "inset-shadow-sm/55", - "inset-shadow-sm/60", - "inset-shadow-sm/65", - "inset-shadow-sm/70", - "inset-shadow-sm/75", - "inset-shadow-sm/80", - "inset-shadow-sm/85", - "inset-shadow-sm/90", - "inset-shadow-sm/95", - "inset-shadow-sm/100", "inset-shadow-transparent", "inset-shadow-transparent/0", "inset-shadow-transparent/5", @@ -5717,275 +2351,58 @@ 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", - "inset-x-1.5", - "inset-x-2", - "inset-x-2.5", "inset-x-3", - "inset-x-3.5", "inset-x-4", - "inset-x-5", - "inset-x-6", - "inset-x-7", - "inset-x-8", - "inset-x-9", - "inset-x-10", - "inset-x-11", - "inset-x-12", - "inset-x-14", - "inset-x-16", - "inset-x-20", - "inset-x-24", - "inset-x-28", - "inset-x-32", - "inset-x-36", - "inset-x-40", - "inset-x-44", - "inset-x-48", - "inset-x-52", - "inset-x-56", - "inset-x-60", - "inset-x-64", - "inset-x-72", - "inset-x-80", - "inset-x-96", - "inset-x-1/2", - "inset-x-1/3", - "inset-x-1/4", - "inset-x-1/5", - "inset-x-1/6", - "inset-x-1/12", - "inset-x-2/3", - "inset-x-2/4", - "inset-x-2/5", - "inset-x-2/6", - "inset-x-2/12", - "inset-x-3/4", - "inset-x-3/5", - "inset-x-3/6", - "inset-x-3/12", - "inset-x-4/5", - "inset-x-4/6", - "inset-x-4/12", - "inset-x-5/6", - "inset-x-5/12", - "inset-x-6/12", - "inset-x-7/12", - "inset-x-8/12", - "inset-x-9/12", - "inset-x-10/12", - "inset-x-11/12", "inset-x-auto", "inset-x-full", - "inset-x-px", - "inset-y-0", "inset-y-0.5", "inset-y-1", - "inset-y-1.5", - "inset-y-2", - "inset-y-2.5", "inset-y-3", - "inset-y-3.5", "inset-y-4", - "inset-y-5", - "inset-y-6", - "inset-y-7", - "inset-y-8", - "inset-y-9", - "inset-y-10", - "inset-y-11", - "inset-y-12", - "inset-y-14", - "inset-y-16", - "inset-y-20", - "inset-y-24", - "inset-y-28", - "inset-y-32", - "inset-y-36", - "inset-y-40", - "inset-y-44", - "inset-y-48", - "inset-y-52", - "inset-y-56", - "inset-y-60", - "inset-y-64", - "inset-y-72", - "inset-y-80", - "inset-y-96", - "inset-y-1/2", - "inset-y-1/3", - "inset-y-1/4", - "inset-y-1/5", - "inset-y-1/6", - "inset-y-1/12", - "inset-y-2/3", - "inset-y-2/4", - "inset-y-2/5", - "inset-y-2/6", - "inset-y-2/12", - "inset-y-3/4", - "inset-y-3/5", - "inset-y-3/6", - "inset-y-3/12", - "inset-y-4/5", - "inset-y-4/6", - "inset-y-4/12", - "inset-y-5/6", - "inset-y-5/12", - "inset-y-6/12", - "inset-y-7/12", - "inset-y-8/12", - "inset-y-9/12", - "inset-y-10/12", - "inset-y-11/12", "inset-y-auto", "inset-y-full", - "inset-y-px", "invert", "invert-0", + "invert-100", "invert-25", "invert-50", "invert-75", - "invert-100", "invisible", "isolate", "isolation-auto", "italic", "items-baseline", - "items-baseline-last", "items-center", - "items-center-safe", "items-end", - "items-end-safe", "items-start", "items-stretch", "justify-around", "justify-baseline", "justify-between", "justify-center", - "justify-center-safe", "justify-end", - "justify-end-safe", "justify-evenly", "justify-items-center", - "justify-items-center-safe", "justify-items-end", - "justify-items-end-safe", "justify-items-normal", "justify-items-start", "justify-items-stretch", "justify-normal", "justify-self-auto", "justify-self-center", - "justify-self-center-safe", "justify-self-end", - "justify-self-end-safe", "justify-self-start", "justify-self-stretch", "justify-start", "justify-stretch", - "leading-0", - "leading-0.5", - "leading-1", - "leading-1.5", - "leading-2", - "leading-2.5", - "leading-3", - "leading-3.5", - "leading-4", - "leading-5", - "leading-6", - "leading-7", - "leading-8", - "leading-9", - "leading-10", - "leading-11", - "leading-12", - "leading-14", - "leading-16", - "leading-20", - "leading-24", - "leading-28", - "leading-32", - "leading-36", - "leading-40", - "leading-44", - "leading-48", - "leading-52", - "leading-56", - "leading-60", - "leading-64", - "leading-72", - "leading-80", - "leading-96", - "leading-none", - "leading-px", - "left-0", "left-0.5", "left-1", - "left-1.5", - "left-2", - "left-2.5", "left-3", - "left-3.5", "left-4", - "left-5", - "left-6", - "left-7", - "left-8", - "left-9", - "left-10", - "left-11", - "left-12", - "left-14", - "left-16", - "left-20", - "left-24", - "left-28", - "left-32", - "left-36", - "left-40", - "left-44", - "left-48", - "left-52", - "left-56", - "left-60", - "left-64", - "left-72", - "left-80", - "left-96", - "left-1/2", - "left-1/3", - "left-1/4", - "left-1/5", - "left-1/6", - "left-1/12", - "left-2/3", - "left-2/4", - "left-2/5", - "left-2/6", - "left-2/12", - "left-3/4", - "left-3/5", - "left-3/6", - "left-3/12", - "left-4/5", - "left-4/6", - "left-4/12", - "left-5/6", - "left-5/12", - "left-6/12", - "left-7/12", - "left-8/12", - "left-9/12", - "left-10/12", - "left-11/12", "left-auto", "left-full", - "left-px", "line-clamp-1", "line-clamp-2", "line-clamp-3", @@ -6003,3075 +2420,67 @@ exports[`getClassList 1`] = ` "list-none", "list-outside", "lowercase", - "m-0", "m-0.5", "m-1", - "m-1.5", - "m-2", - "m-2.5", "m-3", - "m-3.5", "m-4", - "m-5", - "m-6", - "m-7", - "m-8", - "m-9", - "m-10", - "m-11", - "m-12", - "m-14", - "m-16", - "m-20", - "m-24", - "m-28", - "m-32", - "m-36", - "m-40", - "m-44", - "m-48", - "m-52", - "m-56", - "m-60", - "m-64", - "m-72", - "m-80", - "m-96", "m-auto", - "m-px", - "mask-add", - "mask-alpha", - "mask-auto", - "mask-b-from-0", - "mask-b-from-0%", - "mask-b-from-0.5", - "mask-b-from-1", - "mask-b-from-1.5", - "mask-b-from-2", - "mask-b-from-2.5", - "mask-b-from-3", - "mask-b-from-3.5", - "mask-b-from-4", - "mask-b-from-5", - "mask-b-from-5%", - "mask-b-from-6", - "mask-b-from-7", - "mask-b-from-8", - "mask-b-from-9", - "mask-b-from-10", - "mask-b-from-10%", - "mask-b-from-11", - "mask-b-from-12", - "mask-b-from-14", - "mask-b-from-15%", - "mask-b-from-16", - "mask-b-from-20", - "mask-b-from-20%", - "mask-b-from-24", - "mask-b-from-25%", - "mask-b-from-28", - "mask-b-from-30%", - "mask-b-from-32", - "mask-b-from-35%", - "mask-b-from-36", - "mask-b-from-40", - "mask-b-from-40%", - "mask-b-from-44", - "mask-b-from-45%", - "mask-b-from-48", - "mask-b-from-50%", - "mask-b-from-52", - "mask-b-from-55%", - "mask-b-from-56", - "mask-b-from-60", - "mask-b-from-60%", - "mask-b-from-64", - "mask-b-from-65%", - "mask-b-from-70%", - "mask-b-from-72", - "mask-b-from-75%", - "mask-b-from-80", - "mask-b-from-80%", - "mask-b-from-85%", - "mask-b-from-90%", - "mask-b-from-95%", - "mask-b-from-96", - "mask-b-from-100%", - "mask-b-from-current", - "mask-b-from-current/0", - "mask-b-from-current/5", - "mask-b-from-current/10", - "mask-b-from-current/15", - "mask-b-from-current/20", - "mask-b-from-current/25", - "mask-b-from-current/30", - "mask-b-from-current/35", - "mask-b-from-current/40", - "mask-b-from-current/45", - "mask-b-from-current/50", - "mask-b-from-current/55", - "mask-b-from-current/60", - "mask-b-from-current/65", - "mask-b-from-current/70", - "mask-b-from-current/75", - "mask-b-from-current/80", - "mask-b-from-current/85", - "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", - "mask-b-from-inherit/10", - "mask-b-from-inherit/15", - "mask-b-from-inherit/20", - "mask-b-from-inherit/25", - "mask-b-from-inherit/30", - "mask-b-from-inherit/35", - "mask-b-from-inherit/40", - "mask-b-from-inherit/45", - "mask-b-from-inherit/50", - "mask-b-from-inherit/55", - "mask-b-from-inherit/60", - "mask-b-from-inherit/65", - "mask-b-from-inherit/70", - "mask-b-from-inherit/75", - "mask-b-from-inherit/80", - "mask-b-from-inherit/85", - "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", - "mask-b-from-transparent/10", - "mask-b-from-transparent/15", - "mask-b-from-transparent/20", - "mask-b-from-transparent/25", - "mask-b-from-transparent/30", - "mask-b-from-transparent/35", - "mask-b-from-transparent/40", - "mask-b-from-transparent/45", - "mask-b-from-transparent/50", - "mask-b-from-transparent/55", - "mask-b-from-transparent/60", - "mask-b-from-transparent/65", - "mask-b-from-transparent/70", - "mask-b-from-transparent/75", - "mask-b-from-transparent/80", - "mask-b-from-transparent/85", - "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", - "mask-b-to-1", - "mask-b-to-1.5", - "mask-b-to-2", - "mask-b-to-2.5", - "mask-b-to-3", - "mask-b-to-3.5", - "mask-b-to-4", - "mask-b-to-5", - "mask-b-to-5%", - "mask-b-to-6", - "mask-b-to-7", - "mask-b-to-8", - "mask-b-to-9", - "mask-b-to-10", - "mask-b-to-10%", - "mask-b-to-11", - "mask-b-to-12", - "mask-b-to-14", - "mask-b-to-15%", - "mask-b-to-16", - "mask-b-to-20", - "mask-b-to-20%", - "mask-b-to-24", - "mask-b-to-25%", - "mask-b-to-28", - "mask-b-to-30%", - "mask-b-to-32", - "mask-b-to-35%", - "mask-b-to-36", - "mask-b-to-40", - "mask-b-to-40%", - "mask-b-to-44", - "mask-b-to-45%", - "mask-b-to-48", - "mask-b-to-50%", - "mask-b-to-52", - "mask-b-to-55%", - "mask-b-to-56", - "mask-b-to-60", - "mask-b-to-60%", - "mask-b-to-64", - "mask-b-to-65%", - "mask-b-to-70%", - "mask-b-to-72", - "mask-b-to-75%", - "mask-b-to-80", - "mask-b-to-80%", - "mask-b-to-85%", - "mask-b-to-90%", - "mask-b-to-95%", - "mask-b-to-96", - "mask-b-to-100%", - "mask-b-to-current", - "mask-b-to-current/0", - "mask-b-to-current/5", - "mask-b-to-current/10", - "mask-b-to-current/15", - "mask-b-to-current/20", - "mask-b-to-current/25", - "mask-b-to-current/30", - "mask-b-to-current/35", - "mask-b-to-current/40", - "mask-b-to-current/45", - "mask-b-to-current/50", - "mask-b-to-current/55", - "mask-b-to-current/60", - "mask-b-to-current/65", - "mask-b-to-current/70", - "mask-b-to-current/75", - "mask-b-to-current/80", - "mask-b-to-current/85", - "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", - "mask-b-to-inherit/10", - "mask-b-to-inherit/15", - "mask-b-to-inherit/20", - "mask-b-to-inherit/25", - "mask-b-to-inherit/30", - "mask-b-to-inherit/35", - "mask-b-to-inherit/40", - "mask-b-to-inherit/45", - "mask-b-to-inherit/50", - "mask-b-to-inherit/55", - "mask-b-to-inherit/60", - "mask-b-to-inherit/65", - "mask-b-to-inherit/70", - "mask-b-to-inherit/75", - "mask-b-to-inherit/80", - "mask-b-to-inherit/85", - "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", - "mask-b-to-transparent/10", - "mask-b-to-transparent/15", - "mask-b-to-transparent/20", - "mask-b-to-transparent/25", - "mask-b-to-transparent/30", - "mask-b-to-transparent/35", - "mask-b-to-transparent/40", - "mask-b-to-transparent/45", - "mask-b-to-transparent/50", - "mask-b-to-transparent/55", - "mask-b-to-transparent/60", - "mask-b-to-transparent/65", - "mask-b-to-transparent/70", - "mask-b-to-transparent/75", - "mask-b-to-transparent/80", - "mask-b-to-transparent/85", - "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", - "mask-center", - "mask-circle", - "mask-clip-border", - "mask-clip-content", - "mask-clip-fill", - "mask-clip-padding", - "mask-clip-stroke", - "mask-clip-view", - "mask-conic-0", - "mask-conic-1", - "mask-conic-2", - "mask-conic-3", - "mask-conic-6", - "mask-conic-12", - "mask-conic-45", - "mask-conic-90", - "mask-conic-180", - "mask-conic-from-0", - "mask-conic-from-0%", - "mask-conic-from-0.5", - "mask-conic-from-1", - "mask-conic-from-1.5", - "mask-conic-from-2", - "mask-conic-from-2.5", - "mask-conic-from-3", - "mask-conic-from-3.5", - "mask-conic-from-4", - "mask-conic-from-5", - "mask-conic-from-5%", - "mask-conic-from-6", - "mask-conic-from-7", - "mask-conic-from-8", - "mask-conic-from-9", - "mask-conic-from-10", - "mask-conic-from-10%", - "mask-conic-from-11", - "mask-conic-from-12", - "mask-conic-from-14", - "mask-conic-from-15%", - "mask-conic-from-16", - "mask-conic-from-20", - "mask-conic-from-20%", - "mask-conic-from-24", - "mask-conic-from-25%", - "mask-conic-from-28", - "mask-conic-from-30%", - "mask-conic-from-32", - "mask-conic-from-35%", - "mask-conic-from-36", - "mask-conic-from-40", - "mask-conic-from-40%", - "mask-conic-from-44", - "mask-conic-from-45%", - "mask-conic-from-48", - "mask-conic-from-50%", - "mask-conic-from-52", - "mask-conic-from-55%", - "mask-conic-from-56", - "mask-conic-from-60", - "mask-conic-from-60%", - "mask-conic-from-64", - "mask-conic-from-65%", - "mask-conic-from-70%", - "mask-conic-from-72", - "mask-conic-from-75%", - "mask-conic-from-80", - "mask-conic-from-80%", - "mask-conic-from-85%", - "mask-conic-from-90%", - "mask-conic-from-95%", - "mask-conic-from-96", - "mask-conic-from-100%", - "mask-conic-from-current", - "mask-conic-from-current/0", - "mask-conic-from-current/5", - "mask-conic-from-current/10", - "mask-conic-from-current/15", - "mask-conic-from-current/20", - "mask-conic-from-current/25", - "mask-conic-from-current/30", - "mask-conic-from-current/35", - "mask-conic-from-current/40", - "mask-conic-from-current/45", - "mask-conic-from-current/50", - "mask-conic-from-current/55", - "mask-conic-from-current/60", - "mask-conic-from-current/65", - "mask-conic-from-current/70", - "mask-conic-from-current/75", - "mask-conic-from-current/80", - "mask-conic-from-current/85", - "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", - "mask-conic-from-inherit/10", - "mask-conic-from-inherit/15", - "mask-conic-from-inherit/20", - "mask-conic-from-inherit/25", - "mask-conic-from-inherit/30", - "mask-conic-from-inherit/35", - "mask-conic-from-inherit/40", - "mask-conic-from-inherit/45", - "mask-conic-from-inherit/50", - "mask-conic-from-inherit/55", - "mask-conic-from-inherit/60", - "mask-conic-from-inherit/65", - "mask-conic-from-inherit/70", - "mask-conic-from-inherit/75", - "mask-conic-from-inherit/80", - "mask-conic-from-inherit/85", - "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", - "mask-conic-from-transparent/10", - "mask-conic-from-transparent/15", - "mask-conic-from-transparent/20", - "mask-conic-from-transparent/25", - "mask-conic-from-transparent/30", - "mask-conic-from-transparent/35", - "mask-conic-from-transparent/40", - "mask-conic-from-transparent/45", - "mask-conic-from-transparent/50", - "mask-conic-from-transparent/55", - "mask-conic-from-transparent/60", - "mask-conic-from-transparent/65", - "mask-conic-from-transparent/70", - "mask-conic-from-transparent/75", - "mask-conic-from-transparent/80", - "mask-conic-from-transparent/85", - "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", - "mask-conic-to-1", - "mask-conic-to-1.5", - "mask-conic-to-2", - "mask-conic-to-2.5", - "mask-conic-to-3", - "mask-conic-to-3.5", - "mask-conic-to-4", - "mask-conic-to-5", - "mask-conic-to-5%", - "mask-conic-to-6", - "mask-conic-to-7", - "mask-conic-to-8", - "mask-conic-to-9", - "mask-conic-to-10", - "mask-conic-to-10%", - "mask-conic-to-11", - "mask-conic-to-12", - "mask-conic-to-14", - "mask-conic-to-15%", - "mask-conic-to-16", - "mask-conic-to-20", - "mask-conic-to-20%", - "mask-conic-to-24", - "mask-conic-to-25%", - "mask-conic-to-28", - "mask-conic-to-30%", - "mask-conic-to-32", - "mask-conic-to-35%", - "mask-conic-to-36", - "mask-conic-to-40", - "mask-conic-to-40%", - "mask-conic-to-44", - "mask-conic-to-45%", - "mask-conic-to-48", - "mask-conic-to-50%", - "mask-conic-to-52", - "mask-conic-to-55%", - "mask-conic-to-56", - "mask-conic-to-60", - "mask-conic-to-60%", - "mask-conic-to-64", - "mask-conic-to-65%", - "mask-conic-to-70%", - "mask-conic-to-72", - "mask-conic-to-75%", - "mask-conic-to-80", - "mask-conic-to-80%", - "mask-conic-to-85%", - "mask-conic-to-90%", - "mask-conic-to-95%", - "mask-conic-to-96", - "mask-conic-to-100%", - "mask-conic-to-current", - "mask-conic-to-current/0", - "mask-conic-to-current/5", - "mask-conic-to-current/10", - "mask-conic-to-current/15", - "mask-conic-to-current/20", - "mask-conic-to-current/25", - "mask-conic-to-current/30", - "mask-conic-to-current/35", - "mask-conic-to-current/40", - "mask-conic-to-current/45", - "mask-conic-to-current/50", - "mask-conic-to-current/55", - "mask-conic-to-current/60", - "mask-conic-to-current/65", - "mask-conic-to-current/70", - "mask-conic-to-current/75", - "mask-conic-to-current/80", - "mask-conic-to-current/85", - "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", - "mask-conic-to-inherit/10", - "mask-conic-to-inherit/15", - "mask-conic-to-inherit/20", - "mask-conic-to-inherit/25", - "mask-conic-to-inherit/30", - "mask-conic-to-inherit/35", - "mask-conic-to-inherit/40", - "mask-conic-to-inherit/45", - "mask-conic-to-inherit/50", - "mask-conic-to-inherit/55", - "mask-conic-to-inherit/60", - "mask-conic-to-inherit/65", - "mask-conic-to-inherit/70", - "mask-conic-to-inherit/75", - "mask-conic-to-inherit/80", - "mask-conic-to-inherit/85", - "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", - "mask-conic-to-transparent/10", - "mask-conic-to-transparent/15", - "mask-conic-to-transparent/20", - "mask-conic-to-transparent/25", - "mask-conic-to-transparent/30", - "mask-conic-to-transparent/35", - "mask-conic-to-transparent/40", - "mask-conic-to-transparent/45", - "mask-conic-to-transparent/50", - "mask-conic-to-transparent/55", - "mask-conic-to-transparent/60", - "mask-conic-to-transparent/65", - "mask-conic-to-transparent/70", - "mask-conic-to-transparent/75", - "mask-conic-to-transparent/80", - "mask-conic-to-transparent/85", - "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", - "mask-exclude", - "mask-intersect", - "mask-l-from-0", - "mask-l-from-0%", - "mask-l-from-0.5", - "mask-l-from-1", - "mask-l-from-1.5", - "mask-l-from-2", - "mask-l-from-2.5", - "mask-l-from-3", - "mask-l-from-3.5", - "mask-l-from-4", - "mask-l-from-5", - "mask-l-from-5%", - "mask-l-from-6", - "mask-l-from-7", - "mask-l-from-8", - "mask-l-from-9", - "mask-l-from-10", - "mask-l-from-10%", - "mask-l-from-11", - "mask-l-from-12", - "mask-l-from-14", - "mask-l-from-15%", - "mask-l-from-16", - "mask-l-from-20", - "mask-l-from-20%", - "mask-l-from-24", - "mask-l-from-25%", - "mask-l-from-28", - "mask-l-from-30%", - "mask-l-from-32", - "mask-l-from-35%", - "mask-l-from-36", - "mask-l-from-40", - "mask-l-from-40%", - "mask-l-from-44", - "mask-l-from-45%", - "mask-l-from-48", - "mask-l-from-50%", - "mask-l-from-52", - "mask-l-from-55%", - "mask-l-from-56", - "mask-l-from-60", - "mask-l-from-60%", - "mask-l-from-64", - "mask-l-from-65%", - "mask-l-from-70%", - "mask-l-from-72", - "mask-l-from-75%", - "mask-l-from-80", - "mask-l-from-80%", - "mask-l-from-85%", - "mask-l-from-90%", - "mask-l-from-95%", - "mask-l-from-96", - "mask-l-from-100%", - "mask-l-from-current", - "mask-l-from-current/0", - "mask-l-from-current/5", - "mask-l-from-current/10", - "mask-l-from-current/15", - "mask-l-from-current/20", - "mask-l-from-current/25", - "mask-l-from-current/30", - "mask-l-from-current/35", - "mask-l-from-current/40", - "mask-l-from-current/45", - "mask-l-from-current/50", - "mask-l-from-current/55", - "mask-l-from-current/60", - "mask-l-from-current/65", - "mask-l-from-current/70", - "mask-l-from-current/75", - "mask-l-from-current/80", - "mask-l-from-current/85", - "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", - "mask-l-from-inherit/10", - "mask-l-from-inherit/15", - "mask-l-from-inherit/20", - "mask-l-from-inherit/25", - "mask-l-from-inherit/30", - "mask-l-from-inherit/35", - "mask-l-from-inherit/40", - "mask-l-from-inherit/45", - "mask-l-from-inherit/50", - "mask-l-from-inherit/55", - "mask-l-from-inherit/60", - "mask-l-from-inherit/65", - "mask-l-from-inherit/70", - "mask-l-from-inherit/75", - "mask-l-from-inherit/80", - "mask-l-from-inherit/85", - "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", - "mask-l-from-transparent/10", - "mask-l-from-transparent/15", - "mask-l-from-transparent/20", - "mask-l-from-transparent/25", - "mask-l-from-transparent/30", - "mask-l-from-transparent/35", - "mask-l-from-transparent/40", - "mask-l-from-transparent/45", - "mask-l-from-transparent/50", - "mask-l-from-transparent/55", - "mask-l-from-transparent/60", - "mask-l-from-transparent/65", - "mask-l-from-transparent/70", - "mask-l-from-transparent/75", - "mask-l-from-transparent/80", - "mask-l-from-transparent/85", - "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", - "mask-l-to-1", - "mask-l-to-1.5", - "mask-l-to-2", - "mask-l-to-2.5", - "mask-l-to-3", - "mask-l-to-3.5", - "mask-l-to-4", - "mask-l-to-5", - "mask-l-to-5%", - "mask-l-to-6", - "mask-l-to-7", - "mask-l-to-8", - "mask-l-to-9", - "mask-l-to-10", - "mask-l-to-10%", - "mask-l-to-11", - "mask-l-to-12", - "mask-l-to-14", - "mask-l-to-15%", - "mask-l-to-16", - "mask-l-to-20", - "mask-l-to-20%", - "mask-l-to-24", - "mask-l-to-25%", - "mask-l-to-28", - "mask-l-to-30%", - "mask-l-to-32", - "mask-l-to-35%", - "mask-l-to-36", - "mask-l-to-40", - "mask-l-to-40%", - "mask-l-to-44", - "mask-l-to-45%", - "mask-l-to-48", - "mask-l-to-50%", - "mask-l-to-52", - "mask-l-to-55%", - "mask-l-to-56", - "mask-l-to-60", - "mask-l-to-60%", - "mask-l-to-64", - "mask-l-to-65%", - "mask-l-to-70%", - "mask-l-to-72", - "mask-l-to-75%", - "mask-l-to-80", - "mask-l-to-80%", - "mask-l-to-85%", - "mask-l-to-90%", - "mask-l-to-95%", - "mask-l-to-96", - "mask-l-to-100%", - "mask-l-to-current", - "mask-l-to-current/0", - "mask-l-to-current/5", - "mask-l-to-current/10", - "mask-l-to-current/15", - "mask-l-to-current/20", - "mask-l-to-current/25", - "mask-l-to-current/30", - "mask-l-to-current/35", - "mask-l-to-current/40", - "mask-l-to-current/45", - "mask-l-to-current/50", - "mask-l-to-current/55", - "mask-l-to-current/60", - "mask-l-to-current/65", - "mask-l-to-current/70", - "mask-l-to-current/75", - "mask-l-to-current/80", - "mask-l-to-current/85", - "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", - "mask-l-to-inherit/10", - "mask-l-to-inherit/15", - "mask-l-to-inherit/20", - "mask-l-to-inherit/25", - "mask-l-to-inherit/30", - "mask-l-to-inherit/35", - "mask-l-to-inherit/40", - "mask-l-to-inherit/45", - "mask-l-to-inherit/50", - "mask-l-to-inherit/55", - "mask-l-to-inherit/60", - "mask-l-to-inherit/65", - "mask-l-to-inherit/70", - "mask-l-to-inherit/75", - "mask-l-to-inherit/80", - "mask-l-to-inherit/85", - "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", - "mask-l-to-transparent/10", - "mask-l-to-transparent/15", - "mask-l-to-transparent/20", - "mask-l-to-transparent/25", - "mask-l-to-transparent/30", - "mask-l-to-transparent/35", - "mask-l-to-transparent/40", - "mask-l-to-transparent/45", - "mask-l-to-transparent/50", - "mask-l-to-transparent/55", - "mask-l-to-transparent/60", - "mask-l-to-transparent/65", - "mask-l-to-transparent/70", - "mask-l-to-transparent/75", - "mask-l-to-transparent/80", - "mask-l-to-transparent/85", - "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", - "mask-linear-2", - "mask-linear-3", - "mask-linear-6", - "mask-linear-12", - "mask-linear-45", - "mask-linear-90", - "mask-linear-180", - "mask-linear-from-0", - "mask-linear-from-0%", - "mask-linear-from-0.5", - "mask-linear-from-1", - "mask-linear-from-1.5", - "mask-linear-from-2", - "mask-linear-from-2.5", - "mask-linear-from-3", - "mask-linear-from-3.5", - "mask-linear-from-4", - "mask-linear-from-5", - "mask-linear-from-5%", - "mask-linear-from-6", - "mask-linear-from-7", - "mask-linear-from-8", - "mask-linear-from-9", - "mask-linear-from-10", - "mask-linear-from-10%", - "mask-linear-from-11", - "mask-linear-from-12", - "mask-linear-from-14", - "mask-linear-from-15%", - "mask-linear-from-16", - "mask-linear-from-20", - "mask-linear-from-20%", - "mask-linear-from-24", - "mask-linear-from-25%", - "mask-linear-from-28", - "mask-linear-from-30%", - "mask-linear-from-32", - "mask-linear-from-35%", - "mask-linear-from-36", - "mask-linear-from-40", - "mask-linear-from-40%", - "mask-linear-from-44", - "mask-linear-from-45%", - "mask-linear-from-48", - "mask-linear-from-50%", - "mask-linear-from-52", - "mask-linear-from-55%", - "mask-linear-from-56", - "mask-linear-from-60", - "mask-linear-from-60%", - "mask-linear-from-64", - "mask-linear-from-65%", - "mask-linear-from-70%", - "mask-linear-from-72", - "mask-linear-from-75%", - "mask-linear-from-80", - "mask-linear-from-80%", - "mask-linear-from-85%", - "mask-linear-from-90%", - "mask-linear-from-95%", - "mask-linear-from-96", - "mask-linear-from-100%", - "mask-linear-from-current", - "mask-linear-from-current/0", - "mask-linear-from-current/5", - "mask-linear-from-current/10", - "mask-linear-from-current/15", - "mask-linear-from-current/20", - "mask-linear-from-current/25", - "mask-linear-from-current/30", - "mask-linear-from-current/35", - "mask-linear-from-current/40", - "mask-linear-from-current/45", - "mask-linear-from-current/50", - "mask-linear-from-current/55", - "mask-linear-from-current/60", - "mask-linear-from-current/65", - "mask-linear-from-current/70", - "mask-linear-from-current/75", - "mask-linear-from-current/80", - "mask-linear-from-current/85", - "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", - "mask-linear-from-inherit/10", - "mask-linear-from-inherit/15", - "mask-linear-from-inherit/20", - "mask-linear-from-inherit/25", - "mask-linear-from-inherit/30", - "mask-linear-from-inherit/35", - "mask-linear-from-inherit/40", - "mask-linear-from-inherit/45", - "mask-linear-from-inherit/50", - "mask-linear-from-inherit/55", - "mask-linear-from-inherit/60", - "mask-linear-from-inherit/65", - "mask-linear-from-inherit/70", - "mask-linear-from-inherit/75", - "mask-linear-from-inherit/80", - "mask-linear-from-inherit/85", - "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", - "mask-linear-from-transparent/10", - "mask-linear-from-transparent/15", - "mask-linear-from-transparent/20", - "mask-linear-from-transparent/25", - "mask-linear-from-transparent/30", - "mask-linear-from-transparent/35", - "mask-linear-from-transparent/40", - "mask-linear-from-transparent/45", - "mask-linear-from-transparent/50", - "mask-linear-from-transparent/55", - "mask-linear-from-transparent/60", - "mask-linear-from-transparent/65", - "mask-linear-from-transparent/70", - "mask-linear-from-transparent/75", - "mask-linear-from-transparent/80", - "mask-linear-from-transparent/85", - "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", - "mask-linear-to-1", - "mask-linear-to-1.5", - "mask-linear-to-2", - "mask-linear-to-2.5", - "mask-linear-to-3", - "mask-linear-to-3.5", - "mask-linear-to-4", - "mask-linear-to-5", - "mask-linear-to-5%", - "mask-linear-to-6", - "mask-linear-to-7", - "mask-linear-to-8", - "mask-linear-to-9", - "mask-linear-to-10", - "mask-linear-to-10%", - "mask-linear-to-11", - "mask-linear-to-12", - "mask-linear-to-14", - "mask-linear-to-15%", - "mask-linear-to-16", - "mask-linear-to-20", - "mask-linear-to-20%", - "mask-linear-to-24", - "mask-linear-to-25%", - "mask-linear-to-28", - "mask-linear-to-30%", - "mask-linear-to-32", - "mask-linear-to-35%", - "mask-linear-to-36", - "mask-linear-to-40", - "mask-linear-to-40%", - "mask-linear-to-44", - "mask-linear-to-45%", - "mask-linear-to-48", - "mask-linear-to-50%", - "mask-linear-to-52", - "mask-linear-to-55%", - "mask-linear-to-56", - "mask-linear-to-60", - "mask-linear-to-60%", - "mask-linear-to-64", - "mask-linear-to-65%", - "mask-linear-to-70%", - "mask-linear-to-72", - "mask-linear-to-75%", - "mask-linear-to-80", - "mask-linear-to-80%", - "mask-linear-to-85%", - "mask-linear-to-90%", - "mask-linear-to-95%", - "mask-linear-to-96", - "mask-linear-to-100%", - "mask-linear-to-current", - "mask-linear-to-current/0", - "mask-linear-to-current/5", - "mask-linear-to-current/10", - "mask-linear-to-current/15", - "mask-linear-to-current/20", - "mask-linear-to-current/25", - "mask-linear-to-current/30", - "mask-linear-to-current/35", - "mask-linear-to-current/40", - "mask-linear-to-current/45", - "mask-linear-to-current/50", - "mask-linear-to-current/55", - "mask-linear-to-current/60", - "mask-linear-to-current/65", - "mask-linear-to-current/70", - "mask-linear-to-current/75", - "mask-linear-to-current/80", - "mask-linear-to-current/85", - "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", - "mask-linear-to-inherit/10", - "mask-linear-to-inherit/15", - "mask-linear-to-inherit/20", - "mask-linear-to-inherit/25", - "mask-linear-to-inherit/30", - "mask-linear-to-inherit/35", - "mask-linear-to-inherit/40", - "mask-linear-to-inherit/45", - "mask-linear-to-inherit/50", - "mask-linear-to-inherit/55", - "mask-linear-to-inherit/60", - "mask-linear-to-inherit/65", - "mask-linear-to-inherit/70", - "mask-linear-to-inherit/75", - "mask-linear-to-inherit/80", - "mask-linear-to-inherit/85", - "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", - "mask-linear-to-transparent/10", - "mask-linear-to-transparent/15", - "mask-linear-to-transparent/20", - "mask-linear-to-transparent/25", - "mask-linear-to-transparent/30", - "mask-linear-to-transparent/35", - "mask-linear-to-transparent/40", - "mask-linear-to-transparent/45", - "mask-linear-to-transparent/50", - "mask-linear-to-transparent/55", - "mask-linear-to-transparent/60", - "mask-linear-to-transparent/65", - "mask-linear-to-transparent/70", - "mask-linear-to-transparent/75", - "mask-linear-to-transparent/80", - "mask-linear-to-transparent/85", - "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", - "mask-no-repeat", - "mask-none", - "mask-origin-border", - "mask-origin-content", - "mask-origin-fill", - "mask-origin-padding", - "mask-origin-stroke", - "mask-origin-view", - "mask-r-from-0", - "mask-r-from-0%", - "mask-r-from-0.5", - "mask-r-from-1", - "mask-r-from-1.5", - "mask-r-from-2", - "mask-r-from-2.5", - "mask-r-from-3", - "mask-r-from-3.5", - "mask-r-from-4", - "mask-r-from-5", - "mask-r-from-5%", - "mask-r-from-6", - "mask-r-from-7", - "mask-r-from-8", - "mask-r-from-9", - "mask-r-from-10", - "mask-r-from-10%", - "mask-r-from-11", - "mask-r-from-12", - "mask-r-from-14", - "mask-r-from-15%", - "mask-r-from-16", - "mask-r-from-20", - "mask-r-from-20%", - "mask-r-from-24", - "mask-r-from-25%", - "mask-r-from-28", - "mask-r-from-30%", - "mask-r-from-32", - "mask-r-from-35%", - "mask-r-from-36", - "mask-r-from-40", - "mask-r-from-40%", - "mask-r-from-44", - "mask-r-from-45%", - "mask-r-from-48", - "mask-r-from-50%", - "mask-r-from-52", - "mask-r-from-55%", - "mask-r-from-56", - "mask-r-from-60", - "mask-r-from-60%", - "mask-r-from-64", - "mask-r-from-65%", - "mask-r-from-70%", - "mask-r-from-72", - "mask-r-from-75%", - "mask-r-from-80", - "mask-r-from-80%", - "mask-r-from-85%", - "mask-r-from-90%", - "mask-r-from-95%", - "mask-r-from-96", - "mask-r-from-100%", - "mask-r-from-current", - "mask-r-from-current/0", - "mask-r-from-current/5", - "mask-r-from-current/10", - "mask-r-from-current/15", - "mask-r-from-current/20", - "mask-r-from-current/25", - "mask-r-from-current/30", - "mask-r-from-current/35", - "mask-r-from-current/40", - "mask-r-from-current/45", - "mask-r-from-current/50", - "mask-r-from-current/55", - "mask-r-from-current/60", - "mask-r-from-current/65", - "mask-r-from-current/70", - "mask-r-from-current/75", - "mask-r-from-current/80", - "mask-r-from-current/85", - "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", - "mask-r-from-inherit/10", - "mask-r-from-inherit/15", - "mask-r-from-inherit/20", - "mask-r-from-inherit/25", - "mask-r-from-inherit/30", - "mask-r-from-inherit/35", - "mask-r-from-inherit/40", - "mask-r-from-inherit/45", - "mask-r-from-inherit/50", - "mask-r-from-inherit/55", - "mask-r-from-inherit/60", - "mask-r-from-inherit/65", - "mask-r-from-inherit/70", - "mask-r-from-inherit/75", - "mask-r-from-inherit/80", - "mask-r-from-inherit/85", - "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", - "mask-r-from-transparent/10", - "mask-r-from-transparent/15", - "mask-r-from-transparent/20", - "mask-r-from-transparent/25", - "mask-r-from-transparent/30", - "mask-r-from-transparent/35", - "mask-r-from-transparent/40", - "mask-r-from-transparent/45", - "mask-r-from-transparent/50", - "mask-r-from-transparent/55", - "mask-r-from-transparent/60", - "mask-r-from-transparent/65", - "mask-r-from-transparent/70", - "mask-r-from-transparent/75", - "mask-r-from-transparent/80", - "mask-r-from-transparent/85", - "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", - "mask-r-to-1", - "mask-r-to-1.5", - "mask-r-to-2", - "mask-r-to-2.5", - "mask-r-to-3", - "mask-r-to-3.5", - "mask-r-to-4", - "mask-r-to-5", - "mask-r-to-5%", - "mask-r-to-6", - "mask-r-to-7", - "mask-r-to-8", - "mask-r-to-9", - "mask-r-to-10", - "mask-r-to-10%", - "mask-r-to-11", - "mask-r-to-12", - "mask-r-to-14", - "mask-r-to-15%", - "mask-r-to-16", - "mask-r-to-20", - "mask-r-to-20%", - "mask-r-to-24", - "mask-r-to-25%", - "mask-r-to-28", - "mask-r-to-30%", - "mask-r-to-32", - "mask-r-to-35%", - "mask-r-to-36", - "mask-r-to-40", - "mask-r-to-40%", - "mask-r-to-44", - "mask-r-to-45%", - "mask-r-to-48", - "mask-r-to-50%", - "mask-r-to-52", - "mask-r-to-55%", - "mask-r-to-56", - "mask-r-to-60", - "mask-r-to-60%", - "mask-r-to-64", - "mask-r-to-65%", - "mask-r-to-70%", - "mask-r-to-72", - "mask-r-to-75%", - "mask-r-to-80", - "mask-r-to-80%", - "mask-r-to-85%", - "mask-r-to-90%", - "mask-r-to-95%", - "mask-r-to-96", - "mask-r-to-100%", - "mask-r-to-current", - "mask-r-to-current/0", - "mask-r-to-current/5", - "mask-r-to-current/10", - "mask-r-to-current/15", - "mask-r-to-current/20", - "mask-r-to-current/25", - "mask-r-to-current/30", - "mask-r-to-current/35", - "mask-r-to-current/40", - "mask-r-to-current/45", - "mask-r-to-current/50", - "mask-r-to-current/55", - "mask-r-to-current/60", - "mask-r-to-current/65", - "mask-r-to-current/70", - "mask-r-to-current/75", - "mask-r-to-current/80", - "mask-r-to-current/85", - "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", - "mask-r-to-inherit/10", - "mask-r-to-inherit/15", - "mask-r-to-inherit/20", - "mask-r-to-inherit/25", - "mask-r-to-inherit/30", - "mask-r-to-inherit/35", - "mask-r-to-inherit/40", - "mask-r-to-inherit/45", - "mask-r-to-inherit/50", - "mask-r-to-inherit/55", - "mask-r-to-inherit/60", - "mask-r-to-inherit/65", - "mask-r-to-inherit/70", - "mask-r-to-inherit/75", - "mask-r-to-inherit/80", - "mask-r-to-inherit/85", - "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", - "mask-r-to-transparent/10", - "mask-r-to-transparent/15", - "mask-r-to-transparent/20", - "mask-r-to-transparent/25", - "mask-r-to-transparent/30", - "mask-r-to-transparent/35", - "mask-r-to-transparent/40", - "mask-r-to-transparent/45", - "mask-r-to-transparent/50", - "mask-r-to-transparent/55", - "mask-r-to-transparent/60", - "mask-r-to-transparent/65", - "mask-r-to-transparent/70", - "mask-r-to-transparent/75", - "mask-r-to-transparent/80", - "mask-r-to-transparent/85", - "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", - "mask-radial-at-center", - "mask-radial-at-left", - "mask-radial-at-right", - "mask-radial-at-top", - "mask-radial-at-top-left", - "mask-radial-at-top-right", - "mask-radial-closest-corner", - "mask-radial-closest-side", - "mask-radial-farthest-corner", - "mask-radial-farthest-side", - "mask-radial-from-0", - "mask-radial-from-0%", - "mask-radial-from-0.5", - "mask-radial-from-1", - "mask-radial-from-1.5", - "mask-radial-from-2", - "mask-radial-from-2.5", - "mask-radial-from-3", - "mask-radial-from-3.5", - "mask-radial-from-4", - "mask-radial-from-5", - "mask-radial-from-5%", - "mask-radial-from-6", - "mask-radial-from-7", - "mask-radial-from-8", - "mask-radial-from-9", - "mask-radial-from-10", - "mask-radial-from-10%", - "mask-radial-from-11", - "mask-radial-from-12", - "mask-radial-from-14", - "mask-radial-from-15%", - "mask-radial-from-16", - "mask-radial-from-20", - "mask-radial-from-20%", - "mask-radial-from-24", - "mask-radial-from-25%", - "mask-radial-from-28", - "mask-radial-from-30%", - "mask-radial-from-32", - "mask-radial-from-35%", - "mask-radial-from-36", - "mask-radial-from-40", - "mask-radial-from-40%", - "mask-radial-from-44", - "mask-radial-from-45%", - "mask-radial-from-48", - "mask-radial-from-50%", - "mask-radial-from-52", - "mask-radial-from-55%", - "mask-radial-from-56", - "mask-radial-from-60", - "mask-radial-from-60%", - "mask-radial-from-64", - "mask-radial-from-65%", - "mask-radial-from-70%", - "mask-radial-from-72", - "mask-radial-from-75%", - "mask-radial-from-80", - "mask-radial-from-80%", - "mask-radial-from-85%", - "mask-radial-from-90%", - "mask-radial-from-95%", - "mask-radial-from-96", - "mask-radial-from-100%", - "mask-radial-from-current", - "mask-radial-from-current/0", - "mask-radial-from-current/5", - "mask-radial-from-current/10", - "mask-radial-from-current/15", - "mask-radial-from-current/20", - "mask-radial-from-current/25", - "mask-radial-from-current/30", - "mask-radial-from-current/35", - "mask-radial-from-current/40", - "mask-radial-from-current/45", - "mask-radial-from-current/50", - "mask-radial-from-current/55", - "mask-radial-from-current/60", - "mask-radial-from-current/65", - "mask-radial-from-current/70", - "mask-radial-from-current/75", - "mask-radial-from-current/80", - "mask-radial-from-current/85", - "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", - "mask-radial-from-inherit/10", - "mask-radial-from-inherit/15", - "mask-radial-from-inherit/20", - "mask-radial-from-inherit/25", - "mask-radial-from-inherit/30", - "mask-radial-from-inherit/35", - "mask-radial-from-inherit/40", - "mask-radial-from-inherit/45", - "mask-radial-from-inherit/50", - "mask-radial-from-inherit/55", - "mask-radial-from-inherit/60", - "mask-radial-from-inherit/65", - "mask-radial-from-inherit/70", - "mask-radial-from-inherit/75", - "mask-radial-from-inherit/80", - "mask-radial-from-inherit/85", - "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", - "mask-radial-from-transparent/10", - "mask-radial-from-transparent/15", - "mask-radial-from-transparent/20", - "mask-radial-from-transparent/25", - "mask-radial-from-transparent/30", - "mask-radial-from-transparent/35", - "mask-radial-from-transparent/40", - "mask-radial-from-transparent/45", - "mask-radial-from-transparent/50", - "mask-radial-from-transparent/55", - "mask-radial-from-transparent/60", - "mask-radial-from-transparent/65", - "mask-radial-from-transparent/70", - "mask-radial-from-transparent/75", - "mask-radial-from-transparent/80", - "mask-radial-from-transparent/85", - "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", - "mask-radial-to-1", - "mask-radial-to-1.5", - "mask-radial-to-2", - "mask-radial-to-2.5", - "mask-radial-to-3", - "mask-radial-to-3.5", - "mask-radial-to-4", - "mask-radial-to-5", - "mask-radial-to-5%", - "mask-radial-to-6", - "mask-radial-to-7", - "mask-radial-to-8", - "mask-radial-to-9", - "mask-radial-to-10", - "mask-radial-to-10%", - "mask-radial-to-11", - "mask-radial-to-12", - "mask-radial-to-14", - "mask-radial-to-15%", - "mask-radial-to-16", - "mask-radial-to-20", - "mask-radial-to-20%", - "mask-radial-to-24", - "mask-radial-to-25%", - "mask-radial-to-28", - "mask-radial-to-30%", - "mask-radial-to-32", - "mask-radial-to-35%", - "mask-radial-to-36", - "mask-radial-to-40", - "mask-radial-to-40%", - "mask-radial-to-44", - "mask-radial-to-45%", - "mask-radial-to-48", - "mask-radial-to-50%", - "mask-radial-to-52", - "mask-radial-to-55%", - "mask-radial-to-56", - "mask-radial-to-60", - "mask-radial-to-60%", - "mask-radial-to-64", - "mask-radial-to-65%", - "mask-radial-to-70%", - "mask-radial-to-72", - "mask-radial-to-75%", - "mask-radial-to-80", - "mask-radial-to-80%", - "mask-radial-to-85%", - "mask-radial-to-90%", - "mask-radial-to-95%", - "mask-radial-to-96", - "mask-radial-to-100%", - "mask-radial-to-current", - "mask-radial-to-current/0", - "mask-radial-to-current/5", - "mask-radial-to-current/10", - "mask-radial-to-current/15", - "mask-radial-to-current/20", - "mask-radial-to-current/25", - "mask-radial-to-current/30", - "mask-radial-to-current/35", - "mask-radial-to-current/40", - "mask-radial-to-current/45", - "mask-radial-to-current/50", - "mask-radial-to-current/55", - "mask-radial-to-current/60", - "mask-radial-to-current/65", - "mask-radial-to-current/70", - "mask-radial-to-current/75", - "mask-radial-to-current/80", - "mask-radial-to-current/85", - "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", - "mask-radial-to-inherit/10", - "mask-radial-to-inherit/15", - "mask-radial-to-inherit/20", - "mask-radial-to-inherit/25", - "mask-radial-to-inherit/30", - "mask-radial-to-inherit/35", - "mask-radial-to-inherit/40", - "mask-radial-to-inherit/45", - "mask-radial-to-inherit/50", - "mask-radial-to-inherit/55", - "mask-radial-to-inherit/60", - "mask-radial-to-inherit/65", - "mask-radial-to-inherit/70", - "mask-radial-to-inherit/75", - "mask-radial-to-inherit/80", - "mask-radial-to-inherit/85", - "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", - "mask-radial-to-transparent/10", - "mask-radial-to-transparent/15", - "mask-radial-to-transparent/20", - "mask-radial-to-transparent/25", - "mask-radial-to-transparent/30", - "mask-radial-to-transparent/35", - "mask-radial-to-transparent/40", - "mask-radial-to-transparent/45", - "mask-radial-to-transparent/50", - "mask-radial-to-transparent/55", - "mask-radial-to-transparent/60", - "mask-radial-to-transparent/65", - "mask-radial-to-transparent/70", - "mask-radial-to-transparent/75", - "mask-radial-to-transparent/80", - "mask-radial-to-transparent/85", - "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", - "mask-repeat-x", - "mask-repeat-y", - "mask-right", - "mask-subtract", - "mask-t-from-0", - "mask-t-from-0%", - "mask-t-from-0.5", - "mask-t-from-1", - "mask-t-from-1.5", - "mask-t-from-2", - "mask-t-from-2.5", - "mask-t-from-3", - "mask-t-from-3.5", - "mask-t-from-4", - "mask-t-from-5", - "mask-t-from-5%", - "mask-t-from-6", - "mask-t-from-7", - "mask-t-from-8", - "mask-t-from-9", - "mask-t-from-10", - "mask-t-from-10%", - "mask-t-from-11", - "mask-t-from-12", - "mask-t-from-14", - "mask-t-from-15%", - "mask-t-from-16", - "mask-t-from-20", - "mask-t-from-20%", - "mask-t-from-24", - "mask-t-from-25%", - "mask-t-from-28", - "mask-t-from-30%", - "mask-t-from-32", - "mask-t-from-35%", - "mask-t-from-36", - "mask-t-from-40", - "mask-t-from-40%", - "mask-t-from-44", - "mask-t-from-45%", - "mask-t-from-48", - "mask-t-from-50%", - "mask-t-from-52", - "mask-t-from-55%", - "mask-t-from-56", - "mask-t-from-60", - "mask-t-from-60%", - "mask-t-from-64", - "mask-t-from-65%", - "mask-t-from-70%", - "mask-t-from-72", - "mask-t-from-75%", - "mask-t-from-80", - "mask-t-from-80%", - "mask-t-from-85%", - "mask-t-from-90%", - "mask-t-from-95%", - "mask-t-from-96", - "mask-t-from-100%", - "mask-t-from-current", - "mask-t-from-current/0", - "mask-t-from-current/5", - "mask-t-from-current/10", - "mask-t-from-current/15", - "mask-t-from-current/20", - "mask-t-from-current/25", - "mask-t-from-current/30", - "mask-t-from-current/35", - "mask-t-from-current/40", - "mask-t-from-current/45", - "mask-t-from-current/50", - "mask-t-from-current/55", - "mask-t-from-current/60", - "mask-t-from-current/65", - "mask-t-from-current/70", - "mask-t-from-current/75", - "mask-t-from-current/80", - "mask-t-from-current/85", - "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", - "mask-t-from-inherit/10", - "mask-t-from-inherit/15", - "mask-t-from-inherit/20", - "mask-t-from-inherit/25", - "mask-t-from-inherit/30", - "mask-t-from-inherit/35", - "mask-t-from-inherit/40", - "mask-t-from-inherit/45", - "mask-t-from-inherit/50", - "mask-t-from-inherit/55", - "mask-t-from-inherit/60", - "mask-t-from-inherit/65", - "mask-t-from-inherit/70", - "mask-t-from-inherit/75", - "mask-t-from-inherit/80", - "mask-t-from-inherit/85", - "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", - "mask-t-from-transparent/10", - "mask-t-from-transparent/15", - "mask-t-from-transparent/20", - "mask-t-from-transparent/25", - "mask-t-from-transparent/30", - "mask-t-from-transparent/35", - "mask-t-from-transparent/40", - "mask-t-from-transparent/45", - "mask-t-from-transparent/50", - "mask-t-from-transparent/55", - "mask-t-from-transparent/60", - "mask-t-from-transparent/65", - "mask-t-from-transparent/70", - "mask-t-from-transparent/75", - "mask-t-from-transparent/80", - "mask-t-from-transparent/85", - "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", - "mask-t-to-1", - "mask-t-to-1.5", - "mask-t-to-2", - "mask-t-to-2.5", - "mask-t-to-3", - "mask-t-to-3.5", - "mask-t-to-4", - "mask-t-to-5", - "mask-t-to-5%", - "mask-t-to-6", - "mask-t-to-7", - "mask-t-to-8", - "mask-t-to-9", - "mask-t-to-10", - "mask-t-to-10%", - "mask-t-to-11", - "mask-t-to-12", - "mask-t-to-14", - "mask-t-to-15%", - "mask-t-to-16", - "mask-t-to-20", - "mask-t-to-20%", - "mask-t-to-24", - "mask-t-to-25%", - "mask-t-to-28", - "mask-t-to-30%", - "mask-t-to-32", - "mask-t-to-35%", - "mask-t-to-36", - "mask-t-to-40", - "mask-t-to-40%", - "mask-t-to-44", - "mask-t-to-45%", - "mask-t-to-48", - "mask-t-to-50%", - "mask-t-to-52", - "mask-t-to-55%", - "mask-t-to-56", - "mask-t-to-60", - "mask-t-to-60%", - "mask-t-to-64", - "mask-t-to-65%", - "mask-t-to-70%", - "mask-t-to-72", - "mask-t-to-75%", - "mask-t-to-80", - "mask-t-to-80%", - "mask-t-to-85%", - "mask-t-to-90%", - "mask-t-to-95%", - "mask-t-to-96", - "mask-t-to-100%", - "mask-t-to-current", - "mask-t-to-current/0", - "mask-t-to-current/5", - "mask-t-to-current/10", - "mask-t-to-current/15", - "mask-t-to-current/20", - "mask-t-to-current/25", - "mask-t-to-current/30", - "mask-t-to-current/35", - "mask-t-to-current/40", - "mask-t-to-current/45", - "mask-t-to-current/50", - "mask-t-to-current/55", - "mask-t-to-current/60", - "mask-t-to-current/65", - "mask-t-to-current/70", - "mask-t-to-current/75", - "mask-t-to-current/80", - "mask-t-to-current/85", - "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", - "mask-t-to-inherit/10", - "mask-t-to-inherit/15", - "mask-t-to-inherit/20", - "mask-t-to-inherit/25", - "mask-t-to-inherit/30", - "mask-t-to-inherit/35", - "mask-t-to-inherit/40", - "mask-t-to-inherit/45", - "mask-t-to-inherit/50", - "mask-t-to-inherit/55", - "mask-t-to-inherit/60", - "mask-t-to-inherit/65", - "mask-t-to-inherit/70", - "mask-t-to-inherit/75", - "mask-t-to-inherit/80", - "mask-t-to-inherit/85", - "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", - "mask-t-to-transparent/10", - "mask-t-to-transparent/15", - "mask-t-to-transparent/20", - "mask-t-to-transparent/25", - "mask-t-to-transparent/30", - "mask-t-to-transparent/35", - "mask-t-to-transparent/40", - "mask-t-to-transparent/45", - "mask-t-to-transparent/50", - "mask-t-to-transparent/55", - "mask-t-to-transparent/60", - "mask-t-to-transparent/65", - "mask-t-to-transparent/70", - "mask-t-to-transparent/75", - "mask-t-to-transparent/80", - "mask-t-to-transparent/85", - "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", - "mask-type-alpha", - "mask-type-luminance", - "mask-x-from-0", - "mask-x-from-0%", - "mask-x-from-0.5", - "mask-x-from-1", - "mask-x-from-1.5", - "mask-x-from-2", - "mask-x-from-2.5", - "mask-x-from-3", - "mask-x-from-3.5", - "mask-x-from-4", - "mask-x-from-5", - "mask-x-from-5%", - "mask-x-from-6", - "mask-x-from-7", - "mask-x-from-8", - "mask-x-from-9", - "mask-x-from-10", - "mask-x-from-10%", - "mask-x-from-11", - "mask-x-from-12", - "mask-x-from-14", - "mask-x-from-15%", - "mask-x-from-16", - "mask-x-from-20", - "mask-x-from-20%", - "mask-x-from-24", - "mask-x-from-25%", - "mask-x-from-28", - "mask-x-from-30%", - "mask-x-from-32", - "mask-x-from-35%", - "mask-x-from-36", - "mask-x-from-40", - "mask-x-from-40%", - "mask-x-from-44", - "mask-x-from-45%", - "mask-x-from-48", - "mask-x-from-50%", - "mask-x-from-52", - "mask-x-from-55%", - "mask-x-from-56", - "mask-x-from-60", - "mask-x-from-60%", - "mask-x-from-64", - "mask-x-from-65%", - "mask-x-from-70%", - "mask-x-from-72", - "mask-x-from-75%", - "mask-x-from-80", - "mask-x-from-80%", - "mask-x-from-85%", - "mask-x-from-90%", - "mask-x-from-95%", - "mask-x-from-96", - "mask-x-from-100%", - "mask-x-from-current", - "mask-x-from-current/0", - "mask-x-from-current/5", - "mask-x-from-current/10", - "mask-x-from-current/15", - "mask-x-from-current/20", - "mask-x-from-current/25", - "mask-x-from-current/30", - "mask-x-from-current/35", - "mask-x-from-current/40", - "mask-x-from-current/45", - "mask-x-from-current/50", - "mask-x-from-current/55", - "mask-x-from-current/60", - "mask-x-from-current/65", - "mask-x-from-current/70", - "mask-x-from-current/75", - "mask-x-from-current/80", - "mask-x-from-current/85", - "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", - "mask-x-from-inherit/10", - "mask-x-from-inherit/15", - "mask-x-from-inherit/20", - "mask-x-from-inherit/25", - "mask-x-from-inherit/30", - "mask-x-from-inherit/35", - "mask-x-from-inherit/40", - "mask-x-from-inherit/45", - "mask-x-from-inherit/50", - "mask-x-from-inherit/55", - "mask-x-from-inherit/60", - "mask-x-from-inherit/65", - "mask-x-from-inherit/70", - "mask-x-from-inherit/75", - "mask-x-from-inherit/80", - "mask-x-from-inherit/85", - "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", - "mask-x-from-transparent/10", - "mask-x-from-transparent/15", - "mask-x-from-transparent/20", - "mask-x-from-transparent/25", - "mask-x-from-transparent/30", - "mask-x-from-transparent/35", - "mask-x-from-transparent/40", - "mask-x-from-transparent/45", - "mask-x-from-transparent/50", - "mask-x-from-transparent/55", - "mask-x-from-transparent/60", - "mask-x-from-transparent/65", - "mask-x-from-transparent/70", - "mask-x-from-transparent/75", - "mask-x-from-transparent/80", - "mask-x-from-transparent/85", - "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", - "mask-x-to-1", - "mask-x-to-1.5", - "mask-x-to-2", - "mask-x-to-2.5", - "mask-x-to-3", - "mask-x-to-3.5", - "mask-x-to-4", - "mask-x-to-5", - "mask-x-to-5%", - "mask-x-to-6", - "mask-x-to-7", - "mask-x-to-8", - "mask-x-to-9", - "mask-x-to-10", - "mask-x-to-10%", - "mask-x-to-11", - "mask-x-to-12", - "mask-x-to-14", - "mask-x-to-15%", - "mask-x-to-16", - "mask-x-to-20", - "mask-x-to-20%", - "mask-x-to-24", - "mask-x-to-25%", - "mask-x-to-28", - "mask-x-to-30%", - "mask-x-to-32", - "mask-x-to-35%", - "mask-x-to-36", - "mask-x-to-40", - "mask-x-to-40%", - "mask-x-to-44", - "mask-x-to-45%", - "mask-x-to-48", - "mask-x-to-50%", - "mask-x-to-52", - "mask-x-to-55%", - "mask-x-to-56", - "mask-x-to-60", - "mask-x-to-60%", - "mask-x-to-64", - "mask-x-to-65%", - "mask-x-to-70%", - "mask-x-to-72", - "mask-x-to-75%", - "mask-x-to-80", - "mask-x-to-80%", - "mask-x-to-85%", - "mask-x-to-90%", - "mask-x-to-95%", - "mask-x-to-96", - "mask-x-to-100%", - "mask-x-to-current", - "mask-x-to-current/0", - "mask-x-to-current/5", - "mask-x-to-current/10", - "mask-x-to-current/15", - "mask-x-to-current/20", - "mask-x-to-current/25", - "mask-x-to-current/30", - "mask-x-to-current/35", - "mask-x-to-current/40", - "mask-x-to-current/45", - "mask-x-to-current/50", - "mask-x-to-current/55", - "mask-x-to-current/60", - "mask-x-to-current/65", - "mask-x-to-current/70", - "mask-x-to-current/75", - "mask-x-to-current/80", - "mask-x-to-current/85", - "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", - "mask-x-to-inherit/10", - "mask-x-to-inherit/15", - "mask-x-to-inherit/20", - "mask-x-to-inherit/25", - "mask-x-to-inherit/30", - "mask-x-to-inherit/35", - "mask-x-to-inherit/40", - "mask-x-to-inherit/45", - "mask-x-to-inherit/50", - "mask-x-to-inherit/55", - "mask-x-to-inherit/60", - "mask-x-to-inherit/65", - "mask-x-to-inherit/70", - "mask-x-to-inherit/75", - "mask-x-to-inherit/80", - "mask-x-to-inherit/85", - "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", - "mask-x-to-transparent/10", - "mask-x-to-transparent/15", - "mask-x-to-transparent/20", - "mask-x-to-transparent/25", - "mask-x-to-transparent/30", - "mask-x-to-transparent/35", - "mask-x-to-transparent/40", - "mask-x-to-transparent/45", - "mask-x-to-transparent/50", - "mask-x-to-transparent/55", - "mask-x-to-transparent/60", - "mask-x-to-transparent/65", - "mask-x-to-transparent/70", - "mask-x-to-transparent/75", - "mask-x-to-transparent/80", - "mask-x-to-transparent/85", - "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", - "mask-y-from-1", - "mask-y-from-1.5", - "mask-y-from-2", - "mask-y-from-2.5", - "mask-y-from-3", - "mask-y-from-3.5", - "mask-y-from-4", - "mask-y-from-5", - "mask-y-from-5%", - "mask-y-from-6", - "mask-y-from-7", - "mask-y-from-8", - "mask-y-from-9", - "mask-y-from-10", - "mask-y-from-10%", - "mask-y-from-11", - "mask-y-from-12", - "mask-y-from-14", - "mask-y-from-15%", - "mask-y-from-16", - "mask-y-from-20", - "mask-y-from-20%", - "mask-y-from-24", - "mask-y-from-25%", - "mask-y-from-28", - "mask-y-from-30%", - "mask-y-from-32", - "mask-y-from-35%", - "mask-y-from-36", - "mask-y-from-40", - "mask-y-from-40%", - "mask-y-from-44", - "mask-y-from-45%", - "mask-y-from-48", - "mask-y-from-50%", - "mask-y-from-52", - "mask-y-from-55%", - "mask-y-from-56", - "mask-y-from-60", - "mask-y-from-60%", - "mask-y-from-64", - "mask-y-from-65%", - "mask-y-from-70%", - "mask-y-from-72", - "mask-y-from-75%", - "mask-y-from-80", - "mask-y-from-80%", - "mask-y-from-85%", - "mask-y-from-90%", - "mask-y-from-95%", - "mask-y-from-96", - "mask-y-from-100%", - "mask-y-from-current", - "mask-y-from-current/0", - "mask-y-from-current/5", - "mask-y-from-current/10", - "mask-y-from-current/15", - "mask-y-from-current/20", - "mask-y-from-current/25", - "mask-y-from-current/30", - "mask-y-from-current/35", - "mask-y-from-current/40", - "mask-y-from-current/45", - "mask-y-from-current/50", - "mask-y-from-current/55", - "mask-y-from-current/60", - "mask-y-from-current/65", - "mask-y-from-current/70", - "mask-y-from-current/75", - "mask-y-from-current/80", - "mask-y-from-current/85", - "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", - "mask-y-from-inherit/10", - "mask-y-from-inherit/15", - "mask-y-from-inherit/20", - "mask-y-from-inherit/25", - "mask-y-from-inherit/30", - "mask-y-from-inherit/35", - "mask-y-from-inherit/40", - "mask-y-from-inherit/45", - "mask-y-from-inherit/50", - "mask-y-from-inherit/55", - "mask-y-from-inherit/60", - "mask-y-from-inherit/65", - "mask-y-from-inherit/70", - "mask-y-from-inherit/75", - "mask-y-from-inherit/80", - "mask-y-from-inherit/85", - "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", - "mask-y-from-transparent/10", - "mask-y-from-transparent/15", - "mask-y-from-transparent/20", - "mask-y-from-transparent/25", - "mask-y-from-transparent/30", - "mask-y-from-transparent/35", - "mask-y-from-transparent/40", - "mask-y-from-transparent/45", - "mask-y-from-transparent/50", - "mask-y-from-transparent/55", - "mask-y-from-transparent/60", - "mask-y-from-transparent/65", - "mask-y-from-transparent/70", - "mask-y-from-transparent/75", - "mask-y-from-transparent/80", - "mask-y-from-transparent/85", - "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", - "mask-y-to-1", - "mask-y-to-1.5", - "mask-y-to-2", - "mask-y-to-2.5", - "mask-y-to-3", - "mask-y-to-3.5", - "mask-y-to-4", - "mask-y-to-5", - "mask-y-to-5%", - "mask-y-to-6", - "mask-y-to-7", - "mask-y-to-8", - "mask-y-to-9", - "mask-y-to-10", - "mask-y-to-10%", - "mask-y-to-11", - "mask-y-to-12", - "mask-y-to-14", - "mask-y-to-15%", - "mask-y-to-16", - "mask-y-to-20", - "mask-y-to-20%", - "mask-y-to-24", - "mask-y-to-25%", - "mask-y-to-28", - "mask-y-to-30%", - "mask-y-to-32", - "mask-y-to-35%", - "mask-y-to-36", - "mask-y-to-40", - "mask-y-to-40%", - "mask-y-to-44", - "mask-y-to-45%", - "mask-y-to-48", - "mask-y-to-50%", - "mask-y-to-52", - "mask-y-to-55%", - "mask-y-to-56", - "mask-y-to-60", - "mask-y-to-60%", - "mask-y-to-64", - "mask-y-to-65%", - "mask-y-to-70%", - "mask-y-to-72", - "mask-y-to-75%", - "mask-y-to-80", - "mask-y-to-80%", - "mask-y-to-85%", - "mask-y-to-90%", - "mask-y-to-95%", - "mask-y-to-96", - "mask-y-to-100%", - "mask-y-to-current", - "mask-y-to-current/0", - "mask-y-to-current/5", - "mask-y-to-current/10", - "mask-y-to-current/15", - "mask-y-to-current/20", - "mask-y-to-current/25", - "mask-y-to-current/30", - "mask-y-to-current/35", - "mask-y-to-current/40", - "mask-y-to-current/45", - "mask-y-to-current/50", - "mask-y-to-current/55", - "mask-y-to-current/60", - "mask-y-to-current/65", - "mask-y-to-current/70", - "mask-y-to-current/75", - "mask-y-to-current/80", - "mask-y-to-current/85", - "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", - "mask-y-to-inherit/10", - "mask-y-to-inherit/15", - "mask-y-to-inherit/20", - "mask-y-to-inherit/25", - "mask-y-to-inherit/30", - "mask-y-to-inherit/35", - "mask-y-to-inherit/40", - "mask-y-to-inherit/45", - "mask-y-to-inherit/50", - "mask-y-to-inherit/55", - "mask-y-to-inherit/60", - "mask-y-to-inherit/65", - "mask-y-to-inherit/70", - "mask-y-to-inherit/75", - "mask-y-to-inherit/80", - "mask-y-to-inherit/85", - "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", - "mask-y-to-transparent/10", - "mask-y-to-transparent/15", - "mask-y-to-transparent/20", - "mask-y-to-transparent/25", - "mask-y-to-transparent/30", - "mask-y-to-transparent/35", - "mask-y-to-transparent/40", - "mask-y-to-transparent/45", - "mask-y-to-transparent/50", - "mask-y-to-transparent/55", - "mask-y-to-transparent/60", - "mask-y-to-transparent/65", - "mask-y-to-transparent/70", - "mask-y-to-transparent/75", - "mask-y-to-transparent/80", - "mask-y-to-transparent/85", - "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", - "max-h-1.5", - "max-h-2", - "max-h-2.5", "max-h-3", - "max-h-3.5", "max-h-4", - "max-h-5", - "max-h-6", - "max-h-7", - "max-h-8", - "max-h-9", - "max-h-10", - "max-h-11", - "max-h-12", - "max-h-14", - "max-h-16", - "max-h-20", - "max-h-24", - "max-h-28", - "max-h-32", - "max-h-36", - "max-h-40", - "max-h-44", - "max-h-48", - "max-h-52", - "max-h-56", - "max-h-60", - "max-h-64", - "max-h-72", - "max-h-80", - "max-h-96", - "max-h-1/2", - "max-h-1/3", - "max-h-1/4", - "max-h-1/5", - "max-h-1/6", - "max-h-1/12", - "max-h-2/3", - "max-h-2/4", - "max-h-2/5", - "max-h-2/6", - "max-h-2/12", - "max-h-3/4", - "max-h-3/5", - "max-h-3/6", - "max-h-3/12", - "max-h-4/5", - "max-h-4/6", - "max-h-4/12", - "max-h-5/6", - "max-h-5/12", - "max-h-6/12", - "max-h-7/12", - "max-h-8/12", - "max-h-9/12", - "max-h-10/12", - "max-h-11/12", "max-h-dvh", - "max-h-dvw", "max-h-fit", "max-h-full", - "max-h-lh", "max-h-lvh", - "max-h-lvw", "max-h-max", "max-h-min", "max-h-none", - "max-h-px", "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", - "max-w-1.5", - "max-w-2", - "max-w-2.5", "max-w-3", - "max-w-3.5", "max-w-4", - "max-w-5", - "max-w-6", - "max-w-7", - "max-w-8", - "max-w-9", - "max-w-10", - "max-w-11", - "max-w-12", - "max-w-14", - "max-w-16", - "max-w-20", - "max-w-24", - "max-w-28", - "max-w-32", - "max-w-36", - "max-w-40", - "max-w-44", - "max-w-48", - "max-w-52", - "max-w-56", - "max-w-60", - "max-w-64", - "max-w-72", - "max-w-80", - "max-w-96", - "max-w-dvh", - "max-w-dvw", + "max-w-4", "max-w-fit", "max-w-full", - "max-w-lg", - "max-w-lvh", - "max-w-lvw", "max-w-max", - "max-w-md", - "max-w-1/2", - "max-w-1/3", - "max-w-1/4", - "max-w-1/5", - "max-w-1/6", - "max-w-1/12", - "max-w-2/3", - "max-w-2/4", - "max-w-2/5", - "max-w-2/6", - "max-w-2/12", - "max-w-3/4", - "max-w-3/5", - "max-w-3/6", - "max-w-3/12", - "max-w-4/5", - "max-w-4/6", - "max-w-4/12", - "max-w-5/6", - "max-w-5/12", - "max-w-6/12", - "max-w-7/12", - "max-w-8/12", - "max-w-9/12", - "max-w-10/12", - "max-w-11/12", "max-w-min", "max-w-none", - "max-w-px", - "max-w-svh", - "max-w-svw", - "mb-0", "mb-0.5", "mb-1", - "mb-1.5", - "mb-2", - "mb-2.5", "mb-3", - "mb-3.5", "mb-4", - "mb-5", - "mb-6", - "mb-7", - "mb-8", - "mb-9", - "mb-10", - "mb-11", - "mb-12", - "mb-14", - "mb-16", - "mb-20", - "mb-24", - "mb-28", - "mb-32", - "mb-36", - "mb-40", - "mb-44", - "mb-48", - "mb-52", - "mb-56", - "mb-60", - "mb-64", - "mb-72", - "mb-80", - "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", - "me-1.5", - "me-2", - "me-2.5", "me-3", - "me-3.5", "me-4", - "me-5", - "me-6", - "me-7", - "me-8", - "me-9", - "me-10", - "me-11", - "me-12", - "me-14", - "me-16", - "me-20", - "me-24", - "me-28", - "me-32", - "me-36", - "me-40", - "me-44", - "me-48", - "me-52", - "me-56", - "me-60", - "me-64", - "me-72", - "me-80", - "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", - "min-h-1.5", - "min-h-2", - "min-h-2.5", "min-h-3", - "min-h-3.5", "min-h-4", - "min-h-5", - "min-h-6", - "min-h-7", - "min-h-8", - "min-h-9", - "min-h-10", - "min-h-11", - "min-h-12", - "min-h-14", - "min-h-16", - "min-h-20", - "min-h-24", - "min-h-28", - "min-h-32", - "min-h-36", - "min-h-40", - "min-h-44", - "min-h-48", - "min-h-52", - "min-h-56", - "min-h-60", - "min-h-64", - "min-h-72", - "min-h-80", - "min-h-96", - "min-h-1/2", - "min-h-1/3", - "min-h-1/4", - "min-h-1/5", - "min-h-1/6", - "min-h-1/12", - "min-h-2/3", - "min-h-2/4", - "min-h-2/5", - "min-h-2/6", - "min-h-2/12", - "min-h-3/4", - "min-h-3/5", - "min-h-3/6", - "min-h-3/12", - "min-h-4/5", - "min-h-4/6", - "min-h-4/12", - "min-h-5/6", - "min-h-5/12", - "min-h-6/12", - "min-h-7/12", - "min-h-8/12", - "min-h-9/12", - "min-h-10/12", - "min-h-11/12", "min-h-auto", "min-h-dvh", - "min-h-dvw", "min-h-fit", "min-h-full", - "min-h-lh", "min-h-lvh", - "min-h-lvw", "min-h-max", "min-h-min", - "min-h-px", "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", - "min-w-1.5", - "min-w-2", - "min-w-2.5", "min-w-3", - "min-w-3.5", "min-w-4", - "min-w-5", - "min-w-6", - "min-w-7", - "min-w-8", - "min-w-9", - "min-w-10", - "min-w-11", - "min-w-12", - "min-w-14", - "min-w-16", - "min-w-20", - "min-w-24", - "min-w-28", - "min-w-32", - "min-w-36", - "min-w-40", - "min-w-44", - "min-w-48", - "min-w-52", - "min-w-56", - "min-w-60", - "min-w-64", - "min-w-72", - "min-w-80", - "min-w-96", + "min-w-4", "min-w-auto", - "min-w-dvh", - "min-w-dvw", "min-w-fit", "min-w-full", - "min-w-lg", - "min-w-lvh", - "min-w-lvw", "min-w-max", - "min-w-md", - "min-w-1/2", - "min-w-1/3", - "min-w-1/4", - "min-w-1/5", - "min-w-1/6", - "min-w-1/12", - "min-w-2/3", - "min-w-2/4", - "min-w-2/5", - "min-w-2/6", - "min-w-2/12", - "min-w-3/4", - "min-w-3/5", - "min-w-3/6", - "min-w-3/12", - "min-w-4/5", - "min-w-4/6", - "min-w-4/12", - "min-w-5/6", - "min-w-5/12", - "min-w-6/12", - "min-w-7/12", - "min-w-8/12", - "min-w-9/12", - "min-w-10/12", - "min-w-11/12", "min-w-min", - "min-w-px", - "min-w-screen", - "min-w-svh", - "min-w-svw", "mix-blend-color", "mix-blend-color-burn", "mix-blend-color-dodge", @@ -9090,245 +2499,59 @@ exports[`getClassList 1`] = ` "mix-blend-saturation", "mix-blend-screen", "mix-blend-soft-light", - "ml-0", "ml-0.5", "ml-1", - "ml-1.5", - "ml-2", - "ml-2.5", "ml-3", - "ml-3.5", "ml-4", - "ml-5", - "ml-6", - "ml-7", - "ml-8", - "ml-9", - "ml-10", - "ml-11", - "ml-12", - "ml-14", - "ml-16", - "ml-20", - "ml-24", - "ml-28", - "ml-32", - "ml-36", - "ml-40", - "ml-44", - "ml-48", - "ml-52", - "ml-56", - "ml-60", - "ml-64", - "ml-72", - "ml-80", - "ml-96", "ml-auto", - "ml-px", - "mr-0", "mr-0.5", "mr-1", - "mr-1.5", - "mr-2", - "mr-2.5", "mr-3", - "mr-3.5", "mr-4", - "mr-5", - "mr-6", - "mr-7", - "mr-8", - "mr-9", - "mr-10", - "mr-11", - "mr-12", - "mr-14", - "mr-16", - "mr-20", - "mr-24", - "mr-28", - "mr-32", - "mr-36", - "mr-40", - "mr-44", - "mr-48", - "mr-52", - "mr-56", - "mr-60", - "mr-64", - "mr-72", - "mr-80", - "mr-96", "mr-auto", - "mr-px", - "ms-0", "ms-0.5", "ms-1", - "ms-1.5", - "ms-2", - "ms-2.5", "ms-3", - "ms-3.5", "ms-4", - "ms-5", - "ms-6", - "ms-7", - "ms-8", - "ms-9", - "ms-10", - "ms-11", - "ms-12", - "ms-14", - "ms-16", - "ms-20", - "ms-24", - "ms-28", - "ms-32", - "ms-36", - "ms-40", - "ms-44", - "ms-48", - "ms-52", - "ms-56", - "ms-60", - "ms-64", - "ms-72", - "ms-80", - "ms-96", "ms-auto", - "ms-px", - "mt-0", "mt-0.5", "mt-1", - "mt-1.5", - "mt-2", - "mt-2.5", "mt-3", - "mt-3.5", "mt-4", - "mt-5", - "mt-6", - "mt-7", - "mt-8", - "mt-9", - "mt-10", - "mt-11", - "mt-12", - "mt-14", - "mt-16", - "mt-20", - "mt-24", - "mt-28", - "mt-32", - "mt-36", - "mt-40", - "mt-44", - "mt-48", - "mt-52", - "mt-56", - "mt-60", - "mt-64", - "mt-72", - "mt-80", - "mt-96", "mt-auto", - "mt-px", - "mx-0", "mx-0.5", "mx-1", - "mx-1.5", - "mx-2", - "mx-2.5", "mx-3", - "mx-3.5", "mx-4", - "mx-5", - "mx-6", - "mx-7", - "mx-8", - "mx-9", - "mx-10", - "mx-11", - "mx-12", - "mx-14", - "mx-16", - "mx-20", - "mx-24", - "mx-28", - "mx-32", - "mx-36", - "mx-40", - "mx-44", - "mx-48", - "mx-52", - "mx-56", - "mx-60", - "mx-64", - "mx-72", - "mx-80", - "mx-96", "mx-auto", - "mx-px", - "my-0", "my-0.5", "my-1", - "my-1.5", - "my-2", - "my-2.5", "my-3", - "my-3.5", "my-4", - "my-5", - "my-6", - "my-7", - "my-8", - "my-9", - "my-10", - "my-11", - "my-12", - "my-14", - "my-16", - "my-20", - "my-24", - "my-28", - "my-32", - "my-36", - "my-40", - "my-44", - "my-48", - "my-52", - "my-56", - "my-60", - "my-64", - "my-72", - "my-80", - "my-96", "my-auto", - "my-px", "no-underline", "normal-case", "normal-nums", "not-italic", "not-sr-only", "object-bottom", - "object-bottom-left", - "object-bottom-right", "object-center", "object-contain", "object-cover", "object-fill", "object-left", + "object-left-bottom", + "object-left-top", "object-none", "object-right", + "object-right-bottom", + "object-right-top", "object-scale-down", "object-top", - "object-top-left", - "object-top-right", "oldstyle-nums", "opacity-0", - "opacity-5", "opacity-10", + "opacity-100", "opacity-15", "opacity-20", "opacity-25", @@ -9336,6 +2559,7 @@ exports[`getClassList 1`] = ` "opacity-35", "opacity-40", "opacity-45", + "opacity-5", "opacity-50", "opacity-55", "opacity-60", @@ -9346,9 +2570,11 @@ exports[`getClassList 1`] = ` "opacity-85", "opacity-90", "opacity-95", - "opacity-100", "opacity-background", "order-1", + "order-10", + "order-11", + "order-12", "order-2", "order-3", "order-4", @@ -9357,11 +2583,9 @@ exports[`getClassList 1`] = ` "order-7", "order-8", "order-9", - "order-10", - "order-11", - "order-12", "order-first", "order-last", + "order-none", "ordinal", "origin-bottom", "origin-bottom-left", @@ -9394,7 +2618,6 @@ exports[`getClassList 1`] = ` "outline/90", "outline/95", "outline/100", - "outline/background", "outline-0", "outline-1", "outline-2", @@ -9422,11 +2645,9 @@ exports[`getClassList 1`] = ` "outline-current/90", "outline-current/95", "outline-current/100", - "outline-current/background", "outline-dashed", "outline-dotted", "outline-double", - "outline-hidden", "outline-inherit", "outline-inherit/0", "outline-inherit/5", @@ -9449,7 +2670,6 @@ exports[`getClassList 1`] = ` "outline-inherit/90", "outline-inherit/95", "outline-inherit/100", - "outline-inherit/background", "outline-none", "outline-offset-0", "outline-offset-1", @@ -9479,7 +2699,6 @@ exports[`getClassList 1`] = ` "outline-transparent/90", "outline-transparent/95", "outline-transparent/100", - "outline-transparent/background", "overflow-auto", "overflow-clip", "overflow-hidden", @@ -9505,181 +2724,18 @@ exports[`getClassList 1`] = ` "overscroll-y-auto", "overscroll-y-contain", "overscroll-y-none", - "p-0", "p-0.5", "p-1", - "p-1.5", - "p-2", - "p-2.5", "p-3", - "p-3.5", "p-4", - "p-5", - "p-6", - "p-7", - "p-8", - "p-9", - "p-10", - "p-11", - "p-12", - "p-14", - "p-16", - "p-20", - "p-24", - "p-28", - "p-32", - "p-36", - "p-40", - "p-44", - "p-48", - "p-52", - "p-56", - "p-60", - "p-64", - "p-72", - "p-80", - "p-96", - "p-px", - "pb-0", "pb-0.5", "pb-1", - "pb-1.5", - "pb-2", - "pb-2.5", "pb-3", - "pb-3.5", "pb-4", - "pb-5", - "pb-6", - "pb-7", - "pb-8", - "pb-9", - "pb-10", - "pb-11", - "pb-12", - "pb-14", - "pb-16", - "pb-20", - "pb-24", - "pb-28", - "pb-32", - "pb-36", - "pb-40", - "pb-44", - "pb-48", - "pb-52", - "pb-56", - "pb-60", - "pb-64", - "pb-72", - "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", - "pe-1.5", - "pe-2", - "pe-2.5", "pe-3", - "pe-3.5", "pe-4", - "pe-5", - "pe-6", - "pe-7", - "pe-8", - "pe-9", - "pe-10", - "pe-11", - "pe-12", - "pe-14", - "pe-16", - "pe-20", - "pe-24", - "pe-28", - "pe-32", - "pe-36", - "pe-40", - "pe-44", - "pe-48", - "pe-52", - "pe-56", - "pe-60", - "pe-64", - "pe-72", - "pe-80", - "pe-96", - "pe-px", "perspective-dramatic", "perspective-none", "perspective-normal", @@ -9692,63 +2748,26 @@ exports[`getClassList 1`] = ` "perspective-origin-top", "perspective-origin-top-left", "perspective-origin-top-right", - "pl-0", "pl-0.5", "pl-1", - "pl-1.5", - "pl-2", - "pl-2.5", "pl-3", - "pl-3.5", "pl-4", - "pl-5", - "pl-6", - "pl-7", - "pl-8", - "pl-9", - "pl-10", - "pl-11", - "pl-12", - "pl-14", - "pl-16", - "pl-20", - "pl-24", - "pl-28", - "pl-32", - "pl-36", - "pl-40", - "pl-44", - "pl-48", - "pl-52", - "pl-56", - "pl-60", - "pl-64", - "pl-72", - "pl-80", - "pl-96", - "pl-px", "place-content-around", "place-content-baseline", "place-content-between", "place-content-center", - "place-content-center-safe", "place-content-end", - "place-content-end-safe", "place-content-evenly", "place-content-start", "place-content-stretch", "place-items-baseline", "place-items-center", - "place-items-center-safe", "place-items-end", - "place-items-end-safe", "place-items-start", "place-items-stretch", "place-self-auto", "place-self-center", - "place-self-center-safe", "place-self-end", - "place-self-end-safe", "place-self-start", "place-self-stretch", "placeholder-current", @@ -9773,7 +2792,6 @@ exports[`getClassList 1`] = ` "placeholder-current/90", "placeholder-current/95", "placeholder-current/100", - "placeholder-current/background", "placeholder-inherit", "placeholder-inherit/0", "placeholder-inherit/5", @@ -9796,7 +2814,6 @@ exports[`getClassList 1`] = ` "placeholder-inherit/90", "placeholder-inherit/95", "placeholder-inherit/100", - "placeholder-inherit/background", "placeholder-transparent", "placeholder-transparent/0", "placeholder-transparent/5", @@ -9819,253 +2836,40 @@ exports[`getClassList 1`] = ` "placeholder-transparent/90", "placeholder-transparent/95", "placeholder-transparent/100", - "placeholder-transparent/background", "pointer-events-auto", "pointer-events-none", - "pr-0", "pr-0.5", "pr-1", - "pr-1.5", - "pr-2", - "pr-2.5", "pr-3", - "pr-3.5", "pr-4", - "pr-5", - "pr-6", - "pr-7", - "pr-8", - "pr-9", - "pr-10", - "pr-11", - "pr-12", - "pr-14", - "pr-16", - "pr-20", - "pr-24", - "pr-28", - "pr-32", - "pr-36", - "pr-40", - "pr-44", - "pr-48", - "pr-52", - "pr-56", - "pr-60", - "pr-64", - "pr-72", - "pr-80", - "pr-96", - "pr-px", "proportional-nums", - "ps-0", "ps-0.5", "ps-1", - "ps-1.5", - "ps-2", - "ps-2.5", "ps-3", - "ps-3.5", "ps-4", - "ps-5", - "ps-6", - "ps-7", - "ps-8", - "ps-9", - "ps-10", - "ps-11", - "ps-12", - "ps-14", - "ps-16", - "ps-20", - "ps-24", - "ps-28", - "ps-32", - "ps-36", - "ps-40", - "ps-44", - "ps-48", - "ps-52", - "ps-56", - "ps-60", - "ps-64", - "ps-72", - "ps-80", - "ps-96", - "ps-px", - "pt-0", "pt-0.5", "pt-1", - "pt-1.5", - "pt-2", - "pt-2.5", "pt-3", - "pt-3.5", "pt-4", - "pt-5", - "pt-6", - "pt-7", - "pt-8", - "pt-9", - "pt-10", - "pt-11", - "pt-12", - "pt-14", - "pt-16", - "pt-20", - "pt-24", - "pt-28", - "pt-32", - "pt-36", - "pt-40", - "pt-44", - "pt-48", - "pt-52", - "pt-56", - "pt-60", - "pt-64", - "pt-72", - "pt-80", - "pt-96", - "pt-px", - "px-0", "px-0.5", "px-1", - "px-1.5", - "px-2", - "px-2.5", "px-3", - "px-3.5", "px-4", - "px-5", - "px-6", - "px-7", - "px-8", - "px-9", - "px-10", - "px-11", - "px-12", - "px-14", - "px-16", - "px-20", - "px-24", - "px-28", - "px-32", - "px-36", - "px-40", - "px-44", - "px-48", - "px-52", - "px-56", - "px-60", - "px-64", - "px-72", - "px-80", - "px-96", - "px-px", - "py-0", "py-0.5", "py-1", - "py-1.5", - "py-2", - "py-2.5", "py-3", - "py-3.5", "py-4", - "py-5", - "py-6", - "py-7", - "py-8", - "py-9", - "py-10", - "py-11", - "py-12", - "py-14", - "py-16", - "py-20", - "py-24", - "py-28", - "py-32", - "py-36", - "py-40", - "py-44", - "py-48", - "py-52", - "py-56", - "py-60", - "py-64", - "py-72", - "py-80", - "py-96", - "py-px", "relative", "resize", "resize-none", "resize-x", "resize-y", - "right-0", "right-0.5", "right-1", - "right-1.5", - "right-2", - "right-2.5", "right-3", - "right-3.5", "right-4", - "right-5", - "right-6", - "right-7", - "right-8", - "right-9", - "right-10", - "right-11", - "right-12", - "right-14", - "right-16", - "right-20", - "right-24", - "right-28", - "right-32", - "right-36", - "right-40", - "right-44", - "right-48", - "right-52", - "right-56", - "right-60", - "right-64", - "right-72", - "right-80", - "right-96", - "right-1/2", - "right-1/3", - "right-1/4", - "right-1/5", - "right-1/6", - "right-1/12", - "right-2/3", - "right-2/4", - "right-2/5", - "right-2/6", - "right-2/12", - "right-3/4", - "right-3/5", - "right-3/6", - "right-3/12", - "right-4/5", - "right-4/6", - "right-4/12", - "right-5/6", - "right-5/12", - "right-6/12", - "right-7/12", - "right-8/12", - "right-9/12", - "right-10/12", - "right-11/12", "right-auto", "right-full", - "right-px", "ring", "ring-0", "ring-1", @@ -10094,7 +2898,6 @@ exports[`getClassList 1`] = ` "ring-current/90", "ring-current/95", "ring-current/100", - "ring-current/background", "ring-inherit", "ring-inherit/0", "ring-inherit/5", @@ -10117,7 +2920,6 @@ exports[`getClassList 1`] = ` "ring-inherit/90", "ring-inherit/95", "ring-inherit/100", - "ring-inherit/background", "ring-inset", "ring-offset-0", "ring-offset-1", @@ -10146,7 +2948,6 @@ exports[`getClassList 1`] = ` "ring-offset-current/90", "ring-offset-current/95", "ring-offset-current/100", - "ring-offset-current/background", "ring-offset-inherit", "ring-offset-inherit/0", "ring-offset-inherit/5", @@ -10169,7 +2970,6 @@ exports[`getClassList 1`] = ` "ring-offset-inherit/90", "ring-offset-inherit/95", "ring-offset-inherit/100", - "ring-offset-inherit/background", "ring-offset-transparent", "ring-offset-transparent/0", "ring-offset-transparent/5", @@ -10192,7 +2992,6 @@ exports[`getClassList 1`] = ` "ring-offset-transparent/90", "ring-offset-transparent/95", "ring-offset-transparent/100", - "ring-offset-transparent/background", "ring-transparent", "ring-transparent/0", "ring-transparent/5", @@ -10215,44 +3014,43 @@ exports[`getClassList 1`] = ` "ring-transparent/90", "ring-transparent/95", "ring-transparent/100", - "ring-transparent/background", "rotate-0", "rotate-1", + "rotate-12", + "rotate-180", "rotate-2", "rotate-3", - "rotate-6", - "rotate-12", "rotate-45", + "rotate-6", "rotate-90", - "rotate-180", "rotate-none", "rotate-x-0", "rotate-x-1", + "rotate-x-12", + "rotate-x-180", "rotate-x-2", "rotate-x-3", - "rotate-x-6", - "rotate-x-12", "rotate-x-45", + "rotate-x-6", "rotate-x-90", - "rotate-x-180", "rotate-y-0", "rotate-y-1", + "rotate-y-12", + "rotate-y-180", "rotate-y-2", "rotate-y-3", - "rotate-y-6", - "rotate-y-12", "rotate-y-45", + "rotate-y-6", "rotate-y-90", - "rotate-y-180", "rotate-z-0", "rotate-z-1", + "rotate-z-12", + "rotate-z-180", "rotate-z-2", "rotate-z-3", - "rotate-z-6", - "rotate-z-12", "rotate-z-45", + "rotate-z-6", "rotate-z-90", - "rotate-z-180", "rounded-b-full", "rounded-b-none", "rounded-bl-full", @@ -10285,6 +3083,10 @@ exports[`getClassList 1`] = ` "rounded-tr-none", "row-auto", "row-end-1", + "row-end-10", + "row-end-11", + "row-end-12", + "row-end-13", "row-end-2", "row-end-3", "row-end-4", @@ -10293,12 +3095,11 @@ exports[`getClassList 1`] = ` "row-end-7", "row-end-8", "row-end-9", - "row-end-10", - "row-end-11", - "row-end-12", - "row-end-13", "row-end-auto", "row-span-1", + "row-span-10", + "row-span-11", + "row-span-12", "row-span-2", "row-span-3", "row-span-4", @@ -10307,11 +3108,12 @@ exports[`getClassList 1`] = ` "row-span-7", "row-span-8", "row-span-9", - "row-span-10", - "row-span-11", - "row-span-12", "row-span-full", "row-start-1", + "row-start-10", + "row-start-11", + "row-start-12", + "row-start-13", "row-start-2", "row-start-3", "row-start-4", @@ -10320,62 +3122,58 @@ exports[`getClassList 1`] = ` "row-start-7", "row-start-8", "row-start-9", - "row-start-10", - "row-start-11", - "row-start-12", - "row-start-13", "row-start-auto", "saturate-0", - "saturate-50", "saturate-100", "saturate-150", "saturate-200", + "saturate-50", "scale-0", - "scale-3d", - "scale-50", - "scale-75", - "scale-90", - "scale-95", "scale-100", "scale-105", "scale-110", "scale-125", "scale-150", "scale-200", + "scale-3d", + "scale-50", + "scale-75", + "scale-90", + "scale-95", "scale-none", "scale-x-0", - "scale-x-50", - "scale-x-75", - "scale-x-90", - "scale-x-95", "scale-x-100", "scale-x-105", "scale-x-110", "scale-x-125", "scale-x-150", "scale-x-200", + "scale-x-50", + "scale-x-75", + "scale-x-90", + "scale-x-95", "scale-y-0", - "scale-y-50", - "scale-y-75", - "scale-y-90", - "scale-y-95", "scale-y-100", "scale-y-105", "scale-y-110", "scale-y-125", "scale-y-150", "scale-y-200", + "scale-y-50", + "scale-y-75", + "scale-y-90", + "scale-y-95", "scale-z-0", - "scale-z-50", - "scale-z-75", - "scale-z-90", - "scale-z-95", "scale-z-100", "scale-z-105", "scale-z-110", "scale-z-125", "scale-z-150", "scale-z-200", + "scale-z-50", + "scale-z-75", + "scale-z-90", + "scale-z-95", "scheme-dark", "scheme-light", "scheme-light-dark", @@ -10383,938 +3181,94 @@ exports[`getClassList 1`] = ` "scheme-only-dark", "scheme-only-light", "scroll-auto", - "scroll-m-0", "scroll-m-0.5", "scroll-m-1", - "scroll-m-1.5", - "scroll-m-2", - "scroll-m-2.5", "scroll-m-3", - "scroll-m-3.5", "scroll-m-4", - "scroll-m-5", - "scroll-m-6", - "scroll-m-7", - "scroll-m-8", - "scroll-m-9", - "scroll-m-10", - "scroll-m-11", - "scroll-m-12", - "scroll-m-14", - "scroll-m-16", - "scroll-m-20", - "scroll-m-24", - "scroll-m-28", - "scroll-m-32", - "scroll-m-36", - "scroll-m-40", - "scroll-m-44", - "scroll-m-48", - "scroll-m-52", - "scroll-m-56", - "scroll-m-60", - "scroll-m-64", - "scroll-m-72", - "scroll-m-80", - "scroll-m-96", - "scroll-m-px", - "scroll-mb-0", "scroll-mb-0.5", "scroll-mb-1", - "scroll-mb-1.5", - "scroll-mb-2", - "scroll-mb-2.5", "scroll-mb-3", - "scroll-mb-3.5", "scroll-mb-4", - "scroll-mb-5", - "scroll-mb-6", - "scroll-mb-7", - "scroll-mb-8", - "scroll-mb-9", - "scroll-mb-10", - "scroll-mb-11", - "scroll-mb-12", - "scroll-mb-14", - "scroll-mb-16", - "scroll-mb-20", - "scroll-mb-24", - "scroll-mb-28", - "scroll-mb-32", - "scroll-mb-36", - "scroll-mb-40", - "scroll-mb-44", - "scroll-mb-48", - "scroll-mb-52", - "scroll-mb-56", - "scroll-mb-60", - "scroll-mb-64", - "scroll-mb-72", - "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", - "scroll-me-1.5", - "scroll-me-2", - "scroll-me-2.5", "scroll-me-3", - "scroll-me-3.5", "scroll-me-4", - "scroll-me-5", - "scroll-me-6", - "scroll-me-7", - "scroll-me-8", - "scroll-me-9", - "scroll-me-10", - "scroll-me-11", - "scroll-me-12", - "scroll-me-14", - "scroll-me-16", - "scroll-me-20", - "scroll-me-24", - "scroll-me-28", - "scroll-me-32", - "scroll-me-36", - "scroll-me-40", - "scroll-me-44", - "scroll-me-48", - "scroll-me-52", - "scroll-me-56", - "scroll-me-60", - "scroll-me-64", - "scroll-me-72", - "scroll-me-80", - "scroll-me-96", - "scroll-me-px", - "scroll-ml-0", "scroll-ml-0.5", "scroll-ml-1", - "scroll-ml-1.5", - "scroll-ml-2", - "scroll-ml-2.5", "scroll-ml-3", - "scroll-ml-3.5", "scroll-ml-4", - "scroll-ml-5", - "scroll-ml-6", - "scroll-ml-7", - "scroll-ml-8", - "scroll-ml-9", - "scroll-ml-10", - "scroll-ml-11", - "scroll-ml-12", - "scroll-ml-14", - "scroll-ml-16", - "scroll-ml-20", - "scroll-ml-24", - "scroll-ml-28", - "scroll-ml-32", - "scroll-ml-36", - "scroll-ml-40", - "scroll-ml-44", - "scroll-ml-48", - "scroll-ml-52", - "scroll-ml-56", - "scroll-ml-60", - "scroll-ml-64", - "scroll-ml-72", - "scroll-ml-80", - "scroll-ml-96", - "scroll-ml-px", - "scroll-mr-0", "scroll-mr-0.5", "scroll-mr-1", - "scroll-mr-1.5", - "scroll-mr-2", - "scroll-mr-2.5", "scroll-mr-3", - "scroll-mr-3.5", "scroll-mr-4", - "scroll-mr-5", - "scroll-mr-6", - "scroll-mr-7", - "scroll-mr-8", - "scroll-mr-9", - "scroll-mr-10", - "scroll-mr-11", - "scroll-mr-12", - "scroll-mr-14", - "scroll-mr-16", - "scroll-mr-20", - "scroll-mr-24", - "scroll-mr-28", - "scroll-mr-32", - "scroll-mr-36", - "scroll-mr-40", - "scroll-mr-44", - "scroll-mr-48", - "scroll-mr-52", - "scroll-mr-56", - "scroll-mr-60", - "scroll-mr-64", - "scroll-mr-72", - "scroll-mr-80", - "scroll-mr-96", - "scroll-mr-px", - "scroll-ms-0", "scroll-ms-0.5", "scroll-ms-1", - "scroll-ms-1.5", - "scroll-ms-2", - "scroll-ms-2.5", "scroll-ms-3", - "scroll-ms-3.5", "scroll-ms-4", - "scroll-ms-5", - "scroll-ms-6", - "scroll-ms-7", - "scroll-ms-8", - "scroll-ms-9", - "scroll-ms-10", - "scroll-ms-11", - "scroll-ms-12", - "scroll-ms-14", - "scroll-ms-16", - "scroll-ms-20", - "scroll-ms-24", - "scroll-ms-28", - "scroll-ms-32", - "scroll-ms-36", - "scroll-ms-40", - "scroll-ms-44", - "scroll-ms-48", - "scroll-ms-52", - "scroll-ms-56", - "scroll-ms-60", - "scroll-ms-64", - "scroll-ms-72", - "scroll-ms-80", - "scroll-ms-96", - "scroll-ms-px", - "scroll-mt-0", "scroll-mt-0.5", "scroll-mt-1", - "scroll-mt-1.5", - "scroll-mt-2", - "scroll-mt-2.5", "scroll-mt-3", - "scroll-mt-3.5", "scroll-mt-4", - "scroll-mt-5", - "scroll-mt-6", - "scroll-mt-7", - "scroll-mt-8", - "scroll-mt-9", - "scroll-mt-10", - "scroll-mt-11", - "scroll-mt-12", - "scroll-mt-14", - "scroll-mt-16", - "scroll-mt-20", - "scroll-mt-24", - "scroll-mt-28", - "scroll-mt-32", - "scroll-mt-36", - "scroll-mt-40", - "scroll-mt-44", - "scroll-mt-48", - "scroll-mt-52", - "scroll-mt-56", - "scroll-mt-60", - "scroll-mt-64", - "scroll-mt-72", - "scroll-mt-80", - "scroll-mt-96", - "scroll-mt-px", - "scroll-mx-0", "scroll-mx-0.5", "scroll-mx-1", - "scroll-mx-1.5", - "scroll-mx-2", - "scroll-mx-2.5", "scroll-mx-3", - "scroll-mx-3.5", "scroll-mx-4", - "scroll-mx-5", - "scroll-mx-6", - "scroll-mx-7", - "scroll-mx-8", - "scroll-mx-9", - "scroll-mx-10", - "scroll-mx-11", - "scroll-mx-12", - "scroll-mx-14", - "scroll-mx-16", - "scroll-mx-20", - "scroll-mx-24", - "scroll-mx-28", - "scroll-mx-32", - "scroll-mx-36", - "scroll-mx-40", - "scroll-mx-44", - "scroll-mx-48", - "scroll-mx-52", - "scroll-mx-56", - "scroll-mx-60", - "scroll-mx-64", - "scroll-mx-72", - "scroll-mx-80", - "scroll-mx-96", - "scroll-mx-px", - "scroll-my-0", "scroll-my-0.5", "scroll-my-1", - "scroll-my-1.5", - "scroll-my-2", - "scroll-my-2.5", "scroll-my-3", - "scroll-my-3.5", "scroll-my-4", - "scroll-my-5", - "scroll-my-6", - "scroll-my-7", - "scroll-my-8", - "scroll-my-9", - "scroll-my-10", - "scroll-my-11", - "scroll-my-12", - "scroll-my-14", - "scroll-my-16", - "scroll-my-20", - "scroll-my-24", - "scroll-my-28", - "scroll-my-32", - "scroll-my-36", - "scroll-my-40", - "scroll-my-44", - "scroll-my-48", - "scroll-my-52", - "scroll-my-56", - "scroll-my-60", - "scroll-my-64", - "scroll-my-72", - "scroll-my-80", - "scroll-my-96", - "scroll-my-px", - "scroll-p-0", "scroll-p-0.5", "scroll-p-1", - "scroll-p-1.5", - "scroll-p-2", - "scroll-p-2.5", "scroll-p-3", - "scroll-p-3.5", "scroll-p-4", - "scroll-p-5", - "scroll-p-6", - "scroll-p-7", - "scroll-p-8", - "scroll-p-9", - "scroll-p-10", - "scroll-p-11", - "scroll-p-12", - "scroll-p-14", - "scroll-p-16", - "scroll-p-20", - "scroll-p-24", - "scroll-p-28", - "scroll-p-32", - "scroll-p-36", - "scroll-p-40", - "scroll-p-44", - "scroll-p-48", - "scroll-p-52", - "scroll-p-56", - "scroll-p-60", - "scroll-p-64", - "scroll-p-72", - "scroll-p-80", - "scroll-p-96", - "scroll-p-px", - "scroll-pb-0", "scroll-pb-0.5", "scroll-pb-1", - "scroll-pb-1.5", - "scroll-pb-2", - "scroll-pb-2.5", "scroll-pb-3", - "scroll-pb-3.5", "scroll-pb-4", - "scroll-pb-5", - "scroll-pb-6", - "scroll-pb-7", - "scroll-pb-8", - "scroll-pb-9", - "scroll-pb-10", - "scroll-pb-11", - "scroll-pb-12", - "scroll-pb-14", - "scroll-pb-16", - "scroll-pb-20", - "scroll-pb-24", - "scroll-pb-28", - "scroll-pb-32", - "scroll-pb-36", - "scroll-pb-40", - "scroll-pb-44", - "scroll-pb-48", - "scroll-pb-52", - "scroll-pb-56", - "scroll-pb-60", - "scroll-pb-64", - "scroll-pb-72", - "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", - "scroll-pe-1.5", - "scroll-pe-2", - "scroll-pe-2.5", "scroll-pe-3", - "scroll-pe-3.5", "scroll-pe-4", - "scroll-pe-5", - "scroll-pe-6", - "scroll-pe-7", - "scroll-pe-8", - "scroll-pe-9", - "scroll-pe-10", - "scroll-pe-11", - "scroll-pe-12", - "scroll-pe-14", - "scroll-pe-16", - "scroll-pe-20", - "scroll-pe-24", - "scroll-pe-28", - "scroll-pe-32", - "scroll-pe-36", - "scroll-pe-40", - "scroll-pe-44", - "scroll-pe-48", - "scroll-pe-52", - "scroll-pe-56", - "scroll-pe-60", - "scroll-pe-64", - "scroll-pe-72", - "scroll-pe-80", - "scroll-pe-96", - "scroll-pe-px", - "scroll-pl-0", "scroll-pl-0.5", "scroll-pl-1", - "scroll-pl-1.5", - "scroll-pl-2", - "scroll-pl-2.5", "scroll-pl-3", - "scroll-pl-3.5", "scroll-pl-4", - "scroll-pl-5", - "scroll-pl-6", - "scroll-pl-7", - "scroll-pl-8", - "scroll-pl-9", - "scroll-pl-10", - "scroll-pl-11", - "scroll-pl-12", - "scroll-pl-14", - "scroll-pl-16", - "scroll-pl-20", - "scroll-pl-24", - "scroll-pl-28", - "scroll-pl-32", - "scroll-pl-36", - "scroll-pl-40", - "scroll-pl-44", - "scroll-pl-48", - "scroll-pl-52", - "scroll-pl-56", - "scroll-pl-60", - "scroll-pl-64", - "scroll-pl-72", - "scroll-pl-80", - "scroll-pl-96", - "scroll-pl-px", - "scroll-pr-0", "scroll-pr-0.5", "scroll-pr-1", - "scroll-pr-1.5", - "scroll-pr-2", - "scroll-pr-2.5", "scroll-pr-3", - "scroll-pr-3.5", "scroll-pr-4", - "scroll-pr-5", - "scroll-pr-6", - "scroll-pr-7", - "scroll-pr-8", - "scroll-pr-9", - "scroll-pr-10", - "scroll-pr-11", - "scroll-pr-12", - "scroll-pr-14", - "scroll-pr-16", - "scroll-pr-20", - "scroll-pr-24", - "scroll-pr-28", - "scroll-pr-32", - "scroll-pr-36", - "scroll-pr-40", - "scroll-pr-44", - "scroll-pr-48", - "scroll-pr-52", - "scroll-pr-56", - "scroll-pr-60", - "scroll-pr-64", - "scroll-pr-72", - "scroll-pr-80", - "scroll-pr-96", - "scroll-pr-px", - "scroll-ps-0", "scroll-ps-0.5", "scroll-ps-1", - "scroll-ps-1.5", - "scroll-ps-2", - "scroll-ps-2.5", "scroll-ps-3", - "scroll-ps-3.5", "scroll-ps-4", - "scroll-ps-5", - "scroll-ps-6", - "scroll-ps-7", - "scroll-ps-8", - "scroll-ps-9", - "scroll-ps-10", - "scroll-ps-11", - "scroll-ps-12", - "scroll-ps-14", - "scroll-ps-16", - "scroll-ps-20", - "scroll-ps-24", - "scroll-ps-28", - "scroll-ps-32", - "scroll-ps-36", - "scroll-ps-40", - "scroll-ps-44", - "scroll-ps-48", - "scroll-ps-52", - "scroll-ps-56", - "scroll-ps-60", - "scroll-ps-64", - "scroll-ps-72", - "scroll-ps-80", - "scroll-ps-96", - "scroll-ps-px", - "scroll-pt-0", "scroll-pt-0.5", "scroll-pt-1", - "scroll-pt-1.5", - "scroll-pt-2", - "scroll-pt-2.5", "scroll-pt-3", - "scroll-pt-3.5", "scroll-pt-4", - "scroll-pt-5", - "scroll-pt-6", - "scroll-pt-7", - "scroll-pt-8", - "scroll-pt-9", - "scroll-pt-10", - "scroll-pt-11", - "scroll-pt-12", - "scroll-pt-14", - "scroll-pt-16", - "scroll-pt-20", - "scroll-pt-24", - "scroll-pt-28", - "scroll-pt-32", - "scroll-pt-36", - "scroll-pt-40", - "scroll-pt-44", - "scroll-pt-48", - "scroll-pt-52", - "scroll-pt-56", - "scroll-pt-60", - "scroll-pt-64", - "scroll-pt-72", - "scroll-pt-80", - "scroll-pt-96", - "scroll-pt-px", - "scroll-px-0", "scroll-px-0.5", "scroll-px-1", - "scroll-px-1.5", - "scroll-px-2", - "scroll-px-2.5", "scroll-px-3", - "scroll-px-3.5", "scroll-px-4", - "scroll-px-5", - "scroll-px-6", - "scroll-px-7", - "scroll-px-8", - "scroll-px-9", - "scroll-px-10", - "scroll-px-11", - "scroll-px-12", - "scroll-px-14", - "scroll-px-16", - "scroll-px-20", - "scroll-px-24", - "scroll-px-28", - "scroll-px-32", - "scroll-px-36", - "scroll-px-40", - "scroll-px-44", - "scroll-px-48", - "scroll-px-52", - "scroll-px-56", - "scroll-px-60", - "scroll-px-64", - "scroll-px-72", - "scroll-px-80", - "scroll-px-96", - "scroll-px-px", - "scroll-py-0", "scroll-py-0.5", "scroll-py-1", - "scroll-py-1.5", - "scroll-py-2", - "scroll-py-2.5", "scroll-py-3", - "scroll-py-3.5", "scroll-py-4", - "scroll-py-5", - "scroll-py-6", - "scroll-py-7", - "scroll-py-8", - "scroll-py-9", - "scroll-py-10", - "scroll-py-11", - "scroll-py-12", - "scroll-py-14", - "scroll-py-16", - "scroll-py-20", - "scroll-py-24", - "scroll-py-28", - "scroll-py-32", - "scroll-py-36", - "scroll-py-40", - "scroll-py-44", - "scroll-py-48", - "scroll-py-52", - "scroll-py-56", - "scroll-py-60", - "scroll-py-64", - "scroll-py-72", - "scroll-py-80", - "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", "select-text", "self-auto", "self-baseline", - "self-baseline-last", "self-center", - "self-center-safe", "self-end", - "self-end-safe", "self-start", "self-stretch", "sepia", "sepia-0", - "sepia-50", "sepia-100", + "sepia-50", + "shadow", "shadow-current", "shadow-current/0", "shadow-current/5", @@ -11337,7 +3291,6 @@ exports[`getClassList 1`] = ` "shadow-current/90", "shadow-current/95", "shadow-current/100", - "shadow-current/background", "shadow-inherit", "shadow-inherit/0", "shadow-inherit/5", @@ -11360,9 +3313,7 @@ exports[`getClassList 1`] = ` "shadow-inherit/90", "shadow-inherit/95", "shadow-inherit/100", - "shadow-inherit/background", "shadow-initial", - "shadow-none", "shadow-transparent", "shadow-transparent/0", "shadow-transparent/5", @@ -11385,99 +3336,35 @@ exports[`getClassList 1`] = ` "shadow-transparent/90", "shadow-transparent/95", "shadow-transparent/100", - "shadow-transparent/background", "shrink", "shrink-0", - "size-0", "size-0.5", "size-1", - "size-1.5", - "size-2", - "size-2.5", "size-3", - "size-3.5", "size-4", - "size-5", - "size-6", - "size-7", - "size-8", - "size-9", - "size-10", - "size-11", - "size-12", - "size-14", - "size-16", - "size-20", - "size-24", - "size-28", - "size-32", - "size-36", - "size-40", - "size-44", - "size-48", - "size-52", - "size-56", - "size-60", - "size-64", - "size-72", - "size-80", - "size-96", - "size-1/2", - "size-1/3", - "size-1/4", - "size-1/5", - "size-1/6", - "size-1/12", - "size-2/3", - "size-2/4", - "size-2/5", - "size-2/6", - "size-2/12", - "size-3/4", - "size-3/5", - "size-3/6", - "size-3/12", - "size-4/5", - "size-4/6", - "size-4/12", - "size-5/6", - "size-5/12", - "size-6/12", - "size-7/12", - "size-8/12", - "size-9/12", - "size-10/12", - "size-11/12", "size-auto", - "size-dvh", - "size-dvw", "size-fit", "size-full", - "size-lvh", - "size-lvw", "size-max", "size-min", - "size-px", - "size-svh", - "size-svw", "skew-0", "skew-1", + "skew-12", "skew-2", "skew-3", "skew-6", - "skew-12", "skew-x-0", "skew-x-1", + "skew-x-12", "skew-x-2", "skew-x-3", "skew-x-6", - "skew-x-12", "skew-y-0", "skew-y-1", + "skew-y-12", "skew-y-2", "skew-y-3", "skew-y-6", - "skew-y-12", "slashed-zero", "snap-align-none", "snap-always", @@ -11491,83 +3378,24 @@ exports[`getClassList 1`] = ` "snap-start", "snap-x", "snap-y", - "space-x-0", "space-x-0.5", "space-x-1", - "space-x-1.5", - "space-x-2", - "space-x-2.5", "space-x-3", - "space-x-3.5", "space-x-4", - "space-x-5", - "space-x-6", - "space-x-7", - "space-x-8", - "space-x-9", - "space-x-10", - "space-x-11", - "space-x-12", - "space-x-14", - "space-x-16", - "space-x-20", - "space-x-24", - "space-x-28", - "space-x-32", - "space-x-36", - "space-x-40", - "space-x-44", - "space-x-48", - "space-x-52", - "space-x-56", - "space-x-60", - "space-x-64", - "space-x-72", - "space-x-80", - "space-x-96", - "space-x-px", "space-x-reverse", - "space-y-0", "space-y-0.5", "space-y-1", - "space-y-1.5", - "space-y-2", - "space-y-2.5", "space-y-3", - "space-y-3.5", "space-y-4", - "space-y-5", - "space-y-6", - "space-y-7", - "space-y-8", - "space-y-9", - "space-y-10", - "space-y-11", - "space-y-12", - "space-y-14", - "space-y-16", - "space-y-20", - "space-y-24", - "space-y-28", - "space-y-32", - "space-y-36", - "space-y-40", - "space-y-44", - "space-y-48", - "space-y-52", - "space-y-56", - "space-y-60", - "space-y-64", - "space-y-72", - "space-y-80", - "space-y-96", - "space-y-px", "space-y-reverse", "sr-only", "stacked-fractions", + "start-0.5", + "start-1", + "start-3", + "start-4", "start-auto", "start-full", - "start-px", "static", "sticky", "stroke-0", @@ -11596,7 +3424,6 @@ exports[`getClassList 1`] = ` "stroke-current/90", "stroke-current/95", "stroke-current/100", - "stroke-current/background", "stroke-inherit", "stroke-inherit/0", "stroke-inherit/5", @@ -11619,7 +3446,6 @@ exports[`getClassList 1`] = ` "stroke-inherit/90", "stroke-inherit/95", "stroke-inherit/100", - "stroke-inherit/background", "stroke-none", "stroke-transparent", "stroke-transparent/0", @@ -11643,11 +3469,7 @@ exports[`getClassList 1`] = ` "stroke-transparent/90", "stroke-transparent/95", "stroke-transparent/100", - "stroke-transparent/background", "subpixel-antialiased", - "tab-2", - "tab-4", - "tab-8", "table", "table-auto", "table-caption", @@ -11685,7 +3507,6 @@ exports[`getClassList 1`] = ` "text-current/90", "text-current/95", "text-current/100", - "text-current/background", "text-ellipsis", "text-end", "text-inherit", @@ -11710,83 +3531,11 @@ 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-current", - "text-shadow-current/0", - "text-shadow-current/5", - "text-shadow-current/10", - "text-shadow-current/15", - "text-shadow-current/20", - "text-shadow-current/25", - "text-shadow-current/30", - "text-shadow-current/35", - "text-shadow-current/40", - "text-shadow-current/45", - "text-shadow-current/50", - "text-shadow-current/55", - "text-shadow-current/60", - "text-shadow-current/65", - "text-shadow-current/70", - "text-shadow-current/75", - "text-shadow-current/80", - "text-shadow-current/85", - "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", - "text-shadow-inherit/10", - "text-shadow-inherit/15", - "text-shadow-inherit/20", - "text-shadow-inherit/25", - "text-shadow-inherit/30", - "text-shadow-inherit/35", - "text-shadow-inherit/40", - "text-shadow-inherit/45", - "text-shadow-inherit/50", - "text-shadow-inherit/55", - "text-shadow-inherit/60", - "text-shadow-inherit/65", - "text-shadow-inherit/70", - "text-shadow-inherit/75", - "text-shadow-inherit/80", - "text-shadow-inherit/85", - "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", - "text-shadow-transparent/0", - "text-shadow-transparent/5", - "text-shadow-transparent/10", - "text-shadow-transparent/15", - "text-shadow-transparent/20", - "text-shadow-transparent/25", - "text-shadow-transparent/30", - "text-shadow-transparent/35", - "text-shadow-transparent/40", - "text-shadow-transparent/45", - "text-shadow-transparent/50", - "text-shadow-transparent/55", - "text-shadow-transparent/60", - "text-shadow-transparent/65", - "text-shadow-transparent/70", - "text-shadow-transparent/75", - "text-shadow-transparent/80", - "text-shadow-transparent/85", - "text-shadow-transparent/90", - "text-shadow-transparent/95", - "text-shadow-transparent/100", - "text-shadow-transparent/background", "text-start", "text-transparent", "text-transparent/0", @@ -11810,12 +3559,11 @@ exports[`getClassList 1`] = ` "text-transparent/90", "text-transparent/95", "text-transparent/100", - "text-transparent/background", "text-wrap", "text-xs", "to-0%", - "to-5%", "to-10%", + "to-100%", "to-15%", "to-20%", "to-25%", @@ -11823,6 +3571,7 @@ exports[`getClassList 1`] = ` "to-35%", "to-40%", "to-45%", + "to-5%", "to-50%", "to-55%", "to-60%", @@ -11833,7 +3582,6 @@ exports[`getClassList 1`] = ` "to-85%", "to-90%", "to-95%", - "to-100%", "to-current", "to-current/0", "to-current/5", @@ -11856,7 +3604,6 @@ exports[`getClassList 1`] = ` "to-current/90", "to-current/95", "to-current/100", - "to-current/background", "to-inherit", "to-inherit/0", "to-inherit/5", @@ -11879,7 +3626,6 @@ exports[`getClassList 1`] = ` "to-inherit/90", "to-inherit/95", "to-inherit/100", - "to-inherit/background", "to-transparent", "to-transparent/0", "to-transparent/5", @@ -11902,70 +3648,12 @@ exports[`getClassList 1`] = ` "to-transparent/90", "to-transparent/95", "to-transparent/100", - "to-transparent/background", - "top-0", "top-0.5", "top-1", - "top-1.5", - "top-2", - "top-2.5", "top-3", - "top-3.5", "top-4", - "top-5", - "top-6", - "top-7", - "top-8", - "top-9", - "top-10", - "top-11", - "top-12", - "top-14", - "top-16", - "top-20", - "top-24", - "top-28", - "top-32", - "top-36", - "top-40", - "top-44", - "top-48", - "top-52", - "top-56", - "top-60", - "top-64", - "top-72", - "top-80", - "top-96", - "top-1/2", - "top-1/3", - "top-1/4", - "top-1/5", - "top-1/6", - "top-1/12", - "top-2/3", - "top-2/4", - "top-2/5", - "top-2/6", - "top-2/12", - "top-3/4", - "top-3/5", - "top-3/6", - "top-3/12", - "top-4/5", - "top-4/6", - "top-4/12", - "top-5/6", - "top-5/12", - "top-6/12", - "top-7/12", - "top-8/12", - "top-9/12", - "top-10/12", - "top-11/12", "top-auto", "top-full", - "top-px", "touch-auto", "touch-manipulation", "touch-none", @@ -11990,234 +3678,33 @@ exports[`getClassList 1`] = ` "transition", "transition-all", "transition-colors", - "transition-discrete", "transition-none", - "transition-normal", "transition-opacity", "transition-shadow", "transition-transform", - "translate-0", "translate-0.5", "translate-1", - "translate-1.5", - "translate-2", - "translate-2.5", "translate-3", - "translate-3.5", "translate-3d", "translate-4", - "translate-5", - "translate-6", - "translate-7", - "translate-8", - "translate-9", - "translate-10", - "translate-11", - "translate-12", - "translate-14", - "translate-16", - "translate-20", - "translate-24", - "translate-28", - "translate-32", - "translate-36", - "translate-40", - "translate-44", - "translate-48", - "translate-52", - "translate-56", - "translate-60", - "translate-64", - "translate-72", - "translate-80", - "translate-96", - "translate-1/2", - "translate-1/3", - "translate-1/4", - "translate-1/5", - "translate-1/6", - "translate-1/12", - "translate-2/3", - "translate-2/4", - "translate-2/5", - "translate-2/6", - "translate-2/12", - "translate-3/4", - "translate-3/5", - "translate-3/6", - "translate-3/12", - "translate-4/5", - "translate-4/6", - "translate-4/12", - "translate-5/6", - "translate-5/12", - "translate-6/12", - "translate-7/12", - "translate-8/12", - "translate-9/12", - "translate-10/12", - "translate-11/12", "translate-full", "translate-none", - "translate-px", - "translate-x-0", "translate-x-0.5", "translate-x-1", - "translate-x-1.5", - "translate-x-2", - "translate-x-2.5", "translate-x-3", - "translate-x-3.5", "translate-x-4", - "translate-x-5", - "translate-x-6", - "translate-x-7", - "translate-x-8", - "translate-x-9", - "translate-x-10", - "translate-x-11", - "translate-x-12", - "translate-x-14", - "translate-x-16", - "translate-x-20", - "translate-x-24", - "translate-x-28", - "translate-x-32", - "translate-x-36", - "translate-x-40", - "translate-x-44", - "translate-x-48", - "translate-x-52", - "translate-x-56", - "translate-x-60", - "translate-x-64", - "translate-x-72", - "translate-x-80", - "translate-x-96", - "translate-x-1/2", - "translate-x-1/3", - "translate-x-1/4", - "translate-x-1/5", - "translate-x-1/6", - "translate-x-1/12", - "translate-x-2/3", - "translate-x-2/4", - "translate-x-2/5", - "translate-x-2/6", - "translate-x-2/12", - "translate-x-3/4", - "translate-x-3/5", - "translate-x-3/6", - "translate-x-3/12", - "translate-x-4/5", - "translate-x-4/6", - "translate-x-4/12", - "translate-x-5/6", - "translate-x-5/12", - "translate-x-6/12", - "translate-x-7/12", - "translate-x-8/12", - "translate-x-9/12", - "translate-x-10/12", - "translate-x-11/12", "translate-x-full", "translate-x-px", - "translate-y-0", "translate-y-0.5", "translate-y-1", - "translate-y-1.5", - "translate-y-2", - "translate-y-2.5", "translate-y-3", - "translate-y-3.5", "translate-y-4", - "translate-y-5", - "translate-y-6", - "translate-y-7", - "translate-y-8", - "translate-y-9", - "translate-y-10", - "translate-y-11", - "translate-y-12", - "translate-y-14", - "translate-y-16", - "translate-y-20", - "translate-y-24", - "translate-y-28", - "translate-y-32", - "translate-y-36", - "translate-y-40", - "translate-y-44", - "translate-y-48", - "translate-y-52", - "translate-y-56", - "translate-y-60", - "translate-y-64", - "translate-y-72", - "translate-y-80", - "translate-y-96", - "translate-y-1/2", - "translate-y-1/3", - "translate-y-1/4", - "translate-y-1/5", - "translate-y-1/6", - "translate-y-1/12", - "translate-y-2/3", - "translate-y-2/4", - "translate-y-2/5", - "translate-y-2/6", - "translate-y-2/12", - "translate-y-3/4", - "translate-y-3/5", - "translate-y-3/6", - "translate-y-3/12", - "translate-y-4/5", - "translate-y-4/6", - "translate-y-4/12", - "translate-y-5/6", - "translate-y-5/12", - "translate-y-6/12", - "translate-y-7/12", - "translate-y-8/12", - "translate-y-9/12", - "translate-y-10/12", - "translate-y-11/12", "translate-y-full", "translate-y-px", - "translate-z-0", "translate-z-0.5", "translate-z-1", - "translate-z-1.5", - "translate-z-2", - "translate-z-2.5", "translate-z-3", - "translate-z-3.5", "translate-z-4", - "translate-z-5", - "translate-z-6", - "translate-z-7", - "translate-z-8", - "translate-z-9", - "translate-z-10", - "translate-z-11", - "translate-z-12", - "translate-z-14", - "translate-z-16", - "translate-z-20", - "translate-z-24", - "translate-z-28", - "translate-z-32", - "translate-z-36", - "translate-z-40", - "translate-z-44", - "translate-z-48", - "translate-z-52", - "translate-z-56", - "translate-z-60", - "translate-z-64", - "translate-z-72", - "translate-z-80", - "translate-z-96", "translate-z-px", "truncate", "underline", @@ -12229,8 +3716,8 @@ exports[`getClassList 1`] = ` "underline-offset-auto", "uppercase", "via-0%", - "via-5%", "via-10%", + "via-100%", "via-15%", "via-20%", "via-25%", @@ -12238,6 +3725,7 @@ exports[`getClassList 1`] = ` "via-35%", "via-40%", "via-45%", + "via-5%", "via-50%", "via-55%", "via-60%", @@ -12248,7 +3736,6 @@ exports[`getClassList 1`] = ` "via-85%", "via-90%", "via-95%", - "via-100%", "via-current", "via-current/0", "via-current/5", @@ -12271,7 +3758,6 @@ exports[`getClassList 1`] = ` "via-current/90", "via-current/95", "via-current/100", - "via-current/background", "via-inherit", "via-inherit/0", "via-inherit/5", @@ -12294,7 +3780,6 @@ exports[`getClassList 1`] = ` "via-inherit/90", "via-inherit/95", "via-inherit/100", - "via-inherit/background", "via-none", "via-transparent", "via-transparent/0", @@ -12318,82 +3803,20 @@ exports[`getClassList 1`] = ` "via-transparent/90", "via-transparent/95", "via-transparent/100", - "via-transparent/background", "visible", - "w-0", "w-0.5", "w-1", - "w-1.5", - "w-2", - "w-2.5", "w-3", - "w-3.5", "w-4", - "w-5", - "w-6", - "w-7", - "w-8", - "w-9", - "w-10", - "w-11", - "w-12", - "w-14", - "w-16", - "w-20", - "w-24", - "w-28", - "w-32", - "w-36", - "w-40", - "w-44", - "w-48", - "w-52", - "w-56", - "w-60", - "w-64", - "w-72", - "w-80", - "w-96", + "w-4", "w-auto", - "w-dvh", "w-dvw", "w-fit", "w-full", - "w-lg", - "w-lvh", "w-lvw", "w-max", - "w-md", - "w-1/2", - "w-1/3", - "w-1/4", - "w-1/5", - "w-1/6", - "w-1/12", - "w-2/3", - "w-2/4", - "w-2/5", - "w-2/6", - "w-2/12", - "w-3/4", - "w-3/5", - "w-3/6", - "w-3/12", - "w-4/5", - "w-4/6", - "w-4/12", - "w-5/6", - "w-5/12", - "w-6/12", - "w-7/12", - "w-8/12", - "w-9/12", - "w-10/12", - "w-11/12", "w-min", - "w-px", "w-screen", - "w-svh", "w-svw", "whitespace-break-spaces", "whitespace-normal", @@ -12405,9 +3828,6 @@ exports[`getClassList 1`] = ` "will-change-contents", "will-change-scroll", "will-change-transform", - "wrap-anywhere", - "wrap-break-word", - "wrap-normal", "z-0", "z-10", "z-20", @@ -12415,21 +3835,18 @@ 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", ] `; exports[`getVariants 1`] = ` [ + { + "hasDash": true, + "isArbitrary": false, + "name": "force", + "selectors": [Function], + "values": [], + }, { "hasDash": true, "isArbitrary": false, @@ -12437,91 +3854,12 @@ exports[`getVariants 1`] = ` "selectors": [Function], "values": [], }, - { - "hasDash": true, - "isArbitrary": false, - "name": "**", - "selectors": [Function], - "values": [], - }, { "hasDash": true, "isArbitrary": true, "name": "not", "selectors": [Function], - "values": [ - "not", - "group", - "peer", - "first", - "last", - "only", - "odd", - "even", - "first-of-type", - "last-of-type", - "only-of-type", - "visited", - "target", - "open", - "default", - "checked", - "indeterminate", - "placeholder-shown", - "autofill", - "optional", - "required", - "valid", - "invalid", - "user-valid", - "user-invalid", - "in-range", - "out-of-range", - "read-only", - "empty", - "focus-within", - "hover", - "focus", - "focus-visible", - "active", - "enabled", - "disabled", - "inert", - "in", - "has", - "aria", - "data", - "nth", - "nth-last", - "nth-of-type", - "nth-last-of-type", - "supports", - "motion-safe", - "motion-reduce", - "contrast-more", - "contrast-less", - "max", - "sm", - "min", - "@max", - "@", - "@min", - "portrait", - "landscape", - "ltr", - "rtl", - "dark", - "print", - "forced-colors", - "inverted-colors", - "pointer-none", - "pointer-coarse", - "pointer-fine", - "any-pointer-none", - "any-pointer-coarse", - "any-pointer-fine", - "noscript", - ], + "values": [], }, { "hasDash": true, @@ -12552,8 +3890,6 @@ exports[`getVariants 1`] = ` "required", "valid", "invalid", - "user-valid", - "user-invalid", "in-range", "out-of-range", "read-only", @@ -12566,7 +3902,6 @@ exports[`getVariants 1`] = ` "enabled", "disabled", "inert", - "in", "has", "aria", "data", @@ -12607,8 +3942,6 @@ exports[`getVariants 1`] = ` "required", "valid", "invalid", - "user-valid", - "user-invalid", "in-range", "out-of-range", "read-only", @@ -12621,7 +3954,6 @@ exports[`getVariants 1`] = ` "enabled", "disabled", "inert", - "in", "has", "aria", "data", @@ -12682,13 +4014,6 @@ exports[`getVariants 1`] = ` "selectors": [Function], "values": [], }, - { - "hasDash": true, - "isArbitrary": false, - "name": "details-content", - "selectors": [Function], - "values": [], - }, { "hasDash": true, "isArbitrary": false, @@ -12843,20 +4168,6 @@ exports[`getVariants 1`] = ` "selectors": [Function], "values": [], }, - { - "hasDash": true, - "isArbitrary": false, - "name": "user-valid", - "selectors": [Function], - "values": [], - }, - { - "hasDash": true, - "isArbitrary": false, - "name": "user-invalid", - "selectors": [Function], - "values": [], - }, { "hasDash": true, "isArbitrary": false, @@ -12941,61 +4252,6 @@ exports[`getVariants 1`] = ` "selectors": [Function], "values": [], }, - { - "hasDash": true, - "isArbitrary": true, - "name": "in", - "selectors": [Function], - "values": [ - "not", - "group", - "peer", - "first", - "last", - "only", - "odd", - "even", - "first-of-type", - "last-of-type", - "only-of-type", - "visited", - "target", - "open", - "default", - "checked", - "indeterminate", - "placeholder-shown", - "autofill", - "optional", - "required", - "valid", - "invalid", - "user-valid", - "user-invalid", - "in-range", - "out-of-range", - "read-only", - "empty", - "focus-within", - "hover", - "focus", - "focus-visible", - "active", - "enabled", - "disabled", - "inert", - "in", - "has", - "aria", - "data", - "nth", - "nth-last", - "nth-of-type", - "nth-last-of-type", - "ltr", - "rtl", - ], - }, { "hasDash": true, "isArbitrary": true, @@ -13025,8 +4281,6 @@ exports[`getVariants 1`] = ` "required", "valid", "invalid", - "user-valid", - "user-invalid", "in-range", "out-of-range", "read-only", @@ -13039,7 +4293,6 @@ exports[`getVariants 1`] = ` "enabled", "disabled", "inert", - "in", "has", "aria", "data", @@ -13169,19 +4422,15 @@ exports[`getVariants 1`] = ` "name": "@max", "selectors": [Function], "values": [ - "md", - "lg", + "4", ], }, { - "hasDash": false, + "hasDash": true, "isArbitrary": true, "name": "@", "selectors": [Function], - "values": [ - "md", - "lg", - ], + "values": [], }, { "hasDash": true, @@ -13189,8 +4438,7 @@ exports[`getVariants 1`] = ` "name": "@min", "selectors": [Function], "values": [ - "md", - "lg", + "4", ], }, { @@ -13249,61 +4497,5 @@ exports[`getVariants 1`] = ` "selectors": [Function], "values": [], }, - { - "hasDash": true, - "isArbitrary": false, - "name": "inverted-colors", - "selectors": [Function], - "values": [], - }, - { - "hasDash": true, - "isArbitrary": false, - "name": "pointer-none", - "selectors": [Function], - "values": [], - }, - { - "hasDash": true, - "isArbitrary": false, - "name": "pointer-coarse", - "selectors": [Function], - "values": [], - }, - { - "hasDash": true, - "isArbitrary": false, - "name": "pointer-fine", - "selectors": [Function], - "values": [], - }, - { - "hasDash": true, - "isArbitrary": false, - "name": "any-pointer-none", - "selectors": [Function], - "values": [], - }, - { - "hasDash": true, - "isArbitrary": false, - "name": "any-pointer-coarse", - "selectors": [Function], - "values": [], - }, - { - "hasDash": true, - "isArbitrary": false, - "name": "any-pointer-fine", - "selectors": [Function], - "values": [], - }, - { - "hasDash": true, - "isArbitrary": false, - "name": "noscript", - "selectors": [Function], - "values": [], - }, ] `; diff --git a/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap b/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap index 43e8fdf6f..983c17919 100644 --- a/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap @@ -1,18 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`border-* 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 { +":root { + --radius-none: 0px; + --radius-full: 9999px; + --radius-sm: .125rem; --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; } .border { @@ -40,26 +33,11 @@ exports[`border-* 1`] = ` border-width: 123px; } -.border-\\[0_1\\] { - border-style: var(--tw-border-style); - border-width: 0 1px; -} - -.border-\\[0_2px_0_2px\\] { - border-style: var(--tw-border-style); - border-width: 0 2px; -} - .border-\\[12px\\] { border-style: var(--tw-border-style); border-width: 12px; } -.border-\\[12px_8px\\] { - border-style: var(--tw-border-style); - border-width: 12px 8px; -} - .border-\\[length\\:var\\(--my-width\\)\\], .border-\\[line-width\\:var\\(--my-width\\)\\] { border-style: var(--tw-border-style); border-width: var(--my-width); @@ -80,51 +58,36 @@ exports[`border-* 1`] = ` border-width: thin; } -.border-\\[thin_2px\\] { - border-style: var(--tw-border-style); - border-width: thin 2px; -} - .border-\\[\\#0088cc\\] { border-color: #08c; } .border-\\[\\#0088cc\\]\\/50 { - border-color: oklab(59.9824% -.067 -.124 / .5); + border-color: #0088cc80; } -.border-\\[color\\:var\\(--my-color\\)\\], .border-\\[color\\:var\\(--my-color\\)\\]\\/50 { +.border-\\[color\\:var\\(--my-color\\)\\] { border-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-\\[color\\:var\\(--my-color\\)\\]\\/50 { + border-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-\\[var\\(--my-color\\)\\], .border-\\[var\\(--my-color\\)\\]\\/50 { +.border-\\[var\\(--my-color\\)\\] { border-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-\\[var\\(--my-color\\)\\]\\/50 { - border-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-\\[var\\(--my-color\\)\\]\\/50 { + border-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-blue-500 { - border-color: var(--border-color-blue-500); -} - -.border-current, .border-current\\/50 { +.border-current { border-color: currentColor; } -@supports (color: color-mix(in lab, red, red)) { - .border-current\\/50 { - border-color: color-mix(in oklab, currentcolor 50%, transparent); - } +.border-current\\/50 { + border-color: color-mix(in srgb, currentColor 50%, transparent); } .border-inherit { @@ -132,74 +95,38 @@ exports[`border-* 1`] = ` } .border-red-500 { - border-color: var(--color-red-500); -} - -.border-red-500\\/2\\.5 { - border-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-red-500\\/2\\.5 { - border-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-red-500\\/2\\.25 { - border-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-red-500\\/2\\.25 { - border-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-red-500\\/2\\.75 { - border-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-red-500\\/2\\.75 { - border-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } + border-color: var(--color-red-500, #ef4444); } .border-red-500\\/50 { - border-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-red-500\\/50 { - border-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } + border-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); } .border-transparent { border-color: #0000; } -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -" -`; - -exports[`border-b-* 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)))) { +@supports (-moz-orient: inline) { + @layer base { *, :before, :after, ::backdrop { --tw-border-style: solid; } } } -:root, :host { +@property --tw-border-style { + syntax: ""; + inherits: false; + initial-value: solid; +}" +`; + +exports[`border-b-* 1`] = ` +":root { + --radius-none: 0px; + --radius-full: 9999px; + --radius-sm: .125rem; --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; } .border-b { @@ -227,26 +154,11 @@ exports[`border-b-* 1`] = ` border-bottom-width: 123px; } -.border-b-\\[0_1\\] { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 0 1; -} - -.border-b-\\[0_2px_0_2px\\] { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 0 2px 0 2px; -} - .border-b-\\[12px\\] { border-bottom-style: var(--tw-border-style); border-bottom-width: 12px; } -.border-b-\\[12px_8px\\] { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 12px 8px; -} - .border-b-\\[length\\:var\\(--my-width\\)\\], .border-b-\\[line-width\\:var\\(--my-width\\)\\] { border-bottom-style: var(--tw-border-style); border-bottom-width: var(--my-width); @@ -267,51 +179,36 @@ exports[`border-b-* 1`] = ` border-bottom-width: thin; } -.border-b-\\[thin_2px\\] { - border-bottom-style: var(--tw-border-style); - border-bottom-width: thin 2px; -} - .border-b-\\[\\#0088cc\\] { border-bottom-color: #08c; } .border-b-\\[\\#0088cc\\]\\/50 { - border-bottom-color: oklab(59.9824% -.067 -.124 / .5); + border-bottom-color: #0088cc80; } -.border-b-\\[color\\:var\\(--my-color\\)\\], .border-b-\\[color\\:var\\(--my-color\\)\\]\\/50 { +.border-b-\\[color\\:var\\(--my-color\\)\\] { border-bottom-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-b-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-bottom-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-b-\\[color\\:var\\(--my-color\\)\\]\\/50 { + border-bottom-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-b-\\[var\\(--my-color\\)\\], .border-b-\\[var\\(--my-color\\)\\]\\/50 { +.border-b-\\[var\\(--my-color\\)\\] { border-bottom-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-b-\\[var\\(--my-color\\)\\]\\/50 { - border-bottom-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-b-\\[var\\(--my-color\\)\\]\\/50 { + border-bottom-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-b-blue-500 { - border-bottom-color: var(--border-color-blue-500); -} - -.border-b-current, .border-b-current\\/50 { +.border-b-current { border-bottom-color: currentColor; } -@supports (color: color-mix(in lab, red, red)) { - .border-b-current\\/50 { - border-bottom-color: color-mix(in oklab, currentcolor 50%, transparent); - } +.border-b-current\\/50 { + border-bottom-color: color-mix(in srgb, currentColor 50%, transparent); } .border-b-inherit { @@ -319,448 +216,38 @@ exports[`border-b-* 1`] = ` } .border-b-red-500 { - border-bottom-color: var(--color-red-500); -} - -.border-b-red-500\\/2\\.5 { - border-bottom-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-b-red-500\\/2\\.5 { - border-bottom-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-b-red-500\\/2\\.25 { - border-bottom-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-b-red-500\\/2\\.25 { - border-bottom-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-b-red-500\\/2\\.75 { - border-bottom-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-b-red-500\\/2\\.75 { - border-bottom-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } + border-bottom-color: var(--color-red-500, #ef4444); } .border-b-red-500\\/50 { - border-bottom-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-b-red-500\\/50 { - border-bottom-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } + border-bottom-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); } .border-b-transparent { border-bottom-color: #0000; } -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -" -`; - -exports[`border-be-* 1`] = ` -" -@layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { +@supports (-moz-orient: inline) { + @layer base { *, :before, :after, ::backdrop { --tw-border-style: solid; } } } -:root, :host { - --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; -} - -.border-be { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 1px; -} - -.border-be-0 { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 0; -} - -.border-be-2 { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 2px; -} - -.border-be-4 { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 4px; -} - -.border-be-123 { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 123px; -} - -.border-be-\\[0_1\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 0 1; -} - -.border-be-\\[0_2px_0_2px\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 0 2px 0 2px; -} - -.border-be-\\[12px\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 12px; -} - -.border-be-\\[12px_8px\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: 12px 8px; -} - -.border-be-\\[length\\:var\\(--my-width\\)\\], .border-be-\\[line-width\\:var\\(--my-width\\)\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: var(--my-width); -} - -.border-be-\\[medium\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: medium; -} - -.border-be-\\[thick\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: thick; -} - -.border-be-\\[thin\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: thin; -} - -.border-be-\\[thin_2px\\] { - border-block-end-style: var(--tw-border-style); - border-block-end-width: thin 2px; -} - -.border-be-\\[\\#0088cc\\] { - border-block-end-color: #08c; -} - -.border-be-\\[\\#0088cc\\]\\/50 { - border-block-end-color: oklab(59.9824% -.067 -.124 / .5); -} - -.border-be-\\[color\\:var\\(--my-color\\)\\], .border-be-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-block-end-color: var(--my-color); -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-block-end-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } -} - -.border-be-\\[var\\(--my-color\\)\\], .border-be-\\[var\\(--my-color\\)\\]\\/50 { - border-block-end-color: var(--my-color); -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-\\[var\\(--my-color\\)\\]\\/50 { - border-block-end-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } -} - -.border-be-blue-500 { - border-block-end-color: var(--border-color-blue-500); -} - -.border-be-current, .border-be-current\\/50 { - border-block-end-color: currentColor; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-current\\/50 { - border-block-end-color: color-mix(in oklab, currentcolor 50%, transparent); - } -} - -.border-be-inherit { - border-block-end-color: inherit; -} - -.border-be-red-500 { - border-block-end-color: var(--color-red-500); -} - -.border-be-red-500\\/2\\.5 { - border-block-end-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-red-500\\/2\\.5 { - border-block-end-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-be-red-500\\/2\\.25 { - border-block-end-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-red-500\\/2\\.25 { - border-block-end-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-be-red-500\\/2\\.75 { - border-block-end-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-red-500\\/2\\.75 { - border-block-end-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } -} - -.border-be-red-500\\/50 { - border-block-end-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-be-red-500\\/50 { - border-block-end-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } -} - -.border-be-transparent { - border-block-end-color: #0000; -} - @property --tw-border-style { - syntax: "*"; + 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 { +":root { + --radius-none: 0px; + --radius-full: 9999px; + --radius-sm: .125rem; --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; } .border-e { @@ -788,26 +275,11 @@ exports[`border-e-* 1`] = ` border-inline-end-width: 123px; } -.border-e-\\[0_1\\] { - border-inline-end-style: var(--tw-border-style); - border-inline-end-width: 0 1; -} - -.border-e-\\[0_2px_0_2px\\] { - border-inline-end-style: var(--tw-border-style); - border-inline-end-width: 0 2px 0 2px; -} - .border-e-\\[12px\\] { border-inline-end-style: var(--tw-border-style); border-inline-end-width: 12px; } -.border-e-\\[12px_8px\\] { - border-inline-end-style: var(--tw-border-style); - border-inline-end-width: 12px 8px; -} - .border-e-\\[length\\:var\\(--my-width\\)\\], .border-e-\\[line-width\\:var\\(--my-width\\)\\] { border-inline-end-style: var(--tw-border-style); border-inline-end-width: var(--my-width); @@ -828,51 +300,36 @@ exports[`border-e-* 1`] = ` border-inline-end-width: thin; } -.border-e-\\[thin_2px\\] { - border-inline-end-style: var(--tw-border-style); - border-inline-end-width: thin 2px; -} - .border-e-\\[\\#0088cc\\] { border-inline-end-color: #08c; } .border-e-\\[\\#0088cc\\]\\/50 { - border-inline-end-color: oklab(59.9824% -.067 -.124 / .5); + border-inline-end-color: #0088cc80; } -.border-e-\\[color\\:var\\(--my-color\\)\\], .border-e-\\[color\\:var\\(--my-color\\)\\]\\/50 { +.border-e-\\[color\\:var\\(--my-color\\)\\] { border-inline-end-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-e-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-inline-end-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-e-\\[color\\:var\\(--my-color\\)\\]\\/50 { + border-inline-end-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-e-\\[var\\(--my-color\\)\\], .border-e-\\[var\\(--my-color\\)\\]\\/50 { +.border-e-\\[var\\(--my-color\\)\\] { border-inline-end-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-e-\\[var\\(--my-color\\)\\]\\/50 { - border-inline-end-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-e-\\[var\\(--my-color\\)\\]\\/50 { + border-inline-end-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-e-blue-500 { - border-inline-end-color: var(--border-color-blue-500); -} - -.border-e-current, .border-e-current\\/50 { +.border-e-current { border-inline-end-color: currentColor; } -@supports (color: color-mix(in lab, red, red)) { - .border-e-current\\/50 { - border-inline-end-color: color-mix(in oklab, currentcolor 50%, transparent); - } +.border-e-current\\/50 { + border-inline-end-color: color-mix(in srgb, currentColor 50%, transparent); } .border-e-inherit { @@ -880,74 +337,38 @@ exports[`border-e-* 1`] = ` } .border-e-red-500 { - border-inline-end-color: var(--color-red-500); -} - -.border-e-red-500\\/2\\.5 { - border-inline-end-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-e-red-500\\/2\\.5 { - border-inline-end-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-e-red-500\\/2\\.25 { - border-inline-end-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-e-red-500\\/2\\.25 { - border-inline-end-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-e-red-500\\/2\\.75 { - border-inline-end-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-e-red-500\\/2\\.75 { - border-inline-end-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } + border-inline-end-color: var(--color-red-500, #ef4444); } .border-e-red-500\\/50 { - border-inline-end-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-e-red-500\\/50 { - border-inline-end-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } + border-inline-end-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); } .border-e-transparent { border-inline-end-color: #0000; } -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -" -`; - -exports[`border-l-* 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)))) { +@supports (-moz-orient: inline) { + @layer base { *, :before, :after, ::backdrop { --tw-border-style: solid; } } } -:root, :host { +@property --tw-border-style { + syntax: ""; + inherits: false; + initial-value: solid; +}" +`; + +exports[`border-l-* 1`] = ` +":root { + --radius-none: 0px; + --radius-full: 9999px; + --radius-sm: .125rem; --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; } .border-l { @@ -975,26 +396,11 @@ exports[`border-l-* 1`] = ` border-left-width: 123px; } -.border-l-\\[0_1\\] { - border-left-style: var(--tw-border-style); - border-left-width: 0 1; -} - -.border-l-\\[0_2px_0_2px\\] { - border-left-style: var(--tw-border-style); - border-left-width: 0 2px 0 2px; -} - .border-l-\\[12px\\] { border-left-style: var(--tw-border-style); border-left-width: 12px; } -.border-l-\\[12px_8px\\] { - border-left-style: var(--tw-border-style); - border-left-width: 12px 8px; -} - .border-l-\\[length\\:var\\(--my-width\\)\\], .border-l-\\[line-width\\:var\\(--my-width\\)\\] { border-left-style: var(--tw-border-style); border-left-width: var(--my-width); @@ -1015,51 +421,36 @@ exports[`border-l-* 1`] = ` border-left-width: thin; } -.border-l-\\[thin_2px\\] { - border-left-style: var(--tw-border-style); - border-left-width: thin 2px; -} - .border-l-\\[\\#0088cc\\] { border-left-color: #08c; } .border-l-\\[\\#0088cc\\]\\/50 { - border-left-color: oklab(59.9824% -.067 -.124 / .5); + border-left-color: #0088cc80; } -.border-l-\\[color\\:var\\(--my-color\\)\\], .border-l-\\[color\\:var\\(--my-color\\)\\]\\/50 { +.border-l-\\[color\\:var\\(--my-color\\)\\] { border-left-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-l-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-left-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-l-\\[color\\:var\\(--my-color\\)\\]\\/50 { + border-left-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-l-\\[var\\(--my-color\\)\\], .border-l-\\[var\\(--my-color\\)\\]\\/50 { +.border-l-\\[var\\(--my-color\\)\\] { border-left-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-l-\\[var\\(--my-color\\)\\]\\/50 { - border-left-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-l-\\[var\\(--my-color\\)\\]\\/50 { + border-left-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-l-blue-500 { - border-left-color: var(--border-color-blue-500); -} - -.border-l-current, .border-l-current\\/50 { +.border-l-current { border-left-color: currentColor; } -@supports (color: color-mix(in lab, red, red)) { - .border-l-current\\/50 { - border-left-color: color-mix(in oklab, currentcolor 50%, transparent); - } +.border-l-current\\/50 { + border-left-color: color-mix(in srgb, currentColor 50%, transparent); } .border-l-inherit { @@ -1067,74 +458,38 @@ exports[`border-l-* 1`] = ` } .border-l-red-500 { - border-left-color: var(--color-red-500); -} - -.border-l-red-500\\/2\\.5 { - border-left-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-l-red-500\\/2\\.5 { - border-left-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-l-red-500\\/2\\.25 { - border-left-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-l-red-500\\/2\\.25 { - border-left-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-l-red-500\\/2\\.75 { - border-left-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-l-red-500\\/2\\.75 { - border-left-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } + border-left-color: var(--color-red-500, #ef4444); } .border-l-red-500\\/50 { - border-left-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-l-red-500\\/50 { - border-left-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } + border-left-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); } .border-l-transparent { border-left-color: #0000; } -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -" -`; - -exports[`border-r-* 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)))) { +@supports (-moz-orient: inline) { + @layer base { *, :before, :after, ::backdrop { --tw-border-style: solid; } } } -:root, :host { +@property --tw-border-style { + syntax: ""; + inherits: false; + initial-value: solid; +}" +`; + +exports[`border-r-* 1`] = ` +":root { + --radius-none: 0px; + --radius-full: 9999px; + --radius-sm: .125rem; --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; } .border-r { @@ -1162,26 +517,11 @@ exports[`border-r-* 1`] = ` border-right-width: 123px; } -.border-r-\\[0_1\\] { - border-right-style: var(--tw-border-style); - border-right-width: 0 1; -} - -.border-r-\\[0_2px_0_2px\\] { - border-right-style: var(--tw-border-style); - border-right-width: 0 2px 0 2px; -} - .border-r-\\[12px\\] { border-right-style: var(--tw-border-style); border-right-width: 12px; } -.border-r-\\[12px_8px\\] { - border-right-style: var(--tw-border-style); - border-right-width: 12px 8px; -} - .border-r-\\[length\\:var\\(--my-width\\)\\], .border-r-\\[line-width\\:var\\(--my-width\\)\\] { border-right-style: var(--tw-border-style); border-right-width: var(--my-width); @@ -1202,51 +542,36 @@ exports[`border-r-* 1`] = ` border-right-width: thin; } -.border-r-\\[thin_2px\\] { - border-right-style: var(--tw-border-style); - border-right-width: thin 2px; -} - .border-r-\\[\\#0088cc\\] { border-right-color: #08c; } .border-r-\\[\\#0088cc\\]\\/50 { - border-right-color: oklab(59.9824% -.067 -.124 / .5); + border-right-color: #0088cc80; } -.border-r-\\[color\\:var\\(--my-color\\)\\], .border-r-\\[color\\:var\\(--my-color\\)\\]\\/50 { +.border-r-\\[color\\:var\\(--my-color\\)\\] { border-right-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-r-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-right-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-r-\\[color\\:var\\(--my-color\\)\\]\\/50 { + border-right-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-r-\\[var\\(--my-color\\)\\], .border-r-\\[var\\(--my-color\\)\\]\\/50 { +.border-r-\\[var\\(--my-color\\)\\] { border-right-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-r-\\[var\\(--my-color\\)\\]\\/50 { - border-right-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-r-\\[var\\(--my-color\\)\\]\\/50 { + border-right-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-r-blue-500 { - border-right-color: var(--border-color-blue-500); -} - -.border-r-current, .border-r-current\\/50 { +.border-r-current { border-right-color: currentColor; } -@supports (color: color-mix(in lab, red, red)) { - .border-r-current\\/50 { - border-right-color: color-mix(in oklab, currentcolor 50%, transparent); - } +.border-r-current\\/50 { + border-right-color: color-mix(in srgb, currentColor 50%, transparent); } .border-r-inherit { @@ -1254,74 +579,38 @@ exports[`border-r-* 1`] = ` } .border-r-red-500 { - border-right-color: var(--color-red-500); -} - -.border-r-red-500\\/2\\.5 { - border-right-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-r-red-500\\/2\\.5 { - border-right-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-r-red-500\\/2\\.25 { - border-right-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-r-red-500\\/2\\.25 { - border-right-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-r-red-500\\/2\\.75 { - border-right-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-r-red-500\\/2\\.75 { - border-right-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } + border-right-color: var(--color-red-500, #ef4444); } .border-r-red-500\\/50 { - border-right-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-r-red-500\\/50 { - border-right-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } + border-right-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); } .border-r-transparent { border-right-color: #0000; } -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -" -`; - -exports[`border-s-* 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)))) { +@supports (-moz-orient: inline) { + @layer base { *, :before, :after, ::backdrop { --tw-border-style: solid; } } } -:root, :host { +@property --tw-border-style { + syntax: ""; + inherits: false; + initial-value: solid; +}" +`; + +exports[`border-s-* 1`] = ` +":root { + --radius-none: 0px; + --radius-full: 9999px; + --radius-sm: .125rem; --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; } .border-s { @@ -1349,26 +638,11 @@ exports[`border-s-* 1`] = ` border-inline-start-width: 123px; } -.border-s-\\[0_1\\] { - border-inline-start-style: var(--tw-border-style); - border-inline-start-width: 0 1; -} - -.border-s-\\[0_2px_0_2px\\] { - border-inline-start-style: var(--tw-border-style); - border-inline-start-width: 0 2px 0 2px; -} - .border-s-\\[12px\\] { border-inline-start-style: var(--tw-border-style); border-inline-start-width: 12px; } -.border-s-\\[12px_8px\\] { - border-inline-start-style: var(--tw-border-style); - border-inline-start-width: 12px 8px; -} - .border-s-\\[length\\:var\\(--my-width\\)\\], .border-s-\\[line-width\\:var\\(--my-width\\)\\] { border-inline-start-style: var(--tw-border-style); border-inline-start-width: var(--my-width); @@ -1389,51 +663,36 @@ exports[`border-s-* 1`] = ` border-inline-start-width: thin; } -.border-s-\\[thin_2px\\] { - border-inline-start-style: var(--tw-border-style); - border-inline-start-width: thin 2px; -} - .border-s-\\[\\#0088cc\\] { border-inline-start-color: #08c; } .border-s-\\[\\#0088cc\\]\\/50 { - border-inline-start-color: oklab(59.9824% -.067 -.124 / .5); + border-inline-start-color: #0088cc80; } -.border-s-\\[color\\:var\\(--my-color\\)\\], .border-s-\\[color\\:var\\(--my-color\\)\\]\\/50 { +.border-s-\\[color\\:var\\(--my-color\\)\\] { border-inline-start-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-s-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-inline-start-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-s-\\[color\\:var\\(--my-color\\)\\]\\/50 { + border-inline-start-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-s-\\[var\\(--my-color\\)\\], .border-s-\\[var\\(--my-color\\)\\]\\/50 { +.border-s-\\[var\\(--my-color\\)\\] { border-inline-start-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-s-\\[var\\(--my-color\\)\\]\\/50 { - border-inline-start-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-s-\\[var\\(--my-color\\)\\]\\/50 { + border-inline-start-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-s-blue-500 { - border-inline-start-color: var(--border-color-blue-500); -} - -.border-s-current, .border-s-current\\/50 { +.border-s-current { border-inline-start-color: currentColor; } -@supports (color: color-mix(in lab, red, red)) { - .border-s-current\\/50 { - border-inline-start-color: color-mix(in oklab, currentcolor 50%, transparent); - } +.border-s-current\\/50 { + border-inline-start-color: color-mix(in srgb, currentColor 50%, transparent); } .border-s-inherit { @@ -1441,74 +700,38 @@ exports[`border-s-* 1`] = ` } .border-s-red-500 { - border-inline-start-color: var(--color-red-500); -} - -.border-s-red-500\\/2\\.5 { - border-inline-start-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-s-red-500\\/2\\.5 { - border-inline-start-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-s-red-500\\/2\\.25 { - border-inline-start-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-s-red-500\\/2\\.25 { - border-inline-start-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-s-red-500\\/2\\.75 { - border-inline-start-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-s-red-500\\/2\\.75 { - border-inline-start-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } + border-inline-start-color: var(--color-red-500, #ef4444); } .border-s-red-500\\/50 { - border-inline-start-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-s-red-500\\/50 { - border-inline-start-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } + border-inline-start-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); } .border-s-transparent { border-inline-start-color: #0000; } -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -" -`; - -exports[`border-t-* 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)))) { +@supports (-moz-orient: inline) { + @layer base { *, :before, :after, ::backdrop { --tw-border-style: solid; } } } -:root, :host { +@property --tw-border-style { + syntax: ""; + inherits: false; + initial-value: solid; +}" +`; + +exports[`border-t-* 1`] = ` +":root { + --radius-none: 0px; + --radius-full: 9999px; + --radius-sm: .125rem; --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; } .border-t { @@ -1536,26 +759,11 @@ exports[`border-t-* 1`] = ` border-top-width: 123px; } -.border-t-\\[0_1\\] { - border-top-style: var(--tw-border-style); - border-top-width: 0 1; -} - -.border-t-\\[0_2px_0_2px\\] { - border-top-style: var(--tw-border-style); - border-top-width: 0 2px 0 2px; -} - .border-t-\\[12px\\] { border-top-style: var(--tw-border-style); border-top-width: 12px; } -.border-t-\\[12px_8px\\] { - border-top-style: var(--tw-border-style); - border-top-width: 12px 8px; -} - .border-t-\\[length\\:var\\(--my-width\\)\\], .border-t-\\[line-width\\:var\\(--my-width\\)\\] { border-top-style: var(--tw-border-style); border-top-width: var(--my-width); @@ -1576,51 +784,36 @@ exports[`border-t-* 1`] = ` border-top-width: thin; } -.border-t-\\[thin_2px\\] { - border-top-style: var(--tw-border-style); - border-top-width: thin 2px; -} - .border-t-\\[\\#0088cc\\] { border-top-color: #08c; } .border-t-\\[\\#0088cc\\]\\/50 { - border-top-color: oklab(59.9824% -.067 -.124 / .5); + border-top-color: #0088cc80; } -.border-t-\\[color\\:var\\(--my-color\\)\\], .border-t-\\[color\\:var\\(--my-color\\)\\]\\/50 { +.border-t-\\[color\\:var\\(--my-color\\)\\] { border-top-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-t-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-top-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-t-\\[color\\:var\\(--my-color\\)\\]\\/50 { + border-top-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-t-\\[var\\(--my-color\\)\\], .border-t-\\[var\\(--my-color\\)\\]\\/50 { +.border-t-\\[var\\(--my-color\\)\\] { border-top-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-t-\\[var\\(--my-color\\)\\]\\/50 { - border-top-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-t-\\[var\\(--my-color\\)\\]\\/50 { + border-top-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-t-blue-500 { - border-top-color: var(--border-color-blue-500); -} - -.border-t-current, .border-t-current\\/50 { +.border-t-current { border-top-color: currentColor; } -@supports (color: color-mix(in lab, red, red)) { - .border-t-current\\/50 { - border-top-color: color-mix(in oklab, currentcolor 50%, transparent); - } +.border-t-current\\/50 { + border-top-color: color-mix(in srgb, currentColor 50%, transparent); } .border-t-inherit { @@ -1628,431 +821,334 @@ exports[`border-t-* 1`] = ` } .border-t-red-500 { - border-top-color: var(--color-red-500); -} - -.border-t-red-500\\/2\\.5 { - border-top-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-t-red-500\\/2\\.5 { - border-top-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-t-red-500\\/2\\.25 { - border-top-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-t-red-500\\/2\\.25 { - border-top-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-t-red-500\\/2\\.75 { - border-top-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-t-red-500\\/2\\.75 { - border-top-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } + border-top-color: var(--color-red-500, #ef4444); } .border-t-red-500\\/50 { - border-top-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-t-red-500\\/50 { - border-top-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } + border-top-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); } .border-t-transparent { border-top-color: #0000; } +@supports (-moz-orient: inline) { + @layer base { + *, :before, :after, ::backdrop { + --tw-border-style: solid; + } + } +} + @property --tw-border-style { - syntax: "*"; + syntax: ""; inherits: false; initial-value: solid; -} -" +}" `; exports[`border-x-* 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 { +":root { + --radius-none: 0px; + --radius-full: 9999px; + --radius-sm: .125rem; --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; } .border-x { - border-inline-style: var(--tw-border-style); - border-inline-width: 1px; + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: 1px; + border-right-width: 1px; } .border-x-0 { - border-inline-style: var(--tw-border-style); - border-inline-width: 0; + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: 0; + border-right-width: 0; } .border-x-2 { - border-inline-style: var(--tw-border-style); - border-inline-width: 2px; + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: 2px; + border-right-width: 2px; } .border-x-4 { - border-inline-style: var(--tw-border-style); - border-inline-width: 4px; + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: 4px; + border-right-width: 4px; } .border-x-123 { - border-inline-style: var(--tw-border-style); - border-inline-width: 123px; -} - -.border-x-\\[0_1\\] { - border-inline-style: var(--tw-border-style); - border-inline-width: 0 1px; -} - -.border-x-\\[0_2px_0_2px\\] { - border-inline-style: var(--tw-border-style); - border-inline-width: 0 2px 0 2px; + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: 123px; + border-right-width: 123px; } .border-x-\\[12px\\] { - border-inline-style: var(--tw-border-style); - border-inline-width: 12px; -} - -.border-x-\\[12px_8px\\] { - border-inline-style: var(--tw-border-style); - border-inline-width: 12px 8px; + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: 12px; + border-right-width: 12px; } .border-x-\\[length\\:var\\(--my-width\\)\\], .border-x-\\[line-width\\:var\\(--my-width\\)\\] { - border-inline-style: var(--tw-border-style); - border-inline-width: var(--my-width); + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: var(--my-width); + border-right-width: var(--my-width); } .border-x-\\[medium\\] { - border-inline-style: var(--tw-border-style); - border-inline-width: medium; + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: medium; + border-right-width: medium; } .border-x-\\[thick\\] { - border-inline-style: var(--tw-border-style); - border-inline-width: thick; + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: thick; + border-right-width: thick; } .border-x-\\[thin\\] { - border-inline-style: var(--tw-border-style); - border-inline-width: thin; -} - -.border-x-\\[thin_2px\\] { - border-inline-style: var(--tw-border-style); - border-inline-width: thin 2px; + border-left-style: var(--tw-border-style); + border-right-style: var(--tw-border-style); + border-left-width: thin; + border-right-width: thin; } .border-x-\\[\\#0088cc\\] { - border-inline-color: #08c; + border-left-color: #08c; + border-right-color: #08c; } .border-x-\\[\\#0088cc\\]\\/50 { - border-inline-color: oklab(59.9824% -.067 -.124 / .5); + border-left-color: #0088cc80; + border-right-color: #0088cc80; } -.border-x-\\[color\\:var\\(--my-color\\)\\], .border-x-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-inline-color: var(--my-color); +.border-x-\\[color\\:var\\(--my-color\\)\\] { + border-left-color: var(--my-color); + border-right-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-x-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-inline-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-x-\\[color\\:var\\(--my-color\\)\\]\\/50 { + border-left-color: color-mix(in srgb, var(--my-color) 50%, transparent); + border-right-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-x-\\[var\\(--my-color\\)\\], .border-x-\\[var\\(--my-color\\)\\]\\/50 { - border-inline-color: var(--my-color); +.border-x-\\[var\\(--my-color\\)\\] { + border-left-color: var(--my-color); + border-right-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-x-\\[var\\(--my-color\\)\\]\\/50 { - border-inline-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-x-\\[var\\(--my-color\\)\\]\\/50 { + border-left-color: color-mix(in srgb, var(--my-color) 50%, transparent); + border-right-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-x-blue-500 { - border-inline-color: var(--border-color-blue-500); +.border-x-current { + border-left-color: currentColor; + border-right-color: currentColor; } -.border-x-current, .border-x-current\\/50 { - border-inline-color: currentColor; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-x-current\\/50 { - border-inline-color: color-mix(in oklab, currentcolor 50%, transparent); - } +.border-x-current\\/50 { + border-left-color: color-mix(in srgb, currentColor 50%, transparent); + border-right-color: color-mix(in srgb, currentColor 50%, transparent); } .border-x-inherit { - border-inline-color: inherit; + border-left-color: inherit; + border-right-color: inherit; } .border-x-red-500 { - border-inline-color: var(--color-red-500); -} - -.border-x-red-500\\/2\\.5 { - border-inline-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-x-red-500\\/2\\.5 { - border-inline-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-x-red-500\\/2\\.25 { - border-inline-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-x-red-500\\/2\\.25 { - border-inline-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-x-red-500\\/2\\.75 { - border-inline-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-x-red-500\\/2\\.75 { - border-inline-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } + border-left-color: var(--color-red-500, #ef4444); + border-right-color: var(--color-red-500, #ef4444); } .border-x-red-500\\/50 { - border-inline-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-x-red-500\\/50 { - border-inline-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } + border-left-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); + border-right-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); } .border-x-transparent { - border-inline-color: #0000; + border-left-color: #0000; + border-right-color: #0000; } -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -" -`; - -exports[`border-y-* 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)))) { +@supports (-moz-orient: inline) { + @layer base { *, :before, :after, ::backdrop { --tw-border-style: solid; } } } -:root, :host { +@property --tw-border-style { + syntax: ""; + inherits: false; + initial-value: solid; +}" +`; + +exports[`border-y-* 1`] = ` +":root { + --radius-none: 0px; + --radius-full: 9999px; + --radius-sm: .125rem; --color-red-500: #ef4444; - --border-color-blue-500: #3b82f6; } .border-y { - border-block-style: var(--tw-border-style); - border-block-width: 1px; + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: 1px; + border-bottom-width: 1px; } .border-y-0 { - border-block-style: var(--tw-border-style); - border-block-width: 0; + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: 0; + border-bottom-width: 0; } .border-y-2 { - border-block-style: var(--tw-border-style); - border-block-width: 2px; + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: 2px; + border-bottom-width: 2px; } .border-y-4 { - border-block-style: var(--tw-border-style); - border-block-width: 4px; + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: 4px; + border-bottom-width: 4px; } .border-y-123 { - border-block-style: var(--tw-border-style); - border-block-width: 123px; -} - -.border-y-\\[0_1\\] { - border-block-style: var(--tw-border-style); - border-block-width: 0 1px; -} - -.border-y-\\[0_2px_0_2px\\] { - border-block-style: var(--tw-border-style); - border-block-width: 0 2px 0 2px; + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: 123px; + border-bottom-width: 123px; } .border-y-\\[12px\\] { - border-block-style: var(--tw-border-style); - border-block-width: 12px; -} - -.border-y-\\[12px_8px\\] { - border-block-style: var(--tw-border-style); - border-block-width: 12px 8px; + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: 12px; + border-bottom-width: 12px; } .border-y-\\[length\\:var\\(--my-width\\)\\], .border-y-\\[line-width\\:var\\(--my-width\\)\\] { - border-block-style: var(--tw-border-style); - border-block-width: var(--my-width); + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: var(--my-width); + border-bottom-width: var(--my-width); } .border-y-\\[medium\\] { - border-block-style: var(--tw-border-style); - border-block-width: medium; + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: medium; + border-bottom-width: medium; } .border-y-\\[thick\\] { - border-block-style: var(--tw-border-style); - border-block-width: thick; + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: thick; + border-bottom-width: thick; } .border-y-\\[thin\\] { - border-block-style: var(--tw-border-style); - border-block-width: thin; -} - -.border-y-\\[thin_2px\\] { - border-block-style: var(--tw-border-style); - border-block-width: thin 2px; + border-top-style: var(--tw-border-style); + border-bottom-style: var(--tw-border-style); + border-top-width: thin; + border-bottom-width: thin; } .border-y-\\[\\#0088cc\\] { - border-block-color: #08c; + border-top-color: #08c; + border-bottom-color: #08c; } .border-y-\\[\\#0088cc\\]\\/50 { - border-block-color: oklab(59.9824% -.067 -.124 / .5); + border-top-color: #0088cc80; + border-bottom-color: #0088cc80; } -.border-y-\\[color\\:var\\(--my-color\\)\\], .border-y-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-block-color: var(--my-color); +.border-y-\\[color\\:var\\(--my-color\\)\\] { + border-top-color: var(--my-color); + border-bottom-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-y-\\[color\\:var\\(--my-color\\)\\]\\/50 { - border-block-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-y-\\[color\\:var\\(--my-color\\)\\]\\/50 { + border-top-color: color-mix(in srgb, var(--my-color) 50%, transparent); + border-bottom-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-y-\\[var\\(--my-color\\)\\], .border-y-\\[var\\(--my-color\\)\\]\\/50 { - border-block-color: var(--my-color); +.border-y-\\[var\\(--my-color\\)\\] { + border-top-color: var(--my-color); + border-bottom-color: var(--my-color); } -@supports (color: color-mix(in lab, red, red)) { - .border-y-\\[var\\(--my-color\\)\\]\\/50 { - border-block-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } +.border-y-\\[var\\(--my-color\\)\\]\\/50 { + border-top-color: color-mix(in srgb, var(--my-color) 50%, transparent); + border-bottom-color: color-mix(in srgb, var(--my-color) 50%, transparent); } -.border-y-blue-500 { - border-block-color: var(--border-color-blue-500); +.border-y-current { + border-top-color: currentColor; + border-bottom-color: currentColor; } -.border-y-current, .border-y-current\\/50 { - border-block-color: currentColor; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-y-current\\/50 { - border-block-color: color-mix(in oklab, currentcolor 50%, transparent); - } +.border-y-current\\/50 { + border-top-color: color-mix(in srgb, currentColor 50%, transparent); + border-bottom-color: color-mix(in srgb, currentColor 50%, transparent); } .border-y-inherit { - border-block-color: inherit; + border-top-color: inherit; + border-bottom-color: inherit; } .border-y-red-500 { - border-block-color: var(--color-red-500); -} - -.border-y-red-500\\/2\\.5 { - border-block-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-y-red-500\\/2\\.5 { - border-block-color: color-mix(in oklab, var(--color-red-500) 2.5%, transparent); - } -} - -.border-y-red-500\\/2\\.25 { - border-block-color: #ef444406; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-y-red-500\\/2\\.25 { - border-block-color: color-mix(in oklab, var(--color-red-500) 2.25%, transparent); - } -} - -.border-y-red-500\\/2\\.75 { - border-block-color: #ef444407; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-y-red-500\\/2\\.75 { - border-block-color: color-mix(in oklab, var(--color-red-500) 2.75%, transparent); - } + border-top-color: var(--color-red-500, #ef4444); + border-bottom-color: var(--color-red-500, #ef4444); } .border-y-red-500\\/50 { - border-block-color: #ef444480; -} - -@supports (color: color-mix(in lab, red, red)) { - .border-y-red-500\\/50 { - border-block-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } + border-top-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); + border-bottom-color: color-mix(in srgb, var(--color-red-500, #ef4444) 50%, transparent); } .border-y-transparent { - border-block-color: #0000; + border-top-color: #0000; + border-bottom-color: #0000; +} + +@supports (-moz-orient: inline) { + @layer base { + *, :before, :after, ::backdrop { + --tw-border-style: solid; + } + } } @property --tw-border-style { - syntax: "*"; + syntax: ""; inherits: false; initial-value: solid; -} -" +}" `; diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 2d5e5b847..0cbdf4940 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -1,363 +1,70 @@ -import { Features } from '.' -import { cloneAstNode, rule, toCss, type AstNode } from './ast' +import { walk, 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' +import { escape } from './utils/escape' export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { - let features = Features.None + walk(ast, (node, { replaceWith }) => { + if (node.kind !== 'rule') return + if (!(node.selector[0] === '@' && node.selector.startsWith('@apply '))) return - // Wrap the whole AST in a root rule to make sure there is always a parent - // available for `@apply` at-rules. In some cases, the incoming `ast` just - // contains `@apply` at-rules which means that there is no proper parent to - // rely on. - let root = rule('&', ast) + let candidates = node.selector + .slice(7 /* Ignore `@apply ` when parsing the selector */) + .trim() + .split(/\s+/g) - // Track all nodes containing `@apply` - let parents = new Set() + // 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, { + onInvalidCandidate: (candidate) => { + throw new Error(`Cannot apply unknown utility class: ${candidate}`) + }, + }).astNodes - // Track all the dependencies of an `AstNode` - let dependencies = new DefaultMap>(() => new Set()) - - // Track all `@utility` definitions by its root (name) - let definitions = new DefaultMap(() => new Set()) - - // Collect all new `@utility` definitions and all `@apply` rules first - walk([root], (node, ctx) => { - if (node.kind !== 'at-rule') return - - // Do not allow `@apply` rules inside `@keyframes` rules. - if (node.name === '@keyframes') { - walk(node.nodes, (child) => { - if (child.kind === 'at-rule' && child.name === '@apply') { - throw new Error(`You cannot use \`@apply\` inside \`@keyframes\`.`) - } - }) - return WalkAction.Skip - } - - // `@utility` defines a utility, which is important information in order to - // do a correct topological sort later on. - if (node.name === '@utility') { - let name = node.params.replace(/-\*$/, '') - definitions.get(name).add(node) - - // In case `@apply` rules are used inside `@utility` rules. - walk(node.nodes, (child) => { - if (child.kind !== 'at-rule' || child.name !== '@apply') return - - parents.add(node) - - for (let dependency of resolveApplyDependencies(child, designSystem)) { - dependencies.get(node).add(dependency) - } - }) - return - } - - // Any other `@apply` node. - 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 (ctx.parent === null) return - - features |= Features.AtApply - - parents.add(ctx.parent) - - for (let dependency of resolveApplyDependencies(node, designSystem)) { - // 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) + // 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 + // don't want the wrapping selector. + let newNodes: AstNode[] = [] + for (let candidateNode of candidateAst) { + if (candidateNode.kind === 'rule' && candidateNode.selector[0] !== '@') { + for (let child of candidateNode.nodes) { + newNodes.push(child) + } + } else { + newNodes.push(candidateNode) } } + + // Verify that we don't have any circular dependencies by verifying that + // the current node does not appear in the new nodes. + walk(newNodes, (child) => { + if (child !== node) return + + // At this point we already know that we have a circular dependency. + // + // Figure out which candidate caused the circular dependency. This will + // help to create a useful error message for the end user. + for (let candidate of candidates) { + let selector = `.${escape(candidate)}` + + for (let rule of candidateAst) { + if (rule.kind !== 'rule') continue + if (rule.selector !== selector) continue + + walk(rule.nodes, (child) => { + if (child !== node) return + + throw new Error( + `You cannot \`@apply\` the \`${candidate}\` utility here because it creates a circular dependency.`, + ) + }) + } + } + }) + + replaceWith(newNodes) } }) - - // Topological sort before substituting `@apply` - let seen = new Set() - let sorted: AstNode[] = [] - let wip = new Set() - - function visit(node: AstNode, path: AstNode[] = []) { - if (seen.has(node)) { - return - } - - // Circular dependency detected - if (wip.has(node)) { - // Next node in the path is the one that caused the circular dependency - let next = path[(path.indexOf(node) + 1) % path.length] - - if ( - node.kind === 'at-rule' && - node.name === '@utility' && - next.kind === 'at-rule' && - next.name === '@utility' - ) { - walk(node.nodes, (child) => { - if (child.kind !== 'at-rule' || child.name !== '@apply') return - - let candidates = child.params.split(/\s+/g) - for (let candidate of candidates) { - for (let candidateAstNode of designSystem.parseCandidate(candidate)) { - switch (candidateAstNode.kind) { - case 'arbitrary': - break - - case 'static': - case 'functional': - if (next.params.replace(/-\*$/, '') === candidateAstNode.root) { - throw new Error( - `You cannot \`@apply\` the \`${candidate}\` utility here because it creates a circular dependency.`, - ) - } - break - - default: - candidateAstNode satisfies never - } - } - } - }) - } - - // Generic fallback error in case we cannot properly detect the origin of - // the circular dependency. - throw new Error( - `Circular dependency detected:\n\n${toCss([node])}\nRelies on:\n\n${toCss([next])}`, - ) - } - - wip.add(node) - - for (let dependencyId of dependencies.get(node)) { - for (let dependency of definitions.get(dependencyId)) { - path.push(node) - visit(dependency, path) - path.pop() - } - } - - seen.add(node) - wip.delete(node) - - sorted.push(node) - } - - for (let node of parents) { - visit(node) - } - - // Substitute the `@apply` at-rules in order. Note that the list is going to - // be flattened so we do not have to recursively walk over child rules - for (let parent of sorted) { - if (!('nodes' in parent)) continue - - walk(parent.nodes, (child) => { - if (child.kind !== 'at-rule' || child.name !== '@apply') return - - 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 candidates = Object.keys(candidateOffsets) - let compiled = compileCandidates(candidates, designSystem, { - respectImportant: false, - onInvalidCandidate: (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}\``) - }, - }) - - 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 - // don't want the wrapping selector. - let newNodes: AstNode[] = [] - for (let candidateNode of candidateAst) { - if (candidateNode.kind === 'rule') { - for (let child of candidateNode.nodes) { - newNodes.push(child) - } - } else { - newNodes.push(candidateNode) - } - } - - return WalkAction.Replace(newNodes) - } - }) - } - - return features -} - -function* resolveApplyDependencies( - node: Extract, - designSystem: DesignSystem, -) { - for (let candidate of node.params.split(/\s+/g)) { - for (let node of designSystem.parseCandidate(candidate)) { - switch (node.kind) { - case 'arbitrary': - // Doesn't matter, because there is no lookup needed - break - - case 'static': - case 'functional': - // Lookup by "root" - yield node.root - break - - default: - node satisfies never - } - } - } } diff --git a/packages/tailwindcss/src/ast.bench.ts b/packages/tailwindcss/src/ast.bench.ts deleted file mode 100644 index 034faee27..000000000 --- a/packages/tailwindcss/src/ast.bench.ts +++ /dev/null @@ -1,64 +0,0 @@ -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 14092c9f5..21915cba4 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -1,28 +1,10 @@ -import { describe, expect, it, test } from 'vitest' -import { - context, - cssContext, - decl, - handleNesting, - optimizeAst, - rule, - styleRule, - toCss, - type AstNode, -} from './ast' +import { expect, it } from 'vitest' +import { context, decl, rule, toCss, walk } 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(pretty(toCss(CSS.parse('.foo{color:red;&:hover{color:blue;}}')))).toMatchInlineSnapshot(` - " - .foo { + expect(toCss(CSS.parse('.foo{color:red;&:hover{color:blue;}}'))).toMatchInlineSnapshot(` + ".foo { color: red; &:hover { color: blue; @@ -33,14 +15,14 @@ it('should pretty print an AST', () => { }) it('allows the placement of context nodes', () => { - let ast: AstNode[] = [ - styleRule('.foo', [decl('color', 'red')]), + const ast = [ + rule('.foo', [decl('color', 'red')]), context({ context: 'a' }, [ - styleRule('.bar', [ + rule('.bar', [ decl('color', 'blue'), context({ context: 'b' }, [ // - styleRule('.baz', [decl('color', 'green')]), + rule('.baz', [decl('color', 'green')]), ]), ]), ]), @@ -50,18 +32,17 @@ it('allows the placement of context nodes', () => { let blueContext let greenContext - walk(ast, (node, _ctx) => { + walk(ast, (node, { context }) => { if (node.kind !== 'declaration') return - let ctx = cssContext(_ctx) switch (node.value) { case 'red': - redContext = ctx.context + redContext = context break case 'blue': - blueContext = ctx.context + blueContext = context break case 'green': - greenContext = ctx.context + greenContext = context break } }) @@ -70,9 +51,8 @@ it('allows the placement of context nodes', () => { expect(blueContext).toEqual({ context: 'a' }) expect(greenContext).toEqual({ context: 'b' }) - expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` - " - .foo { + expect(toCss(ast)).toMatchInlineSnapshot(` + ".foo { color: red; } .bar { @@ -84,1581 +64,3 @@ it('allows the placement of context nodes', () => { " `) }) - -it('should not emit empty rules once optimized', () => { - let ast = CSS.parse(css` - /* Empty rule */ - .foo { - } - - /* Empty rule, with nesting */ - .foo { - .bar { - } - .baz { - } - } - - /* Empty rule, with special case '&' rules */ - .foo { - & { - &:hover { - } - &:focus { - } - } - } - - /* Empty at-rule */ - @media (min-width: 768px) { - } - - /* Empty at-rule with nesting*/ - @media (min-width: 768px) { - .foo { - } - - @media (min-width: 1024px) { - .bar { - } - } - } - - /* Exceptions: */ - @charset "UTF-8"; - @layer foo, bar, baz; - @layer foo, bar, baz; /* Will be deduped */ - @custom-media --modern (color), (hover); - @namespace 'http://www.w3.org/1999/xhtml'; - @import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap'); - `) - - expect(pretty(toCss(ast))).toMatchInlineSnapshot(` - " - .foo { - } - .foo { - .bar { - } - .baz { - } - } - .foo { - & { - &:hover { - } - &:focus { - } - } - } - @media (min-width: 768px); - @media (min-width: 768px) { - .foo { - } - @media (min-width: 1024px) { - .bar { - } - } - } - @charset "UTF-8"; - @layer foo, bar, baz; - @layer foo, bar, baz; - @custom-media --modern (color), (hover); - @namespace 'http://www.w3.org/1999/xhtml'; - @import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap'); - " - `) - - expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` - " - @charset "UTF-8"; - @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'); - " - `) -}) - -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; - } - .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(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 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); - } - } - `) - - let theme = new Theme() - theme.add('--color-emerald-600', 'oklch(59.6% 0.145 163.225)') - - let design = buildDesignSystem(theme) - - expect(pretty(toCss(optimizeAst(ast, design)))).toMatchInlineSnapshot(` - " - @keyframes my-animation { - 0% { - color: color-mix(in oklab, var(--color-emerald-600) 0%, transparent); - } - 100% { - color: color-mix(in oklab, var(--color-emerald-600) 0%, transparent); - } - } - " - `) -}) - -describe('optimization', () => { - function optimize(input: string) { - let ast = CSS.parse(input) - walk(ast, { - exit(node) { - // Depending on where we use `@scope`, it's handled slightly differently: - // - // 1. In user authored CSS, we perform the normal CSS nesting and - // hoisting of `@scope` rules - // 2. In a `@custom-variant` definition, we need to swap the `@scope` - // with the parent definition. Otherwise you'll never get the - // expected "sandwich" effect. - // - // This little `@in-variant` hack is just a different way of - // differentiating between both cases for these tests. - if (node.kind === 'at-rule' && node.name === '@in-variant') { - return WalkAction.Replace(context({ source: 'variant' }, node.nodes)) - } - }, - }) - return pretty(toCss(handleNesting(ast))) - } - - // See: https://drafts.csswg.org/css-nesting-1/ - describe('CSS Nesting Module Level 1', () => { - it('uses the descendant combinator by default', async () => { - expect( - optimize(css` - .a { - element { - --x: 1; - } - .class { - --x: 2; - } - #id { - --x: 3; - } - :pseudo-class { - --x: 4; - } - ::pseudo-element { - --x: 5; - } - [attribute] { - --x: 6; - } - * { - --x: 7; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a element { - --x: 1; - } - .a .class { - --x: 2; - } - .a #id { - --x: 3; - } - .a :pseudo-class { - --x: 4; - } - .a ::pseudo-element { - --x: 5; - } - .a [attribute] { - --x: 6; - } - .a * { - --x: 7; - } - " - `) - }) - - it('should be possible to change the combinator', async () => { - expect( - optimize(css` - .a { - + .b { - --x: 1; - } - > .c { - --x: 2; - } - ~ .d { - --x: 3; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a + .b { - --x: 1; - } - .a > .c { - --x: 2; - } - .a ~ .d { - --x: 3; - } - " - `) - }) - - it('should replace the first rule, that contains `&` with `:scope`', async () => { - expect( - optimize(css` - /* Standalone */ - & { - --x: 1; - } - - /* In an at-rule */ - @supports (--y: 1) { - & { - --x: 2; - } - } - - /* With :is(…) */ - :is(&) { - --x: 3; - } - - /* In an at-rule, with :is(…) */ - @supports (--y: 2) { - :is(&) { - --x: 4; - } - } - - /* With multiple selectors */ - &, - .a { - --x: 5; - } - - /* With multiple selectors + :is(…) */ - :is(&), - .b { - --x: 6; - } - - /* With multiple selectors in an at-rule */ - @supports (--y: 3) { - &, - .c { - --x: 7; - } - } - - /* With multiple selectors in an at-rule + :is(…) */ - @supports (--y: 4) { - :is(&), - .d { - --x: 8; - } - } - `), - ).toMatchInlineSnapshot(` - " - :scope { - --x: 1; - } - @supports (--y: 1) { - :scope { - --x: 2; - } - } - :is(:scope) { - --x: 3; - } - @supports (--y: 2) { - :is(:scope) { - --x: 4; - } - } - :scope, .a { - --x: 5; - } - :is(:scope), .b { - --x: 6; - } - @supports (--y: 3) { - :scope, .c { - --x: 7; - } - } - @supports (--y: 4) { - :is(:scope), .d { - --x: 8; - } - } - " - `) - }) - - it('should be possible to use `&` to explicitly match the parent', async () => { - expect( - optimize(css` - .a { - & + .b { - --x: 1; - } - & > .c { - --x: 2; - } - & ~ .d { - --x: 3; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a + .b { - --x: 1; - } - .a > .c { - --x: 2; - } - .a ~ .d { - --x: 3; - } - " - `) - }) - - it('should be possible to use `&` in a different location', async () => { - expect( - optimize(css` - .a { - .b & { - --x: 1; - } - .c + & { - --x: 2; - } - .d > & { - --x: 3; - } - .e ~ & { - --x: 4; - } - } - `), - ).toMatchInlineSnapshot(` - " - .b .a { - --x: 1; - } - .c + .a { - --x: 2; - } - .d > .a { - --x: 3; - } - .e ~ .a { - --x: 4; - } - " - `) - }) - - it('should be possible to use `&` on its own', async () => { - expect( - optimize(css` - .a { - & { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a { - --x: 1; - } - " - `) - }) - - it('should be possible to use `&` nested in `:is(…)`', async () => { - expect( - optimize(css` - .a { - :is(&) { - --x: 1; - } - :is(:is(:is(&))) { - --x: 2; - } - } - `), - ).toMatchInlineSnapshot(` - " - :is(.a) { - --x: 1; - } - :is(:is(:is(.a))) { - --x: 2; - } - " - `) - }) - - it('should be possible to handle nesting with a parent selector list', async () => { - expect( - optimize(css` - .a, - .b { - .c, - .d & { - --x: 1; - &:hover { - --x: 2; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - :is(.a, .b) .c, .d :is(.a, .b) { - --x: 1; - &:hover { - --x: 2; - } - } - " - `) - }) - - it('should not replace `\&`', () => { - expect( - optimize(css` - .a { - .b-\& { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a .b-\\& { - --x: 1; - } - " - `) - }) - - it('should not replace `&` as part of a string', () => { - expect( - optimize(css` - .a { - [data-b='c&d'] { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a [data-b='c&d'] { - --x: 1; - } - " - `) - }) - - it.each([ - ['element', '&element', ':is(element)element'], // Invalid CSS - ['element', 'element&', 'element:is(element)'], - ['element', '&.class', 'element.class'], - ['element', '.class&', '.class:is(element)'], // Optimization: element.class - ['element', '&#id', 'element#id'], - ['element', '#id&', '#id:is(element)'], // Optimization: element#id - ['element', '&:hover', 'element:hover'], - ['element', ':hover&', ':hover:is(element)'], - ['element', '&::before', 'element::before'], - ['element', '::before&', '::before:is(element)'], // Invalid CSS - ['element', '&:not(.x)', 'element:not(.x)'], - ['element', ':not(.x)&', ':not(.x):is(element)'], // Optimization: element:not(.x) - ['element', '&[attribute]', 'element[attribute]'], - ['element', '[attribute]&', '[attribute]:is(element)'], // Optimization: element[attribute] - ['element', '&*', ':is(element)*'], // Invalid CSS - ['element', '*&', 'element'], - - ['.class', '&element', ':is(.class)element'], // Invalid CSS - ['.class', 'element&', 'element.class'], - ['.class', '&.class', '.class.class'], - ['.class', '.class&', '.class.class'], - ['.class', '&#id', '.class#id'], - ['.class', '#id&', '#id.class'], - ['.class', '&:hover', '.class:hover'], - ['.class', ':hover&', ':hover.class'], - ['.class', '&::before', '.class::before'], - ['.class', '::before&', '::before.class'], // Invalid CSS - ['.class', '&:not(.x)', '.class:not(.x)'], - ['.class', ':not(.x)&', ':not(.x).class'], - ['.class', '&[attribute]', '.class[attribute]'], - ['.class', '[attribute]&', '[attribute].class'], - ['.class', '&*', ':is(.class)*'], // Invalid CSS - ['.class', '*&', '.class'], - - ['#id', '&element', ':is(#id)element'], // Invalid CSS - ['#id', 'element&', 'element#id'], - ['#id', '&.class', '#id.class'], - ['#id', '.class&', '.class#id'], - ['#id', '&#id', '#id#id'], - ['#id', '#id&', '#id#id'], - ['#id', '&:hover', '#id:hover'], - ['#id', ':hover&', ':hover#id'], - ['#id', '&::before', '#id::before'], - ['#id', '::before&', '::before#id'], // Invalid CSS - ['#id', '&:not(.x)', '#id:not(.x)'], - ['#id', ':not(.x)&', ':not(.x)#id'], - ['#id', '&[attribute]', '#id[attribute]'], - ['#id', '[attribute]&', '[attribute]#id'], - ['#id', '&*', ':is(#id)*'], // Invalid CSS - ['#id', '*&', '#id'], - - [':hover', '&element', ':is(:hover)element'], // Invalid CSS - [':hover', 'element&', 'element:hover'], - [':hover', '&.class', ':hover.class'], - [':hover', '.class&', '.class:hover'], - [':hover', '&#id', ':hover#id'], - [':hover', '#id&', '#id:hover'], - [':hover', '&:hover', ':hover:hover'], - [':hover', ':hover&', ':hover:hover'], - [':hover', '&::before', ':hover::before'], - [':hover', '::before&', '::before:hover'], - [':hover', '&:not(.x)', ':hover:not(.x)'], - [':hover', ':not(.x)&', ':not(.x):hover'], - [':hover', '&[attribute]', ':hover[attribute]'], - [':hover', '[attribute]&', '[attribute]:hover'], - [':hover', '&*', ':is(:hover)*'], // Invalid CSS - [':hover', '*&', ':hover'], - - ['::before', '&element', ':is(::before)element'], // Invalid CSS - ['::before', 'element&', 'element::before'], - ['::before', '&.class', '::before.class'], - ['::before', '.class&', '.class::before'], - ['::before', '&#id', '::before#id'], - ['::before', '#id&', '#id::before'], - ['::before', '&:hover', '::before:hover'], - ['::before', ':hover&', ':hover::before'], - ['::before', '&::before', '::before::before'], - ['::before', '::before&', '::before::before'], - ['::before', '&:not(.x)', '::before:not(.x)'], - ['::before', ':not(.x)&', ':not(.x)::before'], - ['::before', '&[attribute]', '::before[attribute]'], - ['::before', '[attribute]&', '[attribute]::before'], - ['::before', '&*', ':is(::before)*'], // Invalid CSS - ['::before', '*&', '::before'], - - [':not(.x)', '&element', ':is(:not(.x))element'], // Invalid CSS - [':not(.x)', 'element&', 'element:not(.x)'], - [':not(.x)', '&.class', ':not(.x).class'], - [':not(.x)', '.class&', '.class:not(.x)'], - [':not(.x)', '&#id', ':not(.x)#id'], - [':not(.x)', '#id&', '#id:not(.x)'], - [':not(.x)', '&:hover', ':not(.x):hover'], - [':not(.x)', ':hover&', ':hover:not(.x)'], - [':not(.x)', '&::before', ':not(.x)::before'], - [':not(.x)', '::before&', '::before:not(.x)'], - [':not(.x)', '&:not(.x)', ':not(.x):not(.x)'], - [':not(.x)', ':not(.x)&', ':not(.x):not(.x)'], - [':not(.x)', '&[attribute]', ':not(.x)[attribute]'], - [':not(.x)', '[attribute]&', '[attribute]:not(.x)'], - [':not(.x)', '&*', ':is(:not(.x))*'], // Invalid CSS - [':not(.x)', '*&', ':not(.x)'], - - ['[attribute]', '&element', ':is([attribute])element'], // Invalid CSS - ['[attribute]', 'element&', 'element[attribute]'], - ['[attribute]', '&.class', '[attribute].class'], - ['[attribute]', '.class&', '.class[attribute]'], - ['[attribute]', '&#id', '[attribute]#id'], - ['[attribute]', '#id&', '#id[attribute]'], - ['[attribute]', '&:hover', '[attribute]:hover'], - ['[attribute]', ':hover&', ':hover[attribute]'], - ['[attribute]', '&::before', '[attribute]::before'], - ['[attribute]', '::before&', '::before[attribute]'], - ['[attribute]', '&:not(.x)', '[attribute]:not(.x)'], - ['[attribute]', ':not(.x)&', ':not(.x)[attribute]'], - ['[attribute]', '&[attribute]', '[attribute][attribute]'], - ['[attribute]', '[attribute]&', '[attribute][attribute]'], - ['[attribute]', '&*', ':is([attribute])*'], // Invalid CSS - ['[attribute]', '*&', '[attribute]'], - - ['*', '&element', ':is(*)element'], // Invalid CSS - ['*', 'element&', 'element:is(*)'], - ['*', '&.class', '.class'], - ['*', '.class&', '.class:is(*)'], // Optimization: *.class → .class - ['*', '&#id', '#id'], - ['*', '#id&', '#id:is(*)'], // Optimization: *#id → #id - ['*', '&:hover', ':hover'], - ['*', ':hover&', ':hover:is(*)'], - ['*', '&::before', '::before'], - ['*', '::before&', '::before:is(*)'], // Invalid CSS - ['*', '&:not(.x)', ':not(.x)'], - ['*', ':not(.x)&', ':not(.x):is(*)'], // Optimization: *:not(.x) → :not(.x) - ['*', '&[attribute]', '[attribute]'], - ['*', '[attribute]&', '[attribute]:is(*)'], // Optimization: *[attribute] → [attribute] - ['*', '&*', ':is(*)*'], // Invalid CSS - ['*', '*&', '*'], - - ['&', '&element', ':is(:scope)element'], // Invalid CSS - ['&', 'element&', 'element:scope'], - ['&', '&.class', ':scope.class'], - ['&', '.class&', '.class:scope'], - ['&', '&#id', ':scope#id'], - ['&', '#id&', '#id:scope'], - ['&', '&:hover', ':scope:hover'], - ['&', ':hover&', ':hover:scope'], - ['&', '&::before', ':scope::before'], - ['&', '::before&', '::before:scope'], - ['&', '&:not(.x)', ':scope:not(.x)'], - ['&', ':not(.x)&', ':not(.x):scope'], - ['&', '&[attribute]', ':scope[attribute]'], - ['&', '[attribute]&', '[attribute]:scope'], - ['&', '&*', ':is(:scope)*'], // Invalid CSS - ['&', '*&', ':scope'], - ])(`'%s { %s }' → '%s' (%#)`, async (root, nested, expected) => { - let optimized = optimize(toCss([rule(root, [rule(nested, [decl('--x', '0')])])])) - let ast = CSS.parse(optimized) - - let count = 0 - walk(ast, () => void count++) - - // 1 rule, 1 declaration - expect(count).toBe(2) - - if (ast[0].kind !== 'rule') throw new Error('expected a rule') - expect(ast[0].selector).toEqual(expected) - }) - - it('should not remove the `*` namespace from namespaced selectors', () => { - expect( - optimize(css` - .a { - & *|div { - color: red; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a *|div { - color: red; - } - " - `) - }) - - test('multiple selectors in the list are relative to the parent', async () => { - expect( - optimize(css` - .a, - .b { - --x: 1; - + .c { - --x: 2; - } - &.d { - --x: 3; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a, .b { - --x: 1; - + .c { - --x: 2; - } - &.d { - --x: 3; - } - } - " - `) - }) - - it('should be possible to use `&` multiple times', async () => { - expect( - optimize(css` - .a { - & .b & .c & .d { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a .b .a .c .a .d { - --x: 1; - } - " - `) - }) - - it('should be possible to use `&` multiple times in a row', async () => { - expect( - optimize(css` - .a { - &&& { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - .a.a.a { - --x: 1; - } - " - `) - }) - - it('should be possible to use `&` inside a selector', async () => { - expect( - optimize(css` - .a { - :not(&) { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - :not(.a) { - --x: 1; - } - " - `) - }) - - it('should be possible to use deeply nested CSS', async () => { - expect( - optimize(css` - .a, - .b { - --x: 1; - - .c & { - --x: 2; - - &:hover, - &:focus { - --x: 3; - .d { - --x: 4; - } - } - } - } - `), - ).toMatchInlineSnapshot(` - " - .a, .b { - --x: 1; - .c & { - --x: 2; - &:hover, &:focus { - --x: 3; - .d { - --x: 4; - } - } - } - } - " - `) - }) - - it('should properly split rules to guarantee specificity', async () => { - expect( - optimize(css` - .a { - --before: 1; - &:hover { - --inside: 1; - } - --after: 1; - } - `), - ).toMatchInlineSnapshot(` - " - .a { - --before: 1; - &:hover { - --inside: 1; - } - --after: 1; - } - " - `) - }) - - it.each([ - ['div', '&', 'div'], - ['div', '[before]&', '[before]:is(div)'], - ['div', '&[after]', 'div[after]'], - ['div', '[before]&[after]', '[before]:is(div)[after]'], - ['div', '[before] &', '[before] div'], - ['div', '& [after]', 'div [after]'], - ['div', '[before] & [after]', '[before] div [after]'], - - ['.parent', '&', '.parent'], - ['.parent', '[before]&', '[before].parent'], - ['.parent', '&[after]', '.parent[after]'], - ['.parent', '[before]&[after]', '[before].parent[after]'], - ['.parent', '[before] &', '[before] .parent'], - ['.parent', '& [after]', '.parent [after]'], - ['.parent', '[before] & [after]', '[before] .parent [after]'], - - ['.a > .b', '&', '.a > .b'], - ['.a > .b', '[before]&', '[before]:is(.a > .b)'], - ['.a > .b', '&[after]', '.a > .b[after]'], - ['.a > .b', '[before]&[after]', '[before]:is(.a > .b)[after]'], - ['.a > .b', '[before] &', '[before] :is(.a > .b)'], - ['.a > .b', '& [after]', '.a > .b [after]'], - ['.a > .b', '[before] & [after]', '[before] :is(.a > .b) [after]'], - ])(`should optimize '%s { %s }' → '%s' (%#)`, async (root, nested, expected) => { - let optimized = optimize(toCss([rule(root, [rule(nested, [decl('--x', '0')])])])) - let ast = CSS.parse(optimized) - - let count = 0 - walk(ast, () => void count++) - - // 1 rule, 1 declaration - expect(count).toBe(2) - - if (ast[0].kind !== 'rule') throw new Error('expected a rule') - expect(ast[0].selector).toEqual(expected) - }) - - it('should hoist at-rules', async () => { - expect( - optimize(css` - @layer utilities { - .a, - .b { - @media (print) { - --x: 1; - .c { - @media (min-width: 123px) { - --x: 2; - } - } - } - } - .d { - @media (print) { - @media (min-width: 123px) { - --x: 3; - } - } - } - } - @property --foo { - syntax: '*'; - } - @layer utilities { - .e { - @media (print) { - --x: 4; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (print) { - .a, .b { - --x: 1; - } - @media (min-width: 123px) { - :is(.a, .b) .c { - --x: 2; - } - .d { - --x: 3; - } - } - } - } - @property --foo { - syntax: '*'; - } - @layer utilities { - @media (print) { - .e { - --x: 4; - } - } - } - " - `) - }) - - it('should hoist `@scope` at-rules', async () => { - expect( - optimize(css` - @layer utilities { - .parent { - @in-variant { - @scope (.from) to (.to) { - --x: 1; - } - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope (.from) to (.to) { - .parent { - --x: 1; - } - } - } - " - `) - }) - - // We have an optimization where the moment we see a declaration that we - // stop handling the nesting and emit everything as-is. This won't work when - // we're dealing with `@scope` and we still want to hoist that out. - it('should hoist `@scope` at-rules, even when sibling declarations are present', async () => { - expect( - optimize(css` - @layer utilities { - .parent { - --x: 1; - @in-variant { - @scope (.from) { - .inside { - --y: 2; - } - } - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @layer utilities { - .parent { - --x: 1; - } - @scope (.from) { - .parent .inside { - --y: 2; - } - } - } - " - `) - }) - - it('should resolve `&` in the `@scope` prelude when hoisting', async () => { - // `&` in the `` selector refers to the elements matched by - // the nearest ancestor style rule, while `&` in the `` - // selector refers to the scoping root. - // - // See: https://drafts.csswg.org/css-nesting-1/#nesting-at-scope - expect( - optimize(css` - .parent { - @scope (& > .scope) to (& .limit) { - .content { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.parent > .scope) to (:where(:scope) .limit) { - .content { - --x: 1; - } - } - " - `) - - // Selector lists need `:is(…)` to keep their meaning - expect( - optimize(css` - .parent-a, - .parent-b { - @scope (& > .from) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (:is(.parent-a, .parent-b) > .from) { - .inside { - --x: 1; - } - } - " - `) - - // The `` selector is optional, so the first group in the - // prelude is not necessarily the `` selector. - expect( - optimize(css` - .parent { - @scope to (& .to) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope to (:where(:scope) .to) { - .parent .inside { - --x: 1; - } - } - " - `) - - // The parent selector is substituted with the same logic as the nesting - // selector in regular style rules: `:is(…)` is used when substituting the - // parent as-is would change the meaning (e.g. a type selector cannot - // appear after a class selector in a compound selector), … - expect( - optimize(css` - main { - @scope (.card&) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.card:is(main)) { - .inside { - --x: 1; - } - } - " - `) - - // … and dropped when the substitution is provably equivalent, even for - // complex parent selectors - expect( - optimize(css` - .a .b { - @scope (& > .from) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.a .b > .from) { - .inside { - --x: 1; - } - } - " - `) - }) - - it('should wrap bare declarations inside `@scope` in a `&` rule', async () => { - // A bug in Lightning CSS causes an error when there are declarations - // directly in the `@scope`. To fix this we will ensure a rule with - // `:where(:scope)` is available. - expect( - optimize(css` - @scope (.from) to (.to) { - --x: 1; - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) to (.to) { - :where(:scope) { - --x: 1; - } - } - " - `) - - // When generated by a variant, the declarations resolve against the - // parent rule: the `@scope` rule scopes _where the utility applies_ - // (like nesting `@media` does, and like variants do). - // - // Note: this deviates from native CSS nesting, where the `` - // selector would be relative to `.parent` and the declarations would - // apply to the scoping root. User-authored `@scope` rules (without the - // `@in-variant` marker) keep those native semantics. - expect( - optimize(css` - .parent { - @in-variant { - @scope (.from) { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) { - .parent { - --x: 1; - } - } - " - `) - }) - - it('should replace `&` with `:where(:scope)` for rules directly inside `@scope`', async () => { - // Inside an `@scope` rule, the nesting selector behaves like - // `:where(:scope)` and matches the scoping root with zero specificity. - expect( - optimize(css` - @scope (.from) { - & > p { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) { - :where(:scope) > p { - --x: 1; - } - } - " - `) - }) - - it('should handle `&` deeply nested in `@scope` as normal references to a parent', async () => { - expect( - optimize(css` - @scope (.from) { - &, - .inside { - --x: 1; - - &:hover { - --x: 2; - } - - &:focus { - --x: 3; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) { - :where(:scope), .inside { - --x: 1; - &:hover { - --x: 2; - } - &:focus { - --x: 3; - } - } - } - " - `) - }) - - it('should compile user-authored `@scope` rules with their native CSS nesting semantics', async () => { - // The `` selector is relative to the parent rule (like - // nested style rule selectors are), the rules inside are relative to - // the scoping root, and `&` refers to the scoping root - expect( - optimize(css` - .parent { - @scope (.from) to (.to) { - .inside { - --x: 1; - } - - & > .child { - --x: 2; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.parent .from) to (.to) { - .inside { - --x: 1; - } - :where(:scope) > .child { - --x: 2; - } - } - " - `) - - // A trailing `&` makes the parent rule the scoping root: the scoping - // roots are `.parent` elements inside `.from` elements - expect( - optimize(css` - .parent { - @scope (.from &) to (.to) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from .parent) to (.to) { - .inside { - --x: 1; - } - } - " - `) - - // Bare declarations apply to the scoping root. They are wrapped in a - // `:where(:scope)` rule, which is the spec's exact equivalent, because - // Lightning CSS drops bare declarations inside `@scope` entirely. - expect( - optimize(css` - .parent { - @scope (.from) { - --x: 1; - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.parent .from) { - :where(:scope) { - --x: 1; - } - } - " - `) - - // Also at the top level - expect( - optimize(css` - @scope (.from) to (.to) { - --x: 1; - - .inside { - --x: 2; - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.from) to (.to) { - :where(:scope) { - --x: 1; - } - .inside { - --x: 2; - } - } - " - `) - - // Every selector in a `` selector list is relative to the - // parent rule, whether it uses `&` or not - expect( - optimize(css` - .parent { - @scope (.a, & > .b) { - .inside { - --x: 1; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @scope (.parent .a, .parent > .b) { - .inside { - --x: 1; - } - } - " - `) - }) - - it('should leave `@property` and `@apply` alone', async () => { - expect( - optimize(css` - .foo { - .bar { - @apply text-red-500 hover:text-red-600; - } - } - - .baz { - @property --tw-content { - syntax: '*'; - initial-value: ''; - inherits: false; - } - - @property --tw-border-spacing-x { - syntax: ''; - inherits: false; - initial-value: 0; - } - } - `), - ).toMatchInlineSnapshot(` - " - .foo .bar { - @apply text-red-500 hover:text-red-600; - } - .baz { - @property --tw-content { - syntax: '*'; - initial-value: ''; - inherits: false; - } - @property --tw-border-spacing-x { - syntax: ''; - inherits: false; - initial-value: 0; - } - } - " - `) - }) - - it('should merge a body-less @layer with an @layer with the same name that has a body', async () => { - expect( - optimize(css` - @layer a; - - @layer a { - @layer b { - .x { - color: red; - } - } - } - `), - ).toMatchInlineSnapshot(` - " - @layer a { - @layer b { - .x { - color: red; - } - } - } - " - `) - }) - - it('should not get rid of `:is(…)` when the compound selector is part of a complex selector', async () => { - expect( - optimize(css` - .foo .bar { - .system &:focus { - --x: 1; - } - } - .foo:hover { - .system &:focus { - --x: 2; - } - } - `), - ).toMatchInlineSnapshot(` - " - .system :is(.foo .bar):focus { - --x: 1; - } - .system .foo:hover:focus { - --x: 2; - } - " - `) - }) - - it('should not dedupe adjacent at-rules with the same prelude but different bodies', async () => { - expect( - optimize(css` - @font-face { - font-family: 'A'; - src: url('/fonts/a.woff2'); - } - - @font-face { - font-family: 'B'; - src: url('/fonts/b.woff2'); - } - - @keyframes spin { - to { - transform: rotate(360deg); - } - } - - @keyframes spin { - to { - transform: rotate(-360deg); - } - } - `), - ).toMatchInlineSnapshot(` - " - @font-face { - font-family: 'A'; - src: url('/fonts/a.woff2'); - } - @font-face { - font-family: 'B'; - src: url('/fonts/b.woff2'); - } - @keyframes spin { - to { - transform: rotate(360deg); - } - } - @keyframes spin { - to { - transform: rotate(-360deg); - } - } - " - `) - }) - }) -}) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 09c3ea051..e9d1ce48c 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -1,35 +1,7 @@ -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 = { +export type Rule = { kind: 'rule' selector: string nodes: AstNode[] - - src?: SourceLocation - dst?: SourceLocation -} - -export type AtRule = { - kind: 'at-rule' - name: string - params: string - nodes: AstNode[] - - src?: SourceLocation - dst?: SourceLocation } export type Declaration = { @@ -37,40 +9,27 @@ 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 + 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 -export type AstNode = StyleRule | AtRule | Declaration | Comment | Context | AtRoot +export type AstNode = Rule | Declaration | Comment | Context | AtRoot -export function styleRule(selector: string, nodes: AstNode[] = []): StyleRule { +export function rule(selector: string, nodes: AstNode[]): Rule { return { kind: 'rule', selector, @@ -78,29 +37,12 @@ export function styleRule(selector: string, nodes: AstNode[] = []): StyleRule { } } -export function atRule(name: string, params: string = '', nodes: AstNode[] = []): AtRule { - return { - kind: 'at-rule', - name, - params, - nodes, - } -} - -export function rule(selector: string, nodes: AstNode[] = []): StyleRule | AtRule { - if (selector.charCodeAt(0) === AT_SIGN) { - return parseAtRule(selector, nodes) - } - - return styleRule(selector, nodes) -} - -export function decl(property: string, value: string | undefined, important = false): Declaration { +export function decl(property: string, value: string | undefined): Declaration { return { kind: 'declaration', property, value, - important, + important: false, } } @@ -111,7 +53,7 @@ export function comment(value: string): Comment { } } -export function context(context: Record, nodes: AstNode[]): Context { +export function context(context: Record, nodes: AstNode[]): Context { return { kind: 'context', context, @@ -126,203 +68,106 @@ export function atRoot(nodes: AstNode[]): AtRoot { } } -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 +export enum WalkAction { + /** Continue walking, which is the default */ + Continue, - 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 + /** Skip visiting the children of this node */ + Skip, - case 'at-root': - return { - kind: node.kind, - nodes: node.nodes.map(cloneAstNode), - src: node.src, - dst: node.dst, - } satisfies AtRoot as T - - case 'context': - return { - kind: node.kind, - context: { ...node.context }, - nodes: node.nodes.map(cloneAstNode), - src: node.src, - dst: node.dst, - } satisfies Context as T - - 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 - - case 'comment': - return { - kind: node.kind, - value: node.value, - src: node.src, - dst: node.dst, - } satisfies Comment as T - - default: - node satisfies never - throw new Error(`Unknown node kind: ${(node as any).kind}`) - } + /** Stop the walk entirely */ + Stop, } -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) - } - } - - // 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') - }, - } -} - -// Optimize the AST for printing where all the special nodes that require custom -// handling are handled such that the printing is a 1-to-1 transformation. -export function optimizeAst( +export function walk( ast: AstNode[], - designSystem: DesignSystem, - polyfills: Polyfills = Polyfills.All, + visit: ( + node: AstNode, + utils: { + parent: AstNode | null + replaceWith(newNode: AstNode | AstNode[]): void + context: Record + }, + ) => void | WalkAction, + parent: AstNode | null = null, + context: Record = {}, ) { - let atRoots: AstNode[] = [] - let seenAtProperties = new Set() - let cssThemeVariables = new DefaultMap>(() => new Set()) - let colorMixDeclarations = new DefaultMap>(() => new Set()) - let keyframes = new Set() - let usedKeyframeNames = new Set() + for (let i = 0; i < ast.length; i++) { + let node = ast[i] + // 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') { + walk(node.nodes, visit, parent, { ...context, ...node.context }) + continue + } + + let status = + visit(node, { + parent, + replaceWith(newNode) { + ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode])) + // We want to visit 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. + i-- + }, + context, + }) ?? WalkAction.Continue + + // Stop the walk entirely + if (status === WalkAction.Stop) return + + // Skip visiting the children of this node + if (status === WalkAction.Skip) continue + + if (node.kind === 'rule') { + walk(node.nodes, visit, node, context) + } + } +} + +export function toCss(ast: AstNode[]) { + let atRoots: string = '' + let seenAtProperties = new Set() let propertyFallbacksRoot: Declaration[] = [] let propertyFallbacksUniversal: Declaration[] = [] - let variableDependencies = new DefaultMap>(() => new Set()) - - function transform( - node: AstNode, - parent: AstNode[], - context: Record = {}, - depth = 0, - ) { - // Declaration - if (node.kind === 'declaration') { - if (node.property === '--tw-sort' || node.value === undefined || node.value === null) { - return - } - - // Track variables defined in `@theme` - if (context.theme && node.property[0] === '-' && node.property[1] === '-') { - // Variables that resolve to `initial` should never be emitted. This can happen because of - // the `--theme(…)` being used and evaluated lazily - if (node.value === 'initial') { - node.value = undefined - return - } - - if (!context.keyframes) { - cssThemeVariables.get(parent).add(node) - } - } - - // Track used CSS variables - if (node.value.includes('var(')) { - // Declaring another variable does not count as usage. Instead, we mark - // the relationship - if (context.theme && node.property[0] === '-' && node.property[1] === '-') { - for (let variable of extractUsedVariables(node.value)) { - variableDependencies.get(variable).add(node.property) - } - } else { - designSystem.trackUsedVariables(node.value) - } - } - - // Track used animation names - if (node.property === 'animation') { - for (let keyframeName of extractKeyframeNames(node.value)) { - usedKeyframeNames.add(keyframeName) - } - } - - // Create fallback values for usages of the `color-mix(…)` function that reference variables - // found in the theme config. - if ( - polyfills & Polyfills.ColorMix && - !context.supportsColorMix && - !context.keyframes && - node.value.includes('color-mix(') - ) { - colorMixDeclarations.get(parent).add(node) - } - - parent.push(node) - } + function stringify(node: AstNode, depth = 0): string { + let css = '' + let indent = ' '.repeat(depth) // Rule - else if (node.kind === 'rule') { - let nodes: AstNode[] = [] - - for (let child of node.nodes) { - transform(child, nodes, context, depth + 1) + if (node.kind === 'rule') { + if (node.selector === '@tailwind utilities') { + for (let child of node.nodes) { + css += stringify(child, depth) + } + return css } - if (nodes.length > 0) { - parent.push({ ...node, nodes }) - } - } - - // AtRule `@property` - else if (node.kind === 'at-rule' && node.name === '@property' && depth === 0) { - // Don't output duplicate `@property` rules - if (seenAtProperties.has(node.params)) { - return + // Print at-rules without nodes with a `;` instead of an empty block. + // + // E.g.: + // + // ```css + // @layer base, components, utilities; + // ``` + if (node.selector[0] === '@' && node.nodes.length === 0) { + return `${indent}${node.selector};\n` } - // Collect fallbacks for `@property` rules for Firefox support We turn these into rules on - // `:root` or `*` and some pseudo-elements based on the value of `inherits` - if (polyfills & Polyfills.AtProperty) { - let property = node.params + if (node.selector[0] === '@' && node.selector.startsWith('@property ') && depth === 0) { + // Don't output duplicate `@property` rules + if (seenAtProperties.has(node.selector)) { + return '' + } + + // Collect fallbacks for `@property` rules for Firefox support + // We turn these into rules on `:root` or `*` and some pseudo-elements + // based on the value of `inherits`` + let property = node.selector.replace(/@property\s*/g, '') let initialValue = null let inherits = false @@ -335,1498 +180,45 @@ export function optimizeAst( } } - let fallback = decl(property, initialValue ?? 'initial') - fallback.src = node.src - if (inherits) { - propertyFallbacksRoot.push(fallback) + propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial')) } else { - propertyFallbacksUniversal.push(fallback) - } - } - - seenAtProperties.add(node.params) - - let copy = { ...node, nodes: [] } - for (let child of node.nodes) { - transform(child, copy.nodes, context, depth + 1) - } - parent.push(copy) - } - - // AtRule - 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: [] } - for (let child of node.nodes) { - transform(child, copy.nodes, context, depth + 1) - } - - // Only track `@keyframes` that could be removed, when they were defined inside of a `@theme`. - if (node.name === '@keyframes' && context.theme) { - keyframes.add(copy) - } - - if ( - copy.nodes.length > 0 || - copy.name === '@layer' || - copy.name === '@charset' || - copy.name === '@custom-media' || - copy.name === '@namespace' || - copy.name === '@import' || - copy.name === '@apply' - ) { - parent.push(copy) - } - } - - // AtRoot - else if (node.kind === 'at-root') { - for (let child of node.nodes) { - let newParent: AstNode[] = [] - transform(child, newParent, context, 0) - for (let child of newParent) { - atRoots.push(child) - } - } - } - - // Context - else if (node.kind === 'context') { - // 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) + propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial')) } - if (copy.nodes.length > 0) { - parent.push(copy) - } - } else { - for (let child of node.nodes) { - transform(child, parent, { ...context, ...node.context }, depth) - } - } - } - - // Comment - else if (node.kind === 'comment') { - parent.push(node) - } - - // Unknown - else { - node satisfies never - } - } - - let newAst: AstNode[] = [] - for (let node of ast) { - transform(node, newAst, {}, 0) - } - - // Remove unused theme variables - next: for (let [parent, declarations] of cssThemeVariables) { - for (let declaration of declarations) { - // Find out if a variable is either used directly or if any of the - // variables referencing it is used. - let variableUsed = isVariableUsed( - declaration.property, - designSystem.theme, - variableDependencies, - ) - if (variableUsed) { - if (declaration.property.startsWith(designSystem.theme.prefixKey('--animate-'))) { - for (let keyframeName of extractKeyframeNames(declaration.value!)) - usedKeyframeNames.add(keyframeName) - } - - continue + seenAtProperties.add(node.selector) } - // Remove the declaration (from its parent) - let idx = parent.indexOf(declaration) - parent.splice(idx, 1) - - // If the parent is now empty, remove it and any `@layer` rules above it - // from the AST - if (parent.length === 0) { - let path = findNode(newAst, (node) => node.kind === 'rule' && node.nodes === parent) - - if (!path || path.length === 0) continue next - - // Add the root of the AST so we can delete from the top-level - path.unshift({ - kind: 'at-root', - nodes: newAst, - }) - - // Remove nodes from the parent as long as the parent is empty - // otherwise and consist of only @layer rules - do { - let nodeToRemove = path.pop() - if (!nodeToRemove) break - - let removeFrom = path[path.length - 1] - if (!removeFrom) break - if (removeFrom.kind !== 'at-root' && removeFrom.kind !== 'at-rule') break - - let idx = removeFrom.nodes.indexOf(nodeToRemove) - if (idx === -1) break - - removeFrom.nodes.splice(idx, 1) - } while (true) - - continue next - } - } - } - - // Remove unused keyframes - for (let keyframe of keyframes) { - if (!usedKeyframeNames.has(keyframe.params)) { - let idx = atRoots.indexOf(keyframe) - atRoots.splice(idx, 1) - } - } - - 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) { - let wrapper = rule(':root, :host', propertyFallbacksRoot) - wrapper.src = propertyFallbacksRoot[0].src - fallbackAst.push(wrapper) - } - - if (propertyFallbacksUniversal.length > 0) { - let wrapper = rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal) - wrapper.src = propertyFallbacksUniversal[0].src - fallbackAst.push(wrapper) - } - - if (fallbackAst.length > 0) { - // Insert an empty `@layer properties;` at the beginning of the document. We need to place it - // after eventual external imports as `lightningcss` would otherwise move the content into the - // same place. - let firstValidNodeIndex = newAst.findIndex((node) => { - // License comments - if (node.kind === 'comment') return false - - if (node.kind === 'at-rule') { - // Charset - if (node.name === '@charset') return false - - // External imports - if (node.name === '@import') return false - } - - return true - }) - - let layerPropertiesStatement = atRule('@layer', 'properties', []) - layerPropertiesStatement.src = fallbackAst[0].src - - newAst.splice( - firstValidNodeIndex < 0 ? newAst.length : firstValidNodeIndex, - 0, - layerPropertiesStatement, - ) - - 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 handleNesting(newAst) -} - -export function handleNesting(ast: AstNode[]): AstNode[] { - let parseSelectorCache = new DefaultMap(SelectorParser.parse) - - // Nodes with an `@scope` rule somewhere in their subtree. These cannot use - // the "emit as-is" fast path for rules containing declarations, because - // `@scope` rules change meaning when they stay nested inside a style rule and - // must go through the hoisting machinery instead. - let containsAtScope = new Set() - { - function scan(nodes: AstNode[]): boolean { - let found = false - for (let node of nodes) { - if (node.kind === 'declaration' || node.kind === 'comment') continue - - if (scan(node.nodes) || (node.kind === 'at-rule' && node.name === '@scope')) { - containsAtScope.add(node) - found = true - } - } - return found - } - scan(ast) - } - - // Track `rule` selectors as we go - let selectorStack: [ - selector: string, - src: SourceLocation | undefined, - dst: SourceLocation | undefined, - ][] = [] - - // Track `at-rule` information as we go. Tracking this separately from the - // selector stack for rules such that we can hoist this above all the rules. - let atRuleStack: [ - name: string, - params: string, - src: SourceLocation | undefined, - dst: SourceLocation | undefined, - ][] = [] - - // The current "nodes" we can push to - let nodes = null as AstNode[] | null - - // Optimization: Track the declaration properties we've seen in the current - // nodes. - let seenDeclarationProperties = new Set() - - // Track nodes lists where we want to dedupe declarations - let dedupeDeclarationsInNodes = new Set() - - // The final, new AST - let result: AstNode[] = [] - - // Track whether we should skip a node in the `exit` phase - let skipExit = new Set() - - // Track the source of the subtree we are currently in. Variant bodies are - // wrapped in a `context` node with `source: 'variant'`, and the user content - // slotted into them is wrapped in `source: 'user'`. `@scope` rules whose - // nearest source is a variant are hoisted with the variant semantics, all - // other `@scope` rules use their native CSS nesting semantics. - let sourceStack: (string | boolean)[] = [] - - // Selector stacks that are masked while inside an `@scope` rule whose - // `` selector consumed the parent selector via `&`. - // - // Once the parent selector is part of the `` selector, the rules - // inside the `@scope` are relative to the scoping root, not to the parent - // rule. Selectors in scoped style rules only match elements in scope, and the - // parent matches ancestors of the scoping root, so prefixing the rules with - // the parent selector would make them match nothing at all. - // - // E.g.: - // - // ```css - // .parent { - // @scope (& > .scope) { - // .content { - // color: red; - // } - // } - // } - // ``` - // - // becomes: - // - // ```css - // @scope (.parent > .scope) { - // .content { /* ← not `.parent .content` */ - // color: red; - // } - // } - // ``` - let maskedSelectorStacks = new Map() - - walk(ast, { - enter(node) { - switch (node.kind) { - case 'rule': { - nodes = null // Start a new level - - // First time we see a rule - if (selectorStack.length === 0) { - // A rule with a selector containing `&` should replace the `&` with - // `:scope` if there is no parent rule. - // - // Note: there could be false positives when the `&` is escaped or - // part of a string inside an attribute selector. But the - // SelectorParser will take care of that. - if (node.selector.includes('&')) { - // Inside an `@scope` rule, the nesting selector behaves like - // `:where(:scope)` and matches the scoping root with zero - // specificity. E.g.: - // - // ```css - // @scope (.checkout) { - // & > p { - // color: red; - // } - // } - // ``` - // - // becomes: - // - // ```css - // @scope (.checkout) { - // :where(:scope) > p { - // color: red; - // } - // } - // ``` - // - // See: https://drafts.csswg.org/css-nesting-1/#nesting-at-scope - let replacement = atRuleStack.some(([name]) => name === '@scope') - ? ':where(:scope)' - : ':scope' - - let ast = SelectorParser.parse(node.selector) - let changed = false - - walk(ast, (node) => { - if (node.kind === 'selector' && node.value === '&') { - changed = true - node.value = replacement - } - }) - - if (changed) { - selectorStack.push([SelectorParser.toCss(ast), node.src, node.dst]) - } else { - selectorStack.push([node.selector, node.src, node.dst]) - } - } - - // No nesting markers, track as-is - else { - selectorStack.push([node.selector, node.src, node.dst]) - } - } - - // Nested rule, ensure `&` is present in each selector. Then track the - // selector. - else { - // A rule with just `&` can be replaced by its children. Let's - // ignore this node and keep walking its children. - if (node.selector === '&') { - skipExit.add(node) - return - } - - let lastSelector = selectorStack[selectorStack.length - 1][0] - let selector = segment(node.selector, ',') - .map((selector) => substituteNestingSelector(selector, lastSelector)) - .join(', ') - selectorStack.push([selector, node.src, node.dst]) - } - - // Once we hit a rule that has at least one declaration, then we can - // stop handling the nested selectors. - // - // This ensures that browser devtools can at least show _something_ - // for a given rule. This also means that we can leverage CSS nesting - // which is better for gzip results due to increased repetition. - // - // E.g.: - // - // ```css - // .a { - // &[b] { - // color: red; - // &:hover { - // color: blue; - // } - // } - // } - // ``` - // - // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ - // - // ```css - // .a[b] { /* ← flattened */ - // color: red; /* ← saw a declaration, therefore */ - // &:hover { /* ← we keep this nested syntax */ - // color: blue; - // } - // } - // ``` - // - // The exception is when the rule contains an `@scope` rule somewhere, - // because it must go through the normal hoisting machinery instead. - // See `containsAtScope` for more details. - if ( - node.nodes.some((child) => child.kind === 'declaration') && - !containsAtScope.has(node) - ) { - // Emitting each child instead of the node itself because we do want - // to flatten the current node and its selector that is already - // pushed to the stack. - for (let child of node.nodes) emit(child) - return WalkAction.Skip - } - break - } - - case 'at-rule': { - nodes = null // Start a new level - - // `@layer` is hoistable, but when it's empty then we have to make - // sure that we still emit it because this might influence the layer - // order. We can't just get rid of it. - if (node.nodes.length === 0 && !DROPPABLE_IF_EMPTY_AT_RULES.has(node.name)) { - emit(node) - skipExit.add(node) - return WalkAction.Skip - } - - // Hoist at-rules - else if (HOISTABLE_AT_RULES.has(node.name)) { - let params = node.params - - // `@scope` is the only hoistable at-rule with selectors in its - // prelude, and the only one that changes meaning when it stays - // nested inside a style rule. Resolve the prelude and its contents - // before hoisting, using one of two composition modes: - // - // 1. Variant-generated `@scope` rules attach the parent selector - // (the utility) to the rules _inside_ the scope: - // - // ```css - // .scoped\:underline { - // @scope (.from) to (.to) { - // text-decoration-line: underline; - // } - // } - // ``` - // - // becomes: - // - // ```css - // @scope (.from) to (.to) { - // .scoped\:underline { - // text-decoration-line: underline; - // } - // } - // ``` - // This way the utility is properly sandwiched between the - // `.from` and `.to` boundaries. - // - // 2. User-authored `@scope` rules keep their native CSS nesting - // semantics: the `` selector is relative to the parent - // rule, and the rules inside are relative to the scoping root: - // - // ```css - // .parent { - // @scope (.from) { - // color: red; - // } - // } - // ``` - // - // becomes: - // - // ```css - // @scope (.parent .from) { - // :where(:scope) { - // color: red; - // } - // } - // ``` - // - // In a perfect world we leave this alone, but Lightning CSS - // seems to incorrectly transform this into a version where the - // `.parent` class is gone entirely: - // - // ```css - // @scope (.from) { - // color: red; - // } - // ``` - // - // See: https://drafts.csswg.org/css-nesting-1/#nesting-at-scope - if (node.name === '@scope') { - let isVariant = sourceStack[sourceStack.length - 1] === 'variant' - let hasParent = selectorStack.length > 0 - - // Lightning CSS bug: This snippet will error - // - // ```css - // @scope { - // --x: 1; - // } - // ``` - // - // We will wrap these bare-declarations in a `&` rule to fix the crash: - // ```css - // @scope { - // & { - // --x: 1; - // } - // } - // ``` - { - let queue: (AtRule | Context)[] = [node] - - for (let parent of queue) { - let newNodes: AstNode[] = [] - let group: AstNode[] | null = null - - for (let child of parent.nodes) { - if (child.kind === 'context') queue.push(child) - - if (child.kind === 'declaration' || child.kind === 'comment') { - if (group === null) { - group = [] - newNodes.push(styleRule('&', group)) - } - group.push(child) - } else { - group = null - newNodes.push(child) - } - } - - parent.nodes = newNodes - } - } - - if (params.includes('&') || (!isVariant && hasParent)) { - // The selector to substitute for `&` in the `` - // selector: the elements matched by the nearest ancestor style - // rule, or `:scope` when there is none. - let parentSelector = hasParent ? selectorStack[selectorStack.length - 1][0] : null - - let paramsAst = ValueParser.parse(params) - - // Whether the current `(…)` group is a `` selector. - // - // ```css - // @scope (.from) to (.to) - // ^^^^^^^ scope-start (optional) - // ^^^^^ scope-end - // - // @scope to (.to) - // ^^^^^ scope-end - // ``` - let isScopeEnd = false - - // Whether the `` selector consumed the parent - // selector - let consumedParent = false - - for (let paramNode of paramsAst) { - if (paramNode.kind === 'word' && paramNode.value === 'to') { - isScopeEnd = true - continue - } - - // A `(…)` group parses as a function call without a name - if (paramNode.kind !== 'function' || paramNode.value !== '') continue - - let selector = ValueParser.toCss(paramNode.nodes) - - // A user-authored `` selector is relative to the - // parent rule, and resolves exactly like nested style rule - // selectors do: `&` refers to the elements matched by the - // parent rule, and selectors without `&` are prefixed with - // it. - if (!isScopeEnd && !isVariant && parentSelector !== null) { - paramNode.nodes = [ - ValueParser.word( - segment(selector, ',') - .map((part) => substituteNestingSelector(part.trim(), parentSelector)) - .join(', '), - ), - ] - consumedParent = true - } - - // Otherwise, only an explicit `&` is resolved. In the - // `` selector, `&` refers to the elements - // matched by the nearest ancestor style rule (or `:scope` - // when there is none). In the `` selector, `&` - // refers to the scoping root and behaves like - // `:where(:scope)`. - else if (selector.includes('&')) { - let replacement = isScopeEnd ? ':where(:scope)' : (parentSelector ?? ':scope') - - paramNode.nodes = [ - ValueParser.word(substituteNestingSelector(selector, replacement)), - ] - if (!isScopeEnd && hasParent) consumedParent = true - } - } - - params = ValueParser.toCss(paramsAst) - - // Mask the selector stack for the `@scope` rule's subtree, so - // its rules resolve like top-level rules within the scope (and - // `&` resolves to `:where(:scope)`). The stack is restored in - // the exit phase. See `maskedSelectorStacks` for more details. - if (consumedParent) { - maskedSelectorStacks.set(node, selectorStack) - selectorStack = [] - } - } - } - - atRuleStack.push([node.name, params, node.src, node.dst]) - } - - // If we can't hoist them, emit them immediately as-is - else { - emit(node) - skipExit.add(node) - return WalkAction.Skip - } - break - } - - case 'declaration': - case 'comment': { - emit(node) - break - } - - case 'context': - if (node.context.source) sourceStack.push(node.context.source) - break - - case 'at-root': - break - - default: - node satisfies never - break - } - }, - exit(node) { - if (skipExit.delete(node)) return - - switch (node.kind) { - case 'rule': { - nodes = null - selectorStack.pop() - break - } - - case 'at-rule': { - nodes = null - atRuleStack.pop() - - // Restore the selector stack that was masked by this `@scope` rule - let maskedSelectorStack = maskedSelectorStacks.get(node) - if (maskedSelectorStack) { - selectorStack = maskedSelectorStack - maskedSelectorStacks.delete(node) - } - break - } - - case 'context': - if (node.context.source) sourceStack.pop() - break - - case 'declaration': - case 'comment': - case 'at-root': - break - - default: - node satisfies never - break - } - }, - }) - - // Dedupe declarations that we've already seen before if they match the - // `property`, `value` and `important` information. - { - for (let nodes of dedupeDeclarationsInNodes) { - let seen = new Set() - for (let i = nodes.length - 1; i >= 0; --i) { - let node = nodes[i] - if (node.kind !== 'declaration') continue - - let id = `${node.property}\0${node.value}\0${node.important}` - - if (seen.has(id)) nodes.splice(i, 1) - else seen.add(id) - } - } - } - - return result - - function emit(node: AstNode) { - // Existing nodes are available, emit into those nodes - if (nodes) { - // Optimization: track used declarations in the current node. - if (node.kind === 'declaration') { - if (seenDeclarationProperties.has(node.property)) { - dedupeDeclarationsInNodes.add(nodes) - } else { - seenDeclarationProperties.add(node.property) - } - } - - nodes.push(node) - return - } - - // Nothing available, setup a fresh node - { - // There are no parent rules or at-rules available, which means that we - // can emit the node as-is. - if (selectorStack.length === 0 && atRuleStack.length === 0) { - let target = result - let lastNode = target[target.length - 1] - - // Optimization: when the current and last node are the same, ignore the - // new node entirely otherwise we will get unnecessary duplicate - // results. - // - // We only care about at-rules with no body because some of them (such - // as `@charset` or `@layer`) need to be emitted. A normal rule that's - // empty doesn't need to be emitted. At-rules _with_ a body (such as - // `@font-face` or `@keyframes`) can share the same prelude while - // containing different bodies, so they must all be emitted. - if ( - lastNode && - lastNode.kind === 'at-rule' && - node.kind === 'at-rule' && - lastNode.nodes.length === 0 && - node.nodes.length === 0 && - lastNode.name === node.name && - lastNode.params === node.params - ) { - return - } - - result.push(node) - return - } - - // Track the new "parent" nodes - { - nodes = [node] - - // Clear out seen declarations from the previous work in progress nodes - seenDeclarationProperties.clear() - - // Track new declaration - if (node.kind === 'declaration') { - seenDeclarationProperties.add(node.property) - } - } - - // Track the new root node that we build up to store in the final AST - let root = null as AstNode | null - - let target = result - let atRuleOffset = 0 - - // Optimization: merge adjacent at-rules - // - // Figure out whether we can push our new node into a previous node that - // was already emitted. - // - // We have to make sure that the order stays the same, so therefore we - // only ever have to look at the last node that was emitted. - { - let lastNode = target[target.length - 1] - if (lastNode && lastNode.kind === 'at-rule') { - for (let i = 0; i < atRuleStack.length; i++) { - let atRule = atRuleStack[i] - if (!lastNode) break - if (lastNode.kind !== 'at-rule') break - if (lastNode.name !== atRule[0]) break - if (lastNode.params !== atRule[1]) break - - atRuleOffset++ - target = lastNode.nodes - lastNode = lastNode.nodes[lastNode.nodes.length - 1] - } - } - } - - // Build up the rule - if (selectorStack.length > 0) { - let [selector, src, dst] = selectorStack[selectorStack.length - 1] - - // Optimization: merge adjacent rules with the same selector - // - // Figure out whether we can push into an existing rule. - // - // If we have some at-rules that we have to keep into account, then we - // definitely can't. - if (atRuleStack.length - atRuleOffset <= 0) { - let lastNode = target[target.length - 1] - if (lastNode && lastNode.kind === 'rule' && lastNode.selector === selector) { - lastNode.nodes.push(...nodes) - - // Ensure that our current nodes points to the nodes of the - // `lastNode`, otherwise we will lose information. - nodes = lastNode.nodes - - // We appended a group that could contain declarations already in the - // existing rule, so let the final dedupe pass handle it once. - // - // Note: we could loop over _all_ previous nodes to figure out if we - // really want to dedupe this. But this could result in a bunch of - // duplicate work if we have `n` nodes that we want to merge - // together. - dedupeDeclarationsInNodes.add(nodes) - - // We know that we don't have to handle any more at-rules, so we can - // bail early since we just merged the nodes with the same selector. - return - } - } - - // Can't push into existing node, create a new node - root = rule(selector, nodes) - if (src || dst) Object.assign(root, { src, dst }) - } - - // Wrap in at-rules, if we can push into an existing node then we can - // ignore `offset` amount of nodes since the `root`/`nodes` will already - // point to a nested node. - for (let i = atRuleStack.length - 1; i >= atRuleOffset; --i) { - let [name, params, src, dst] = atRuleStack[i] - - root = atRule(name, params, root ? [root] : nodes) - if (src || dst) Object.assign(root, { src, dst }) - } - - // Track the root node in the AST - if (root) { - target.push(root) - } - - // We didn't build up any new root, so we can move our node directly into - // the target. This can happen when we emit a node that is not a - // declaration or a comment. - else { - target.push(...nodes) - } - } - } - - // Substitutes the `&` selector with the parent selector with `:is(…)` semantics. - // - // If the `&` is not found, we will prepend it using a descendant combinator. - // E.g.: `& ${selector}` → `${parentSelector} ${selector}` - // - // In some situations we can get rid of the `:is(…)` wrapper, but only if it's safe to do so. - function substituteNestingSelector(selector: string, parentSelector: string): string { - // Slow path: we need to replace the `&` with the parent selector. A simple - // `replaceAll(…)` won't work because a `&` could be escaped, or could be - // part of an attribute selector. - // - // Much safer to parse the selector and replace the `&` that way - if (selector.includes('&')) { - let ast = SelectorParser.parse(selector) - let changed = false - walk(ast, { - enter(node, ctx) { - if (node.kind !== 'selector' || node.value !== '&') return - - changed = true - - // Safest option: use `is(…)` semantics when substituting `&` for the - // parent selector. - node.value = `:is(${parentSelector})` - - // We should always have a parent, so this shouldn't happen - if (ctx.parent === null) return - - // Optimizations: - let parentAst = parseSelectorCache.get(parentSelector) - - // 1. If we're dealing with multiple selectors, then we know that the - // `:is(…)` needs to stay. Nothing to optimize. - if (parentAst.length === 1 && parentAst[0].kind === 'list') { - return // Keep `:is(…)` semantics - } - - // 2. We know that `&` is standalone when it's inside of a complex - // selector. E.g. `[before] & [after]` - if (ctx.parent.kind === 'complex') { - // `& [after]` - // - // `:is(…)` semantics are not required, because these are equivalent: - // - // - `:is(div) [after]` → `div [after]` - // - `:is(.x) [after]` → `.x [after]` - // - `:is([before]) [after]` → `[before] [after]` - // - `:is(.a > .b) [after]` → `.a > .b [after]` - if (ctx.index === 0) { - node.value = parentSelector - return - } - - // `[before] &` - // - // `:is(…)` semantics are required if we're dealing with a complex - // parent selector. Otherwise the meaning of the selector could - // change: - // - // - `[before] :is(div)` → `[before] div` - // - `[before] :is(.x)` → `[before] .x` - // - `[before] :is([after])` → `[before] [after]` - // - `[before] :is(.a > .b)` → `[before] :is(.a > .b)` (!) - else if (ctx.index === ctx.siblings.length - 1) { - if (parentAst[0].kind === 'complex') { - return // Keep `:is(…)` semantics - } - - node.value = parentSelector - return - } - - // `[before] & [after]` - // - // `:is(…)` semantics are required if we're dealing with a complex - // parent selector. Otherwise the meaning of the selector could - // change: - // - // - `[before] :is(div) [after]` → `[before] div [after]` - // - `[before] :is(.x) [after]` → `[before] .x [after]` - // - `[before] :is(.a > .b) [after]` → `[before] :is(.a > .b) [after]` (!) - else { - if (parentAst[0].kind === 'complex') { - return // Keep `:is(…)` semantics - } - - node.value = parentSelector - return - } - } - - // 3. We know that `&` is attached to some other selector when it's - // inside of a compound selector. - // - // E.g. `[before]&[after]` - // - // We have to be careful that our parent, when it's part of a complex - // selector, that the same rules apply - // - // E.g.: `[before] &[after]` - // ^ current node - // ^^^^^^^^ compound selector - // ^^^^^^^^^^^^^^^^^ complex selector - // - else if (ctx.parent.kind === 'compound') { - if (parentAst[0].kind === 'complex') { - let path = ctx.path() - let grandParent = path[path.length - 2] - - // When our compound parent is part of a complex selector, and it's - // not the very first node, then we can't safely get rid of the - // `:is(…)` if the last selector is a complex selector as well, - // unless the `&` maps to a single selector or compound selector. - // - // ```css - // .foo .bar { /* Complex selector */ - // .system &:focus { /* Complex selector + compound selecto*/ - // --x: 1; - // } - // } - // .foo:hover { /* Compound selector */ - // .system &:focus { /* Complex selector + compound selector */ - // --x: 2; - // } - // } - // ``` - // - // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ - // - // ```css - // .system :is(.foo .bar):focus { /* Cannot drop the `:is(…)`, otherwise `.system` and `.foo` can be swapped in the DOM */ - // --x: 1; - // } - // .system .foo:hover:focus { - // --x: 2; - // } - // ``` - if ( - grandParent && - grandParent.kind === 'complex' && - grandParent.nodes[0] !== ctx.parent - ) { - return // Keep `:is(…)` semantics - } - } - - // `&*` and `&div` are invalid CSS so these should stay invalid. They - // should be written as `*&` and `div&` instead. - if ( - ctx.siblings - .slice(ctx.index + 1) - .some( - (sibling) => - SelectorParser.isUniversalSelector(sibling) || - SelectorParser.isTypeSelector(sibling), - ) - ) { - return // Keep `:is(…)` semantics - } - - // `&[after]` - // - // `:is(…)` semantics are not required, because these are equivalent: - // - // - `:is(div)[after]` → `div[after]` - // - `:is(.x)[after]` → `.x[after]` - // - `:is([before])[after]` → `[before][after]` - // - `:is(.a > .b)[after]` → `.a > .b[after]` - if (ctx.index === 0) { - node.value = parentSelector - return - } - - // `[before]&` - // - // `:is(…)` semantics are required if we're dealing with a - // - // - complex parent selector, to prevent changing the meaning of the selector - // - a universal selector, because `*` needs to be first - // - a type selector, because `div` needs to be first - // - // - `[before]:is(div)` → `[before]:is(div)` (!) - // - `[before]:is(*)` → `[before]:is(*)` (!) - // - `[before]:is(.a > .b)` → `[before]:is(.a > .b)` (!) - // - `[before]:is(.x)` → `[before].x` - else if (ctx.index === ctx.siblings.length - 1) { - if ( - parentAst[0].kind === 'complex' || - SelectorParser.isUniversalSelector(parentAst[0]) || - SelectorParser.isTypeSelector(parentAst[0]) - ) { - return // Keep `:is(…)` semantics - } - - node.value = parentSelector - return - } - - // `[before]&[after]` - // - // `:is(…)` semantics are required if we're dealing with a - // - // - complex parent selector, to prevent changing the meaning of the selector - // - a universal selector, because `*` needs to be first - // - a type selector, because `div` needs to be first - // - // - `[before]:is(div)[after]` → `[before]:is(div)[after]` (!) - // - `[before]:is(*)[after]` → `[before]:is(*)[after]` (!) - // - `[before]:is(.a > .b)[after]` → `[before]:is(.a > .b)[after]` (!) - // - `[before]:is(.x)[after]` → `[before].x[after]` - else { - if ( - parentAst[0].kind === 'complex' || - SelectorParser.isUniversalSelector(parentAst[0]) || - SelectorParser.isTypeSelector(parentAst[0]) - ) { - return // Keep `:is(…)` semantics - } - - node.value = parentSelector - return - } - } - - // 4. When the current node is a function argument (e.g. `:not(&))`, - // then we can drop the `:is(…)` entirely. - // - // The only exception is when the parent has multiple selectors - // because then we would introduce multiple arguments. Multiple - // selectors are already handled. - else if (ctx.parent.kind === 'function') { - node.value = parentSelector - return - } - }, - exit(node, ctx) { - // Optimization: We can remove the universal selector `*` if they are - // part of a compound selector. E.g.: - // - // - `*:hover` → `:hover` - // - `*[attribute]` → `[attribute]` - // - // Except when the `*` is a namespace, e.g.: `*|div`, because `*|div` - // (any namespace) and `|div` (no namespace) have different meanings. - if ( - ctx.index === 0 && - ctx.siblings.length > 1 && - ctx.parent?.kind === 'compound' && - SelectorParser.isUniversalSelector(node) - ) { - let next = ctx.siblings[1] - if (next.kind === 'selector' && next.value.charCodeAt(0) === PIPE) { - return - } - return WalkAction.ReplaceSkip([]) - } - }, - }) - - if (changed) return SelectorParser.toCss(ast) - } - - // Fast path: we know there isn't an `&` so we can prepend the parent - // selector immediately. - let parentAst = parseSelectorCache.get(parentSelector) - return `${parentAst.length === 1 && parentAst[0].kind === 'list' ? `:is(${parentSelector})` : parentSelector} ${selector}` - } -} - -// A set of at-rules that can be hoisted to the top without any repercussions. -// Typically at-rules that rely on the environment, not parent information and -// contain other rules/declarations. -const HOISTABLE_AT_RULES = new Set([ - '@container', - '@layer', - '@media', - '@page', - '@scope', - '@starting-style', - '@supports', - '@view-transition', -]) - -// A set of at-rules that can be dropped if they don't contain any nodes. We -// don't have the distinction between an at-rule with no body, or an at-rule -// with a body that is empty right now. -const DROPPABLE_IF_EMPTY_AT_RULES = new Set([ - '@container', - '@media', - '@page', - '@scope', - '@starting-style', - '@supports', - '@view-transition', -]) - -export function toCss(ast: AstNode[], track?: boolean) { - let pos = 0 - - let source: Source = { - file: null, - code: '', - } - - function stringify(node: AstNode, depth = 0): string { - let css = '' - let indent = ' '.repeat(depth) - - // 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 - else if (node.kind === 'at-rule') { - // Print at-rules without nodes with a `;` instead of an empty block. - // - // E.g.: - // - // ```css - // @layer base, components, utilities; - // ``` - if (node.nodes.length === 0) { - 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 + // Context Node + else if (node.kind === 'context') { + for (let child of node.nodes) { + css += stringify(child, depth) } } - // 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') { - return '' + // AtRoot Node + else if (node.kind === 'at-root') { + for (let child of node.nodes) { + atRoots += stringify(child, 0) + } + return css } - // Unknown - else { - node satisfies never + // Declaration + else if (node.property !== '--tw-sort' && node.value !== undefined && node.value !== null) { + css += `${indent}${node.property}: ${node.value}${node.important ? ' !important' : ''};\n` } return css @@ -1835,56 +227,29 @@ export function toCss(ast: AstNode[], track?: boolean) { let css = '' for (let node of ast) { - 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, ctx) => { - if (fn(node)) { - foundPath = ctx.path() - foundPath.push(node) - return WalkAction.Stop - } - }) - return foundPath -} - -// Find out if a variable is either used directly or if any of the variables that depend on it are -// used -function isVariableUsed( - variable: string, - theme: Theme, - variableDependencies: Map>, - alreadySeenVariables: Set = new Set(), -): boolean { - // Break recursions when visiting a variable twice - if (alreadySeenVariables.has(variable)) { - return true - } else { - alreadySeenVariables.add(variable) - } - - let options = theme.getOptions(variable) - if (options & (ThemeOptions.STATIC | ThemeOptions.USED)) { - return true - } else { - let dependencies = variableDependencies.get(variable) ?? [] - for (let dependency of dependencies) { - if (isVariableUsed(dependency, theme, variableDependencies, alreadySeenVariables)) { - return true - } + let result = stringify(node) + if (result !== '') { + css += result } } - return false -} + let fallbackAst = [] -function extractKeyframeNames(value: string): string[] { - return value.split(/[\s,]+/) + if (propertyFallbacksRoot.length) { + fallbackAst.push(rule(':root', propertyFallbacksRoot)) + } + + if (propertyFallbacksUniversal.length) { + fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)) + } + + let fallback = '' + + if (fallbackAst.length) { + fallback = stringify( + rule('@supports (-moz-orient: inline)', [rule('@layer base', fallbackAst)]), + ) + } + + return `${css}${fallback}${atRoots}` } diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index a19123105..deedd554f 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -1,13 +1,36 @@ -import dedent from 'dedent' import { expect, test, vi } from 'vitest' +import type { Plugin } from './compat/plugin-api' import { compile, type Config } from './index' import plugin from './plugin' -import { compileCss, run } from './test-utils/run' +import { optimizeCss } from './test-utils/run' -const css = dedent +const css = String.raw + +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 () => { - async function loadStylesheet(id: string, base: string) { + let loadStylesheet = async (id: string, base: string) => { expect(base).toBe('/root') expect(id).toBe('./foo/bar.css') return { @@ -17,20 +40,18 @@ test('can resolve relative @imports', async () => { } `, base: '/root/foo', - path: '', } } - expect( - await compileCss( + await expect( + run( css` @import './foo/bar.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - .foo { + ).resolves.toMatchInlineSnapshot(` + ".foo { color: red; } " @@ -38,14 +59,13 @@ test('can resolve relative @imports', async () => { }) test('can recursively resolve relative @imports', async () => { - async function loadStylesheet(id: string, base: string) { + let loadStylesheet = async (id: string, base: string) => { if (base === '/root' && id === './foo/bar.css') { return { content: css` @import './bar/baz.css'; `, base: '/root/foo', - path: '', } } else if (base === '/root/foo' && id === './bar/baz.css') { return { @@ -55,23 +75,21 @@ test('can recursively resolve relative @imports', async () => { } `, base: '/root/foo/bar', - path: '', } } throw new Error(`Unexpected import: ${id}`) } - expect( - await compileCss( + await expect( + run( css` @import './foo/bar.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - .baz { + ).resolves.toMatchInlineSnapshot(` + ".baz { color: #00f; } " @@ -83,56 +101,52 @@ let exampleCSS = css` color: red; } ` -async function loadStylesheet(id: string) { +let loadStylesheet = async (id: string) => { if (!id.endsWith('example.css')) throw new Error('Unexpected import: ' + id) return { content: exampleCSS, base: '/root', - path: '', } } test('extracts path from @import nodes', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - a { + ).resolves.toMatchInlineSnapshot(` + "a { color: red; } " `) - expect( - await compileCss( + await expect( + run( css` @import './example.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - a { + ).resolves.toMatchInlineSnapshot(` + "a { color: red; } " `) - expect( - await compileCss( + await expect( + run( css` @import '/example.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - a { + ).resolves.toMatchInlineSnapshot(` + "a { color: red; } " @@ -140,96 +154,89 @@ test('extracts path from @import nodes', async () => { }) test('url() imports are passed-through', async () => { - expect( - await compileCss( + await expect( + run( css` @import url('example.css'); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "example.css"; + ).resolves.toMatchInlineSnapshot(` + "@import url('example.css'); " `) - expect( - await compileCss( + await expect( + run( css` @import url('./example.css'); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "./example.css"; + ).resolves.toMatchInlineSnapshot(` + "@import url('./example.css'); " `) - expect( - await compileCss( + await expect( + run( css` @import url('/example.css'); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "/example.css"; + ).resolves.toMatchInlineSnapshot(` + "@import url('/example.css'); " `) - expect( - await compileCss( + await expect( + run( css` @import url(example.css); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "example.css"; + ).resolves.toMatchInlineSnapshot(` + "@import url(example.css); " `) - expect( - await compileCss( + await expect( + run( css` @import url(./example.css); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "./example.css"; + ).resolves.toMatchInlineSnapshot(` + "@import url(./example.css); " `) - expect( - await compileCss( + await expect( + run( css` @import url(/example.css); `, - { base: '/root', loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')) }, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @import "/example.css"; + ).resolves.toMatchInlineSnapshot(` + "@import url(/example.css); " `) }) test('handles case-insensitive @import directive', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css'; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - a { + ).resolves.toMatchInlineSnapshot(` + "a { color: red; } " @@ -237,16 +244,15 @@ test('handles case-insensitive @import directive', async () => { }) test('@media', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css' print; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @media print { + ).resolves.toMatchInlineSnapshot(` + "@media print { a { color: red; } @@ -254,16 +260,15 @@ test('@media', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' print, screen; `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @media print, screen { + ).resolves.toMatchInlineSnapshot(` + "@media print, screen { a { color: red; } @@ -271,16 +276,15 @@ test('@media', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' screen and (orientation: landscape); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @media screen and (orientation: landscape) { + ).resolves.toMatchInlineSnapshot(` + "@media screen and (orientation: landscape) { a { color: red; } @@ -288,16 +292,15 @@ test('@media', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' foo(bar); `, - { base: '/root', loadStylesheet }, + { loadStylesheet, optimize: false }, ), - ).toMatchInlineSnapshot(` - " - @media foo(bar) { + ).resolves.toMatchInlineSnapshot(` + "@media foo(bar) { a { color: red; } @@ -307,16 +310,15 @@ test('@media', async () => { }) test('@supports', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css' supports(display: grid); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @supports (display: grid) { + ).resolves.toMatchInlineSnapshot(` + "@supports (display: grid) { a { color: red; } @@ -324,17 +326,16 @@ test('@supports', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' supports(display: grid) screen and (max-width: 400px); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @supports (display: grid) { - @media screen and (max-width: 400px) { + ).resolves.toMatchInlineSnapshot(` + "@supports (display: grid) { + @media screen and (width <= 400px) { a { color: red; } @@ -343,18 +344,17 @@ test('@supports', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' supports((not (display: grid)) and (display: flex)) screen and (max-width: 400px); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @supports (not (display: grid)) and (display: flex) { - @media screen and (max-width: 400px) { + ).resolves.toMatchInlineSnapshot(` + "@supports (not (display: grid)) and (display: flex) { + @media screen and (width <= 400px) { a { color: red; } @@ -363,18 +363,17 @@ test('@supports', async () => { " `) - expect( - await compileCss( + await expect( + run( // prettier-ignore css` @import 'example.css' supports((selector(h2 > p)) and (font-tech(color-COLRv1))); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @supports selector(h2 > p) and font-tech(color-COLRv1) { + ).resolves.toMatchInlineSnapshot(` + "@supports selector(h2 > p) and font-tech(color-COLRv1) { a { color: red; } @@ -384,16 +383,15 @@ test('@supports', async () => { }) test('@layer', async () => { - expect( - await compileCss( + await expect( + run( css` @import 'example.css' layer(utilities); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + ).resolves.toMatchInlineSnapshot(` + "@layer utilities { a { color: red; } @@ -401,16 +399,15 @@ test('@layer', async () => { " `) - expect( - await compileCss( + await expect( + run( css` @import 'example.css' layer(); `, - { base: '/root', loadStylesheet }, + { loadStylesheet }, ), - ).toMatchInlineSnapshot(` - " - @layer { + ).resolves.toMatchInlineSnapshot(` + "@layer { a { color: red; } @@ -421,14 +418,12 @@ test('@layer', async () => { test('supports theme(reference) imports', async () => { expect( - await run( - ['text-red-500'], + run( css` @tailwind utilities; @import 'example.css' theme(reference); `, { - base: '/root', loadStylesheet: () => Promise.resolve({ content: css` @@ -437,13 +432,12 @@ test('supports theme(reference) imports', async () => { } `, base: '', - path: '', }), + candidates: ['text-red-500'], }, ), - ).toMatchInlineSnapshot(` - " - .text-red-500 { + ).resolves.toMatchInlineSnapshot(` + ".text-red-500 { color: var(--color-red-500, red); } " @@ -451,27 +445,27 @@ test('supports theme(reference) imports', async () => { }) test('updates the base when loading modules inside nested files', async () => { - async function loadStylesheet() { - return { + let loadStylesheet = () => + Promise.resolve({ content: css` @config './nested-config.js'; @plugin './nested-plugin.js'; `, base: '/root/foo', - path: '', - } - } - using loadModule = vi.fn().mockResolvedValue({ base: '', path: '', module: () => {} }) + }) + let loadModule = vi.fn().mockResolvedValue({ base: '', module: () => {} }) expect( - await compileCss( - css` - @import './foo/bar.css'; - @config './root-config.js'; - @plugin './root-plugin.js'; - `, - { base: '/root', loadStylesheet, loadModule }, - ), + ( + await run( + css` + @import './foo/bar.css'; + @config './root-config.js'; + @plugin './root-plugin.js'; + `, + { loadStylesheet, loadModule }, + ) + ).trim(), ).toBe('') expect(loadModule).toHaveBeenNthCalledWith(1, './nested-config.js', '/root/foo', 'config') @@ -481,15 +475,13 @@ test('updates the base when loading modules inside nested files', async () => { }) test('emits the right base for @source directives inside nested files', async () => { - async function loadStylesheet() { - return { + let loadStylesheet = () => + Promise.resolve({ content: css` @source './nested/**/*.css'; `, base: '/root/foo', - path: '', - } - } + }) let compiler = await compile( css` @@ -499,25 +491,22 @@ test('emits the right base for @source directives inside nested files', async () { base: '/root', loadStylesheet }, ) - expect(compiler.sources).toEqual([ - { pattern: './nested/**/*.css', base: '/root/foo', negated: false }, - { pattern: './root/**/*.css', base: '/root', negated: false }, + expect(compiler.globs).toEqual([ + { pattern: './nested/**/*.css', base: '/root/foo' }, + { pattern: './root/**/*.css', base: '/root' }, ]) }) test('emits the right base for @source found inside JS configs and plugins from nested imports', async () => { - async function loadStylesheet() { - return { + let loadStylesheet = () => + Promise.resolve({ content: css` @config './nested-config.js'; @plugin './nested-plugin.js'; `, base: '/root/foo', - path: '', - } - } - - using loadModule = vi.fn().mockImplementation((id: string) => { + }) + let loadModule = vi.fn().mockImplementation((id: string) => { let base = id.includes('nested') ? '/root/foo' : '/root' if (id.includes('config')) { let glob = id.includes('nested') ? './nested-config/*.html' : './root-config/*.html' @@ -530,14 +519,12 @@ 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: '', } } }) @@ -551,103 +538,35 @@ test('emits the right base for @source found inside JS configs and plugins from { base: '/root', loadStylesheet, loadModule }, ) - expect(compiler.sources).toEqual([ - { pattern: './nested-plugin/*.html', base: '/root/foo-plugin', negated: false }, - { pattern: './root-plugin/*.html', base: '/root-plugin', negated: false }, + expect(compiler.globs).toEqual([ + { pattern: './nested-plugin/*.html', base: '/root/foo-plugin' }, + { pattern: './root-plugin/*.html', base: '/root-plugin' }, - { pattern: './nested-config-plugin/*.html', base: '/root/foo-config', negated: false }, - { pattern: './nested-config/*.html', base: '/root/foo-config', negated: false }, + { pattern: './nested-config-plugin/*.html', base: '/root/foo-config' }, + { pattern: './nested-config/*.html', base: '/root/foo-config' }, - { pattern: './root-config-plugin/*.html', base: '/root-config', negated: false }, - { pattern: './root-config/*.html', base: '/root-config', negated: false }, + { pattern: './root-config-plugin/*.html', base: '/root-config' }, + { pattern: './root-config/*.html', base: '/root-config' }, ]) }) test('it crashes when inside a cycle', async () => { - let input = css` - @import 'foo.css'; - ` - - async function loadStylesheet() { - return { - content: input, + let loadStylesheet = () => + Promise.resolve({ + content: css` + @import 'foo.css'; + `, base: '/root', - path: '', - } - } + }) - await expect(compileCss(input, { base: '/root', loadStylesheet })).rejects.toMatchInlineSnapshot( + expect( + run( + css` + @import 'foo.css'; + `, + { loadStylesheet }, + ), + ).rejects.toMatchInlineSnapshot( `[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`, ) }) - -test('resolves @reference as `@import "…" reference`', async () => { - async function loadStylesheet(id: string, base: string) { - expect(base).toBe('/root') - expect(id).toBe('./foo/bar.css') - return { - content: css` - @theme { - --color-red-500: red; - } - .foo { - color: red; - } - `, - base: '/root/foo', - path: '', - } - } - - expect( - await run( - ['text-red-500'], - css` - @reference './foo/bar.css'; - @tailwind utilities; - `, - { base: '/root', loadStylesheet }, - ), - ).toMatchInlineSnapshot(` - " - .text-red-500 { - color: var(--color-red-500, red); - } - " - `) -}) - -test('resolves `@variant` used as `@custom-variant` inside `@reference`', async () => { - async function loadStylesheet(id: string, base: string) { - expect(base).toBe('/root') - expect(id).toBe('./foo/bar.css') - return { - content: css` - @variant dark { - &:where([data-theme='dark'] *) { - @slot; - } - } - `, - base: '/root/foo', - path: '', - } - } - - expect( - await run( - ['dark:flex'], - css` - @reference './foo/bar.css'; - @tailwind utilities; - `, - { base: '/root', loadStylesheet }, - ), - ).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 ba7172f8e..a06000826 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -1,127 +1,103 @@ -import { Features } from '.' -import { atRule, context, type AstNode } from './ast' +import { context, rule, walk, WalkAction, 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<{ - path: string - base: string - content: string -}> +type LoadStylesheet = (id: string, basedir: string) => Promise<{ 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) => { - if (node.kind === 'at-rule' && (node.name === '@import' || node.name === '@reference')) { - let parsed = parseImportParams(ValueParser.parse(node.params)) - if (parsed === null) return - if (node.name === '@reference') { - parsed.media = 'reference' + walk(ast, (node, { replaceWith }) => { + if ( + node.kind === 'rule' && + node.selector[0] === '@' && + node.selector.toLowerCase().startsWith('@import ') + ) { + try { + let { uri, layer, media, supports } = parseImportParams( + ValueParser.parse(node.selector.slice(8)), + ) + + // Skip importing data or remote URIs + if (uri.startsWith('data:')) return + if (uri.startsWith('http://') || uri.startsWith('https://')) return + + let contextNode = context({}, []) + + promises.push( + (async () => { + // Since we do not have fully resolved paths in core, we can't reliably detect circular + // imports. Instead, we try to limit the recursion depth to a number that is too large + // to be reached in practice. + if (recurseCount > 100) { + throw new Error( + `Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`, + ) + } + + const loaded = await loadStylesheet(uri, base) + let ast = CSS.parse(loaded.content) + await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1) + + contextNode.nodes = buildImportNodes(ast, layer, media, supports) + contextNode.context.base = loaded.base + })(), + ) + + replaceWith(contextNode) + // The resolved Stylesheets already have their transitive @imports + // resolved, so we can skip walking them. + return WalkAction.Skip + } catch (e: any) { + // When an error occurs while parsing the `@import` statement, we skip + // the import. } - - features |= Features.AtImport - - let { uri, layer, media, supports } = parsed - - // Skip importing data or remote URIs - if (uri.startsWith('data:')) return - if (uri.startsWith('http://') || uri.startsWith('https://')) return - - let contextNode = context({}, []) - - promises.push( - (async () => { - // Since we do not have fully resolved paths in core, we can't - // reliably detect circular imports. Instead, we try to limit the - // recursion depth to a number that is too large to be reached in - // practice. - if (recurseCount > 100) { - throw new Error( - `Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`, - ) - } - - let loaded = await loadStylesheet(uri, base) - 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, - supports, - ) - })(), - ) - - // The resolved Stylesheets already have their transitive @imports - // resolved, so we can skip walking them. - return WalkAction.ReplaceSkip(contextNode) } }) - if (promises.length > 0) { - await Promise.all(promises) - } - - return features + await Promise.all(promises) } // Modified and inlined version of `parse-statements` from // `postcss-import` // Copyright (c) 2014 Maxime Thirouin, Jason Campbell & Kevin Mårtensson // Released under the MIT License. -export function parseImportParams(params: ValueParser.ValueAstNode[]) { +function parseImportParams(params: ValueParser.ValueAstNode[]) { let uri let layer: string | null = null let media: string | null = null let supports: string | null = null for (let i = 0; i < params.length; i++) { - let node = params[i] + const node = params[i] if (node.kind === 'separator') continue if (node.kind === 'word' && !uri) { - if (!node.value) return null - if (node.value[0] !== '"' && node.value[0] !== "'") return null + if (!node.value) throw new Error(`Unable to find uri`) + if (node.value[0] !== '"' && node.value[0] !== "'") throw new Error('Unable to find uri') uri = node.value.slice(1, -1) continue } if (node.kind === 'function' && node.value.toLowerCase() === 'url') { - // `@import` with `url(…)` functions are not inlined but skipped and kept - // in the final CSS instead. - // E.g.: `@import url("https://fonts.google.com")` - return null + throw new Error('url functions are not supported') } - if (!uri) return null + if (!uri) throw new Error('Unable to find uri') if ( (node.kind === 'word' || node.kind === 'function') && node.value.toLowerCase() === 'layer' ) { - if (layer) return null - if (supports) { - throw new Error( - '`layer(…)` in an `@import` should come before any other functions or conditions', - ) - } + if (layer) throw new Error('Multiple layers') + if (supports) throw new Error('layers must be defined before support conditions') if ('nodes' in node) { layer = ValueParser.toCss(node.nodes) @@ -133,7 +109,7 @@ export function parseImportParams(params: ValueParser.ValueAstNode[]) { } if (node.kind === 'function' && node.value.toLowerCase() === 'supports') { - if (supports) return null + if (supports) throw new Error('Multiple support conditions') supports = ValueParser.toCss(node.nodes) continue } @@ -142,13 +118,12 @@ export function parseImportParams(params: ValueParser.ValueAstNode[]) { break } - if (!uri) return null + if (!uri) throw new Error('Unable to find uri') return { uri, layer, media, supports } } function buildImportNodes( - importNode: AstNode, importedAst: AstNode[], layer: string | null, media: string | null, @@ -157,21 +132,15 @@ function buildImportNodes( let root = importedAst if (layer !== null) { - let node = atRule('@layer', layer, root) - node.src = importNode.src - root = [node] + root = [rule('@layer ' + layer, root)] } if (media !== null) { - let node = atRule('@media', media, root) - node.src = importNode.src - root = [node] + root = [rule('@media ' + media, root)] } if (supports !== null) { - let node = atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root) - node.src = importNode.src - root = [node] + root = [rule(`@supports ${supports[0] === '(' ? supports : `(${supports})`}`, root)] } return root diff --git a/packages/tailwindcss/src/attribute-selector-parser.bench.ts b/packages/tailwindcss/src/attribute-selector-parser.bench.ts deleted file mode 100644 index 2d6e89590..000000000 --- a/packages/tailwindcss/src/attribute-selector-parser.bench.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 87f5f3013..000000000 --- a/packages/tailwindcss/src/attribute-selector-parser.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index 624043d59..000000000 --- a/packages/tailwindcss/src/attribute-selector-parser.ts +++ /dev/null @@ -1,232 +0,0 @@ -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 7ac0dbb0a..ef7c97114 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, describe } from 'vitest' -import { cloneCandidate, parseCandidate, type Candidate, type Variant } from './candidate' +import { bench } from 'vitest' +import { parseCandidate } from './candidate' import { buildDesignSystem } from './design-system' import { Theme } from './theme' @@ -8,87 +8,13 @@ import { Theme } from './theme' const root = process.env.FOLDER || process.cwd() // Auto content detection -const scanner = new Scanner({ sources: [{ base: root, pattern: '**/*', negated: false }] }) +const scanner = new Scanner({ detectSources: { base: root } }) const candidates = scanner.scan() const designSystem = buildDesignSystem(new Theme()) -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') +bench('parseCandidate', () => { + for (let candidate of candidates) { + Array.from(parseCandidate(candidate, designSystem)) } -} - -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 b8f914e1b..a0a84ce5c 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1,9 +1,8 @@ -import { describe, expect, it, test } from 'vitest' -import { __unstable__loadDesignSystem } from '.' +import { expect, it } from 'vitest' import { buildDesignSystem } from './design-system' import { Theme } from './theme' import { Utilities } from './utilities' -import { Compounds, Variants } from './variants' +import { Variants } from './variants' function run( candidate: string, @@ -42,6 +41,7 @@ it('should parse a simple utility', () => { { "important": false, "kind": "static", + "negative": false, "raw": "flex", "root": "flex", "variants": [], @@ -59,6 +59,7 @@ it('should parse a simple utility that should be important', () => { { "important": true, "kind": "static", + "negative": false, "raw": "flex!", "root": "flex", "variants": [], @@ -69,7 +70,7 @@ it('should parse a simple utility that should be important', () => { it('should parse a simple utility that can be negative', () => { let utilities = new Utilities() - utilities.functional('-translate-x', () => []) + utilities.functional('translate-x', () => []) expect(run('-translate-x-4', { utilities })).toMatchInlineSnapshot(` [ @@ -77,8 +78,9 @@ it('should parse a simple utility that can be negative', () => { "important": false, "kind": "functional", "modifier": null, + "negative": true, "raw": "-translate-x-4", - "root": "-translate-x", + "root": "translate-x", "value": { "fraction": null, "kind": "named", @@ -102,10 +104,12 @@ it('should parse a simple utility with a variant', () => { { "important": false, "kind": "static", + "negative": false, "raw": "hover:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "static", "root": "hover", }, @@ -128,14 +132,17 @@ it('should parse a simple utility with stacked variants', () => { { "important": false, "kind": "static", + "negative": false, "raw": "focus:hover:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "static", "root": "hover", }, { + "compounds": true, "kind": "static", "root": "focus", }, @@ -154,10 +161,12 @@ it('should parse a simple utility with an arbitrary variant', () => { { "important": false, "kind": "static", + "negative": false, "raw": "[&_p]:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "arbitrary", "relative": false, "selector": "& p", @@ -168,35 +177,6 @@ it('should parse a simple utility with an arbitrary variant', () => { `) }) -it('should parse an arbitrary variant using the automatic var shorthand', () => { - let utilities = new Utilities() - utilities.static('flex', () => []) - let variants = new Variants() - variants.functional('supports', () => {}) - - expect(run('supports-(--test):flex', { utilities, variants })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "static", - "raw": "supports-(--test):flex", - "root": "flex", - "variants": [ - { - "kind": "functional", - "modifier": null, - "root": "supports", - "value": { - "kind": "arbitrary", - "value": "var(--test)", - }, - }, - ], - }, - ] - `) -}) - it('should parse a simple utility with a parameterized variant', () => { let utilities = new Utilities() utilities.static('flex', () => []) @@ -209,10 +189,12 @@ it('should parse a simple utility with a parameterized variant', () => { { "important": false, "kind": "static", + "negative": false, "raw": "data-[disabled]:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "functional", "modifier": null, "root": "data", @@ -232,17 +214,19 @@ it('should parse compound variants with an arbitrary value as an arbitrary varia utilities.static('flex', () => []) let variants = new Variants() - variants.compound('group', Compounds.StyleRules, () => {}) + variants.compound('group', () => {}) expect(run('group-[&_p]/parent-name:flex', { utilities, variants })).toMatchInlineSnapshot(` [ { "important": false, "kind": "static", + "negative": false, "raw": "group-[&_p]/parent-name:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "compound", "modifier": { "kind": "named", @@ -250,6 +234,7 @@ it('should parse compound variants with an arbitrary value as an arbitrary varia }, "root": "group", "variant": { + "compounds": true, "kind": "arbitrary", "relative": false, "selector": "& p", @@ -266,7 +251,7 @@ it('should parse a simple utility with a parameterized variant and a modifier', utilities.static('flex', () => []) let variants = new Variants() - variants.compound('group', Compounds.StyleRules, () => {}) + variants.compound('group', () => {}) variants.functional('aria', () => {}) expect(run('group-aria-[disabled]/parent-name:flex', { utilities, variants })) @@ -275,10 +260,12 @@ it('should parse a simple utility with a parameterized variant and a modifier', { "important": false, "kind": "static", + "negative": false, "raw": "group-aria-[disabled]/parent-name:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "compound", "modifier": { "kind": "named", @@ -286,6 +273,7 @@ it('should parse a simple utility with a parameterized variant and a modifier', }, "root": "group", "variant": { + "compounds": true, "kind": "functional", "modifier": null, "root": "aria", @@ -307,7 +295,7 @@ it('should parse compound group with itself group-group-*', () => { let variants = new Variants() variants.static('hover', () => {}) - variants.compound('group', Compounds.StyleRules, () => {}) + variants.compound('group', () => {}) expect(run('group-group-group-hover/parent-name:flex', { utilities, variants })) .toMatchInlineSnapshot(` @@ -315,10 +303,12 @@ it('should parse compound group with itself group-group-*', () => { { "important": false, "kind": "static", + "negative": false, "raw": "group-group-group-hover/parent-name:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "compound", "modifier": { "kind": "named", @@ -326,14 +316,17 @@ it('should parse compound group with itself group-group-*', () => { }, "root": "group", "variant": { + "compounds": true, "kind": "compound", "modifier": null, "root": "group", "variant": { + "compounds": true, "kind": "compound", "modifier": null, "root": "group", "variant": { + "compounds": true, "kind": "static", "root": "hover", }, @@ -355,10 +348,12 @@ it('should parse a simple utility with an arbitrary media variant', () => { { "important": false, "kind": "static", + "negative": false, "raw": "[@media(width>=123px)]:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "arbitrary", "relative": false, "selector": "@media(width>=123px)", @@ -389,6 +384,7 @@ it('should parse a utility with a modifier', () => { "kind": "named", "value": "50", }, + "negative": false, "raw": "bg-red-500/50", "root": "bg", "value": { @@ -415,6 +411,7 @@ it('should parse a utility with an arbitrary modifier', () => { "kind": "arbitrary", "value": "50%", }, + "negative": false, "raw": "bg-red-500/[50%]", "root": "bg", "value": { @@ -441,6 +438,7 @@ it('should parse a utility with a modifier that is important', () => { "kind": "named", "value": "50", }, + "negative": false, "raw": "bg-red-500/50!", "root": "bg", "value": { @@ -470,6 +468,7 @@ it('should parse a utility with a modifier and a variant', () => { "kind": "named", "value": "50", }, + "negative": false, "raw": "hover:bg-red-500/50", "root": "bg", "value": { @@ -479,6 +478,7 @@ it('should parse a utility with a modifier and a variant', () => { }, "variants": [ { + "compounds": true, "kind": "static", "root": "hover", }, @@ -516,8 +516,6 @@ it('should not parse functional utilities with multiple modifiers', () => { utilities.functional('bg', () => []) expect(run('bg-red-1/2/3', { utilities })).toMatchInlineSnapshot(`[]`) - expect(run('bg-red-500/50/', { utilities })).toMatchInlineSnapshot(`[]`) - expect(run('bg-red-500/50//foo', { utilities })).toMatchInlineSnapshot(`[]`) }) it('should parse a utility with an arbitrary value', () => { @@ -530,6 +528,7 @@ it('should parse a utility with an arbitrary value', () => { "important": false, "kind": "functional", "modifier": null, + "negative": false, "raw": "bg-[#0088cc]", "root": "bg", "value": { @@ -543,43 +542,6 @@ it('should parse a utility with an arbitrary value', () => { `) }) -it('should not parse a utility with an incomplete arbitrary value', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-[#0088cc', { utilities })).toMatchInlineSnapshot(`[]`) -}) - -it('should parse a utility with an arbitrary value with parens', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-(--my-color)', { utilities })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "bg-(--my-color)", - "root": "bg", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "var(--my-color)", - }, - "variants": [], - }, - ] - `) -}) - -it('should not parse a utility with an arbitrary value with parens that does not start with --', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-(my-color)', { utilities })).toMatchInlineSnapshot(`[]`) -}) - it('should parse a utility with an arbitrary value including a typehint', () => { let utilities = new Utilities() utilities.functional('bg', () => []) @@ -590,6 +552,7 @@ it('should parse a utility with an arbitrary value including a typehint', () => "important": false, "kind": "functional", "modifier": null, + "negative": false, "raw": "bg-[color:var(--value)]", "root": "bg", "value": { @@ -603,59 +566,6 @@ it('should parse a utility with an arbitrary value including a typehint', () => `) }) -it('should parse a utility with an arbitrary value with parens including a typehint', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-(color:--my-color)', { utilities })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "bg-(color:--my-color)", - "root": "bg", - "value": { - "dataType": "color", - "kind": "arbitrary", - "value": "var(--my-color)", - }, - "variants": [], - }, - ] - `) -}) - -it('should not parse a utility with an arbitrary value with parens including a typehint that does not start with --', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-(color:my-color)', { utilities })).toMatchInlineSnapshot(`[]`) -}) - -it('should parse a utility with an arbitrary value with parens and a fallback', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-(color:--my-color,#0088cc)', { utilities })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "bg-(color:--my-color,#0088cc)", - "root": "bg", - "value": { - "dataType": "color", - "kind": "arbitrary", - "value": "var(--my-color,#0088cc)", - }, - "variants": [], - }, - ] - `) -}) - it('should parse a utility with an arbitrary value with a modifier', () => { let utilities = new Utilities() utilities.functional('bg', () => []) @@ -669,6 +579,7 @@ it('should parse a utility with an arbitrary value with a modifier', () => { "kind": "named", "value": "50", }, + "negative": false, "raw": "bg-[#0088cc]/50", "root": "bg", "value": { @@ -695,6 +606,7 @@ it('should parse a utility with an arbitrary value with an arbitrary modifier', "kind": "arbitrary", "value": "50%", }, + "negative": false, "raw": "bg-[#0088cc]/[50%]", "root": "bg", "value": { @@ -718,6 +630,7 @@ it('should parse a utility with an arbitrary value that is important', () => { "important": true, "kind": "functional", "modifier": null, + "negative": false, "raw": "bg-[#0088cc]!", "root": "bg", "value": { @@ -741,6 +654,7 @@ it('should parse a utility with an implicit variable as the arbitrary value', () "important": false, "kind": "functional", "modifier": null, + "negative": false, "raw": "bg-[var(--value)]", "root": "bg", "value": { @@ -764,6 +678,7 @@ it('should parse a utility with an implicit variable as the arbitrary value that "important": true, "kind": "functional", "modifier": null, + "negative": false, "raw": "bg-[var(--value)]!", "root": "bg", "value": { @@ -787,6 +702,7 @@ it('should parse a utility with an explicit variable as the arbitrary value', () "important": false, "kind": "functional", "modifier": null, + "negative": false, "raw": "bg-[var(--value)]", "root": "bg", "value": { @@ -810,6 +726,7 @@ it('should parse a utility with an explicit variable as the arbitrary value that "important": true, "kind": "functional", "modifier": null, + "negative": false, "raw": "bg-[var(--value)]!", "root": "bg", "value": { @@ -853,65 +770,6 @@ it('should not parse invalid arbitrary values', () => { } }) -it('should not parse invalid arbitrary values in variants', () => { - let utilities = new Utilities() - utilities.static('flex', () => []) - - let variants = new Variants() - variants.functional('data', () => {}) - - for (let candidate of [ - 'data-foo-[#0088cc]:flex', - 'data-foo[#0088cc]:flex', - - 'data-foo-[color:var(--value)]:flex', - 'data-foo[color:var(--value)]:flex', - - 'data-foo-[#0088cc]/50:flex', - 'data-foo[#0088cc]/50:flex', - - 'data-foo-[#0088cc]/[50%]:flex', - 'data-foo[#0088cc]/[50%]:flex', - - 'data-foo-[#0088cc]:flex!', - 'data-foo[#0088cc]:flex!', - - 'data-foo-[var(--value)]:flex', - 'data-foo[var(--value)]:flex', - - 'data-foo-[var(--value)]:flex!', - 'data-foo[var(--value)]:flex!', - - 'data-foo-(color:--value):flex', - 'data-foo(color:--value):flex', - - 'data-foo-(color:--value)/50:flex', - 'data-foo(color:--value)/50:flex', - - 'data-foo-(color:--value)/(--mod):flex', - 'data-foo(color:--value)/(--mod):flex', - - 'data-foo-(color:--value)/(number:--mod):flex', - 'data-foo(color:--value)/(number:--mod):flex', - - 'data-foo-(--value):flex', - 'data-foo(--value):flex', - - 'data-foo-(--value)/50:flex', - 'data-foo(--value)/50:flex', - - 'data-foo-(--value)/(--mod):flex', - 'data-foo(--value)/(--mod):flex', - - 'data-foo-(--value)/(number:--mod):flex', - 'data-foo(--value)/(number:--mod):flex', - - 'data-(value):flex', - ]) { - expect(run(candidate, { utilities, variants })).toEqual([]) - } -}) - it('should parse a utility with an implicit variable as the modifier', () => { let utilities = new Utilities() utilities.functional('bg', () => []) @@ -925,6 +783,7 @@ it('should parse a utility with an implicit variable as the modifier', () => { "kind": "arbitrary", "value": "var(--value)", }, + "negative": false, "raw": "bg-red-500/[var(--value)]", "root": "bg", "value": { @@ -938,338 +797,6 @@ it('should parse a utility with an implicit variable as the modifier', () => { `) }) -it('should properly decode escaped underscores but not convert underscores to spaces for CSS variables in arbitrary positions', () => { - let utilities = new Utilities() - utilities.functional('flex', () => []) - let variants = new Variants() - variants.functional('supports', () => {}) - - expect(run('flex-(--\\_foo)', { utilities, variants })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "flex-(--\\_foo)", - "root": "flex", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "var(--_foo)", - }, - "variants": [], - }, - ] - `) - expect(run('flex-(--_foo)', { utilities, variants })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "flex-(--_foo)", - "root": "flex", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "var(--_foo)", - }, - "variants": [], - }, - ] - `) - expect(run('flex-[var(--\\_foo)]', { utilities, variants })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "flex-[var(--\\_foo)]", - "root": "flex", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "var(--_foo)", - }, - "variants": [], - }, - ] - `) - expect(run('flex-[var(--_foo)]', { utilities, variants })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "flex-[var(--_foo)]", - "root": "flex", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "var(--_foo)", - }, - "variants": [], - }, - ] - `) - - expect(run('flex-[calc(var(--\\_foo)*0.2)]', { utilities, variants })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "flex-[calc(var(--\\_foo)*0.2)]", - "root": "flex", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "calc(var(--_foo) * 0.2)", - }, - "variants": [], - }, - ] - `) - expect(run('flex-[calc(var(--_foo)*0.2)]', { utilities, variants })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "flex-[calc(var(--_foo)*0.2)]", - "root": "flex", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "calc(var(--_foo) * 0.2)", - }, - "variants": [], - }, - ] - `) - - // Due to limitations in the CSS value parser, the `var(…)` inside the `calc(…)` is not correctly - // scanned here. - expect(run('flex-[calc(0.2*var(--\\_foo)]', { utilities, variants })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "flex-[calc(0.2*var(--\\_foo)]", - "root": "flex", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "calc(0.2 * var(--_foo))", - }, - "variants": [], - }, - ] - `) - expect(run('flex-[calc(0.2*var(--_foo)]', { utilities, variants })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "flex-[calc(0.2*var(--_foo)]", - "root": "flex", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "calc(0.2 * var(-- foo))", - }, - "variants": [], - }, - ] - `) -}) - -it('should parse a utility with an implicit variable as the modifier using the shorthand', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - // Standard case (no underscores) - expect(run('bg-red-500/(--value)', { utilities })).toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": { - "kind": "arbitrary", - "value": "var(--value)", - }, - "raw": "bg-red-500/(--value)", - "root": "bg", - "value": { - "fraction": null, - "kind": "named", - "value": "red-500", - }, - "variants": [], - }, - ] - `) - - // 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', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-red-500/(value)', { utilities })).toMatchInlineSnapshot(`[]`) -}) - it('should parse a utility with an implicit variable as the modifier that is important', () => { let utilities = new Utilities() utilities.functional('bg', () => []) @@ -1283,6 +810,7 @@ it('should parse a utility with an implicit variable as the modifier that is imp "kind": "arbitrary", "value": "var(--value)", }, + "negative": false, "raw": "bg-red-500/[var(--value)]!", "root": "bg", "value": { @@ -1309,6 +837,7 @@ it('should parse a utility with an explicit variable as the modifier', () => { "kind": "arbitrary", "value": "var(--value)", }, + "negative": false, "raw": "bg-red-500/[var(--value)]", "root": "bg", "value": { @@ -1335,6 +864,7 @@ it('should parse a utility with an explicit variable as the modifier that is imp "kind": "arbitrary", "value": "var(--value)", }, + "negative": false, "raw": "bg-red-500/[var(--value)]!", "root": "bg", "value": { @@ -1348,18 +878,6 @@ it('should parse a utility with an explicit variable as the modifier that is imp `) }) -it('should not parse a partial variant', () => { - let utilities = new Utilities() - utilities.static('flex', () => []) - - let variants = new Variants() - variants.static('open', () => {}) - variants.functional('data', () => {}) - - expect(run('open-:flex', { utilities, variants })).toMatchInlineSnapshot(`[]`) - expect(run('data-:flex', { utilities, variants })).toMatchInlineSnapshot(`[]`) -}) - it('should parse a static variant starting with @', () => { let utilities = new Utilities() utilities.static('flex', () => []) @@ -1372,10 +890,12 @@ it('should parse a static variant starting with @', () => { { "important": false, "kind": "static", + "negative": false, "raw": "@lg:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "static", "root": "@lg", }, @@ -1397,10 +917,12 @@ it('should parse a functional variant with a modifier', () => { { "important": false, "kind": "static", + "negative": false, "raw": "foo-bar/50:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "functional", "modifier": { "kind": "named", @@ -1418,19 +940,6 @@ it('should parse a functional variant with a modifier', () => { `) }) -it('should not parse variants with multiple modifiers', () => { - let utilities = new Utilities() - utilities.static('flex', () => []) - - let variants = new Variants() - variants.static('hover', () => {}) - variants.compound('group', Compounds.StyleRules, () => {}) - - expect(run('group-hover/foo:flex', { utilities, variants })).toHaveLength(1) - expect(run('group-hover/foo/:flex', { utilities, variants })).toEqual([]) - expect(run('group-hover/foo//bar:flex', { utilities, variants })).toEqual([]) -}) - it('should parse a functional variant starting with @', () => { let utilities = new Utilities() utilities.static('flex', () => []) @@ -1443,10 +952,12 @@ it('should parse a functional variant starting with @', () => { { "important": false, "kind": "static", + "negative": false, "raw": "@lg:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "functional", "modifier": null, "root": "@", @@ -1461,36 +972,6 @@ 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', () => []) @@ -1503,10 +984,12 @@ it('should parse a functional variant starting with @ and a modifier', () => { { "important": false, "kind": "static", + "negative": false, "raw": "@lg/name:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "functional", "modifier": { "kind": "named", @@ -1534,6 +1017,7 @@ it('should replace `_` with ` `', () => { "important": false, "kind": "functional", "modifier": null, + "negative": false, "raw": "content-["hello_world"]", "root": "content", "value": { @@ -1557,6 +1041,7 @@ it('should not replace `\\_` with ` ` (when it is escaped)', () => { "important": false, "kind": "functional", "modifier": null, + "negative": false, "raw": "content-["hello\\_world"]", "root": "content", "value": { @@ -1574,72 +1059,24 @@ it('should not replace `_` inside of `url()`', () => { let utilities = new Utilities() utilities.functional('bg', () => []) - expect(run('bg-[no-repeat_url(https://example.com/some_page)]', { utilities })) - .toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "bg-[no-repeat_url(https://example.com/some_page)]", - "root": "bg", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "no-repeat url(https://example.com/some_page)", - }, - "variants": [], + expect(run('bg-[url(https://example.com/some_page)]', { utilities })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "negative": false, + "raw": "bg-[url(https://example.com/some_page)]", + "root": "bg", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "url(https://example.com/some_page)", }, - ] - `) -}) - -it('should not replace `_` in the first argument to `var()`', () => { - let utilities = new Utilities() - utilities.functional('ml', () => []) - - expect(run('ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]', { utilities })) - .toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]", - "root": "ml", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "var(--spacing-1_5, var(--spacing-2_5, 1rem))", - }, - "variants": [], - }, - ] - `) -}) - -it('should not replace `_` in the first argument to `theme()`', () => { - let utilities = new Utilities() - utilities.functional('ml', () => []) - - expect(run('ml-[theme(--spacing-1_5,_theme(--spacing-2_5,_1rem))]', { utilities })) - .toMatchInlineSnapshot(` - [ - { - "important": false, - "kind": "functional", - "modifier": null, - "raw": "ml-[theme(--spacing-1_5,_theme(--spacing-2_5,_1rem))]", - "root": "ml", - "value": { - "dataType": null, - "kind": "arbitrary", - "value": "theme(--spacing-1_5, theme(--spacing-2_5, 1rem))", - }, - "variants": [], - }, - ] - `) + "variants": [], + }, + ] + `) }) it('should parse arbitrary properties', () => { @@ -1716,6 +1153,7 @@ it('should parse arbitrary properties with a variant', () => { "value": "red", "variants": [ { + "compounds": true, "kind": "static", "root": "hover", }, @@ -1741,10 +1179,12 @@ it('should parse arbitrary properties with stacked variants', () => { "value": "red", "variants": [ { + "compounds": true, "kind": "static", "root": "hover", }, { + "compounds": true, "kind": "static", "root": "focus", }, @@ -1766,11 +1206,13 @@ it('should parse arbitrary properties that are important and using stacked arbit "value": "red", "variants": [ { + "compounds": true, "kind": "arbitrary", "relative": false, "selector": "& p", }, { + "compounds": true, "kind": "arbitrary", "relative": false, "selector": "@media(width>=123px)", @@ -1786,19 +1228,11 @@ it('should not parse compound group with a non-compoundable variant', () => { utilities.static('flex', () => []) let variants = new Variants() - variants.compound('group', Compounds.StyleRules, () => {}) + variants.compound('group', () => {}) 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', () => []) @@ -1811,10 +1245,12 @@ it('should parse a variant containing an arbitrary string with unbalanced parens { "important": false, "kind": "static", + "negative": false, "raw": "string-['}[("\\'']:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "functional", "modifier": null, "root": "string", @@ -1845,6 +1281,7 @@ it('should parse candidates with a prefix', () => { { "important": false, "kind": "static", + "negative": false, "raw": "tw:flex", "root": "flex", "variants": [], @@ -1856,10 +1293,12 @@ it('should parse candidates with a prefix', () => { { "important": false, "kind": "static", + "negative": false, "raw": "tw:hover:flex", "root": "flex", "variants": [ { + "compounds": true, "kind": "static", "root": "hover", }, @@ -1868,289 +1307,3 @@ it('should parse candidates with a prefix', () => { ] `) }) - -it.each([ - // Empty arbitrary value - 'bg-[]', - 'bg-()', - // — Tricking the parser with a space is not allowed - 'bg-[_]', - 'bg-(_)', - - // Empty arbitrary value, with typehint - 'bg-[color:]', - 'bg-(color:)', - // — Tricking the parser with a space is not allowed - 'bg-[color:_]', - 'bg-(color:_)', - - // Empty arbitrary modifier - 'bg-red-500/[]', - 'bg-red-500/()', - // — Tricking the parser with a space is not allowed - 'bg-red-500/[_]', - 'bg-red-500/(_)', - - // Empty arbitrary modifier for arbitrary properties - '[color:red]/[]', - '[color:red]/()', - // — Tricking the parser with a space is not allowed - '[color:red]/[_]', - '[color:red]/(_)', - - // Empty arbitrary value and modifier - 'bg-[]/[]', - 'bg-()/[]', - 'bg-[]/()', - 'bg-()/()', - // — Tricking the parser with a space is not allowed - 'bg-[_]/[]', - 'bg-(_)/[]', - 'bg-[_]/()', - 'bg-(_)/()', - 'bg-[]/[_]', - 'bg-()/[_]', - 'bg-[]/(_)', - 'bg-()/(_)', - 'bg-[_]/[_]', - 'bg-(_)/[_]', - 'bg-[_]/(_)', - 'bg-(_)/(_)', - - // Functional variants - // Empty arbitrary value in variant - 'data-[]:flex', - 'data-():flex', - // — Tricking the parser with a space is not allowed - 'data-[_]:flex', - 'data-(_):flex', - - // Empty arbitrary modifier in variant - 'data-foo/[]:flex', - 'data-foo/():flex', - // — Tricking the parser with a space is not allowed - 'data-foo/[_]:flex', - 'data-foo/(_):flex', - - // Empty arbitrary value and modifier in variant - 'data-[]/[]:flex', - 'data-()/[]:flex', - 'data-[]/():flex', - 'data-()/():flex', - // — Tricking the parser with a space is not allowed - 'data-[_]/[]:flex', - 'data-(_)/[]:flex', - 'data-[_]/():flex', - 'data-(_)/():flex', - 'data-[]/[_]:flex', - 'data-()/[_]:flex', - 'data-[]/(_):flex', - 'data-()/(_):flex', - 'data-[_]/[_]:flex', - 'data-(_)/[_]:flex', - 'data-[_]/(_):flex', - 'data-(_)/(_):flex', - - // Compound variants - // Empty arbitrary value in variant - 'group-data-[]:flex', - 'group-data-():flex', - // — Tricking the parser with a space is not allowed - 'group-data-[_]:flex', - 'group-data-(_):flex', - - // Empty arbitrary modifier in variant - 'group-data-foo/[]:flex', - 'group-data-foo/():flex', - // — Tricking the parser with a space is not allowed - 'group-data-foo/[_]:flex', - 'group-data-foo/(_):flex', - - // Empty arbitrary value and modifier in variant - 'group-data-[]/[]:flex', - 'group-data-()/[]:flex', - 'group-data-[]/():flex', - 'group-data-()/():flex', - // — Tricking the parser with a space is not allowed - 'group-data-[_]/[]:flex', - 'group-data-(_)/[]:flex', - 'group-data-[_]/():flex', - 'group-data-(_)/():flex', - 'group-data-[]/[_]:flex', - 'group-data-()/[_]:flex', - 'group-data-[]/(_):flex', - 'group-data-()/(_):flex', - 'group-data-[_]/[_]:flex', - 'group-data-(_)/[_]:flex', - 'group-data-[_]/(_):flex', - 'group-data-(_)/(_):flex', -])('should not parse invalid empty arbitrary values: %s', (rawCandidate) => { - let utilities = new Utilities() - utilities.static('flex', () => []) - utilities.functional('bg', () => []) - - let variants = new Variants() - variants.functional('data', () => {}) - variants.compound('group', Compounds.StyleRules, () => {}) - - expect(run(rawCandidate, { utilities, variants })).toEqual([]) -}) - -it.each([ - // Arbitrary properties with `;` or `}` - '[color:red;color:blue]', - '[color:red}html{color:blue]', - - // Arbitrary values that end the declaration - 'bg-[red;color:blue]', - - // Arbitrary values that end the block - 'bg-[red}html{color:blue]', - - // Arbitrary variants that end the block - '[&{color:red}]:flex', - - // Arbitrary variant values that end the block - 'data-[a]{color:red}foo[a]:flex', - - // Named values contain invalid characters - 'data-foo=bar:flex', - 'bg-foo=bar', - 'bg-red-500/foo=bar', -])('should not parse invalid values: %s', (rawCandidate) => { - let utilities = new Utilities() - utilities.static('flex', () => []) - utilities.functional('bg', () => []) - - let variants = new Variants() - variants.functional('data', () => {}) - variants.compound('group', Compounds.StyleRules, () => {}) - - 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 a389af365..9738c9b0f 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -1,28 +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_.%-]+$/ -export type ArbitraryUtilityValue = { +type ArbitraryUtilityValue = { kind: 'arbitrary' /** * ``` * bg-[color:var(--my-color)] * ^^^^^ - * - * bg-(color:--my-color) - * ^^^^^ * ``` */ dataType: string | null @@ -34,9 +25,6 @@ export type ArbitraryUtilityValue = { * * bg-[var(--my_variable)] * ^^^^^^^^^^^^^^^^^^ - * - * bg-(--my_variable) - * ^^^^^^^^^^^^^^ * ``` */ value: string @@ -65,7 +53,7 @@ export type NamedUtilityValue = { fraction: string | null } -export type ArbitraryModifier = { +type ArbitraryModifier = { kind: 'arbitrary' /** @@ -77,7 +65,7 @@ export type ArbitraryModifier = { value: string } -export type NamedModifier = { +type NamedModifier = { kind: 'named' /** @@ -112,6 +100,9 @@ export type Variant = kind: 'arbitrary' selector: string + // If true, it can be applied as a child of a compound variant + compounds: boolean + // Whether or not the selector is a relative selector // @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#relative_selector relative: boolean @@ -125,6 +116,9 @@ export type Variant = | { kind: 'static' root: string + + // If true, it can be applied as a child of a compound variant + compounds: boolean } /** @@ -144,6 +138,9 @@ export type Variant = root: string value: ArbitraryVariantValue | NamedVariantValue | null modifier: ArbitraryModifier | NamedModifier | null + + // If true, it can be applied as a child of a compound variant + compounds: boolean } /** @@ -160,6 +157,9 @@ export type Variant = root: string modifier: ArbitraryModifier | NamedModifier | null variant: Variant + + // If true, it can be applied as a child of a compound variant + compounds: boolean } export type Candidate = @@ -195,6 +195,7 @@ export type Candidate = kind: 'static' root: string variants: Variant[] + negative: boolean important: boolean raw: string } @@ -214,106 +215,11 @@ export type Candidate = value: ArbitraryUtilityValue | NamedUtilityValue | null modifier: ArbitraryModifier | NamedModifier | null variants: Variant[] + negative: boolean important: boolean 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 @@ -346,6 +252,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter } let important = false + let negative = false // Candidates that end with an exclamation mark are the important version with // higher specificity of the non-important candidate, e.g. `mx-4!`. @@ -360,6 +267,13 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter base = base.slice(1) } + // Candidates that start with a dash are the negative versions of another + // candidate, e.g. `-mx-4`. + if (base[0] === '-') { + negative = true + base = base.slice(1) + } + // Check for an exact match of a static utility first as long as it does not // look like an arbitrary value. if (designSystem.utilities.has(base, 'static') && !base.includes('[')) { @@ -367,19 +281,12 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter kind: 'static', root: base, variants: parsedCandidateVariants, + negative, important, raw: input, } } - // If there's more than one modifier, the utility is invalid. - // - // E.g.: - // - // - `bg-red-500/50/50` - let parts = segment(base, '/') - if (parts.length > 2) return - // Figure out the new base and the modifier segment if present. // // E.g.: @@ -389,15 +296,14 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // ^^^^^^^^^^ -> Base without modifier // ^^ -> Modifier segment // ``` - let [baseWithoutModifier, modifierSegment = null] = parts + let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/') - let parsedModifier = modifierSegment === null ? null : parseModifier(modifierSegment) - - // Empty arbitrary values are invalid. E.g.: `[color:red]/[]` or `[color:red]/()`. - // ^^ ^^ - // `bg-[#0088cc]/[]` or `bg-[#0088cc]/()`. - // ^^ ^^ - if (modifierSegment !== null && parsedModifier === null) return + // If there's more than one modifier, the utility is invalid. + // + // E.g.: + // + // - `bg-red-500/50/50` + if (additionalModifier) return // Arbitrary properties if (baseWithoutModifier[0] === '[') { @@ -429,14 +335,11 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter let property = baseWithoutModifier.slice(0, idx) let value = decodeArbitraryValue(baseWithoutModifier.slice(idx + 1)) - // Values can't contain `;` or `}` characters at the top-level. - if (!isValidArbitrary(value)) return - yield { kind: 'arbitrary', property, value, - modifier: parsedModifier, + modifier: modifierSegment === null ? null : parseModifier(modifierSegment), variants: parsedCandidateVariants, important, raw: input, @@ -460,9 +363,9 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // ^^ -> Root // ^^^^^^^^^ -> Arbitrary value // - // border-l-[#0088cc] - // ^^^^^^^^ -> Root - // ^^^^^^^^^ -> Arbitrary value + // bg-red-[#0088cc] + // ^^^^^^ -> Root + // ^^^^^^^^^ -> Arbitrary value // ``` if (baseWithoutModifier[baseWithoutModifier.length - 1] === ']') { let idx = baseWithoutModifier.indexOf('-[') @@ -479,47 +382,6 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter roots = [[root, value]] } - // If the base of the utility ends with a `)`, then we know it's an arbitrary - // value that encapsulates a CSS variable. This also means that everything - // before the `(…)` part should be the root of the utility. - // - // E.g.: - // - // ``` - // bg-(--my-var) - // ^^ -> Root - // ^^^^^^^^^^ -> Arbitrary value - // ``` - else if (baseWithoutModifier[baseWithoutModifier.length - 1] === ')') { - let idx = baseWithoutModifier.indexOf('-(') - if (idx === -1) return - - let root = baseWithoutModifier.slice(0, idx) - - // The root of the utility should exist as-is in the utilities map. If not, - // it's an invalid utility and we can skip continue parsing. - if (!designSystem.utilities.has(root, 'functional')) return - - let value = baseWithoutModifier.slice(idx + 2, -1) - - let parts = segment(value, ':') - - let dataType = null - if (parts.length === 2) { - dataType = parts[0] - value = parts[1] - } - - // An arbitrary value with `(…)` should always start with `--` since it - // represents a CSS variable. - 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})]`]] - } - // Not an arbitrary value else { roots = findRoots(baseWithoutModifier, (root: string) => { @@ -531,9 +393,10 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter let candidate: Candidate = { kind: 'functional', root, - modifier: parsedModifier, + modifier: modifierSegment === null ? null : parseModifier(modifierSegment), value: null, variants: parsedCandidateVariants, + negative, important, raw: input, } @@ -548,16 +411,10 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter let valueIsArbitrary = startArbitraryIdx !== -1 if (valueIsArbitrary) { - // Arbitrary values must end with a `]`. - if (value[value.length - 1] !== ']') return - - let arbitraryValue = decodeArbitraryValue(value.slice(startArbitraryIdx + 1, -1)) - - // Values can't contain `;` or `}` characters at the top-level. - if (!isValidArbitrary(arbitraryValue)) continue + let arbitraryValue = value.slice(startArbitraryIdx + 1, -1) // Extract an explicit typehint if present, e.g. `bg-[color:var(--my-var)])` - let typehint: string | null = null + let typehint = '' for (let i = 0; i < arbitraryValue.length; i++) { let code = arbitraryValue.charCodeAt(i) @@ -577,18 +434,10 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter break } - // Empty arbitrary values are invalid. E.g.: `p-[]` - // ^^ - if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) { - continue - } - - if (typehint === '') continue - candidate.value = { kind: 'arbitrary', dataType: typehint || null, - value: arbitraryValue, + value: decodeArbitraryValue(arbitraryValue), } } else { // Some utilities support fractions as values, e.g. `w-1/2`. Since it's @@ -599,8 +448,6 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter ? null : `${value}/${modifierSegment}` - if (!IS_VALID_NAMED_VALUE.test(value)) continue - candidate.value = { kind: 'named', value, @@ -613,47 +460,16 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter } } -function parseModifier(modifier: string): CandidateModifier | null { +function parseModifier(modifier: string): CandidateModifier { if (modifier[0] === '[' && modifier[modifier.length - 1] === ']') { - let arbitraryValue = decodeArbitraryValue(modifier.slice(1, -1)) - - // Values can't contain `;` or `}` characters at the top-level. - if (!isValidArbitrary(arbitraryValue)) return null - - // Empty arbitrary values are invalid. E.g.: `data-[]:` - // ^^ - if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) return null + let arbitraryValue = modifier.slice(1, -1) return { kind: 'arbitrary', - value: arbitraryValue, + value: decodeArbitraryValue(arbitraryValue), } } - if (modifier[0] === '(' && modifier[modifier.length - 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(modifier)) return null - - // Wrap the value in `var(…)` to ensure that it is a valid CSS variable. - modifier = `var(${modifier})` - - let arbitraryValue = decodeArbitraryValue(modifier) - - return { - kind: 'arbitrary', - value: arbitraryValue, - } - } - - if (!IS_VALID_NAMED_VALUE.test(modifier)) return null - return { kind: 'named', value: modifier, @@ -680,13 +496,6 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia let selector = decodeArbitraryValue(variant.slice(1, -1)) - // Values can't contain `;` or `}` characters at the top-level. - if (!isValidArbitrary(selector)) return null - - // Empty arbitrary values are invalid. E.g.: `[]:` - // ^^ - if (selector.length === 0 || selector.trim().length === 0) return null - let relative = selector[0] === '>' || selector[0] === '+' || selector[0] === '~' // Ensure `&` is always present by wrapping the selector in `&:is(…)`, @@ -702,24 +511,24 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia return { kind: 'arbitrary', selector, + compounds: true, relative, } } // Static, functional and compound variants { + // group-hover/group-name + // ^^^^^^^^^^^ -> Variant without modifier + // ^^^^^^^^^^ -> Modifier + let [variantWithoutModifier, modifier = null, additionalModifier] = segment(variant, '/') + // If there's more than one modifier, the variant is invalid. // // E.g.: // // - `group-hover/foo/bar` - let parts = segment(variant, '/') - if (parts.length > 2) return null - - // group-hover/group-name - // ^^^^^^^^^^^ -> Variant without modifier - // ^^^^^^^^^^ -> Modifier - let [variantWithoutModifier, modifier = null] = parts + if (additionalModifier) return null let roots = findRoots(variantWithoutModifier, (root) => { return designSystem.variants.has(root) @@ -737,111 +546,56 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia return { kind: 'static', root, + compounds: designSystem.variants.compounds(root), } } case 'functional': { - let parsedModifier = modifier === null ? null : parseModifier(modifier) - // Empty arbitrary values are invalid. E.g.: `@max-md/[]:` or `@max-md/():` - // ^^ ^^ - if (modifier !== null && parsedModifier === null) return null - if (value === null) { return { kind: 'functional', root, - modifier: parsedModifier, + modifier: modifier === null ? null : parseModifier(modifier), value: null, + compounds: designSystem.variants.compounds(root), } } - if (value[value.length - 1] === ']') { - // Discard values like `foo-[#bar]` - if (value[0] !== '[') continue - - let arbitraryValue = decodeArbitraryValue(value.slice(1, -1)) - - // Values can't contain `;` or `}` characters at the top-level. - if (!isValidArbitrary(arbitraryValue)) return null - - // Empty arbitrary values are invalid. E.g.: `data-[]:` - // ^^ - if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) return null - + if (value[0] === '[' && value[value.length - 1] === ']') { return { kind: 'functional', root, - modifier: parsedModifier, + modifier: modifier === null ? null : parseModifier(modifier), value: { kind: 'arbitrary', - value: arbitraryValue, + value: decodeArbitraryValue(value.slice(1, -1)), }, + compounds: designSystem.variants.compounds(root), } } - if (value[value.length - 1] === ')') { - // Discard values like `foo-(--bar)` - if (value[0] !== '(') continue - - let arbitraryValue = decodeArbitraryValue(value.slice(1, -1)) - - // Values can't contain `;` or `}` characters at the top-level. - if (!isValidArbitrary(arbitraryValue)) return null - - // Empty arbitrary values are invalid. E.g.: `data-():` - // ^^ - 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 - - return { - kind: 'functional', - root, - modifier: parsedModifier, - value: { - kind: 'arbitrary', - value: `var(${arbitraryValue})`, - }, - } - } - - if (!IS_VALID_NAMED_VALUE.test(value)) continue - return { kind: 'functional', root, - modifier: parsedModifier, + modifier: modifier === null ? null : parseModifier(modifier), value: { kind: 'named', value }, + compounds: designSystem.variants.compounds(root), } } 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 - - // These two variants must be compatible when compounded - if (!designSystem.variants.compoundsWith(root, subVariant)) return null - - let parsedModifier = modifier === null ? null : parseModifier(modifier) - // Empty arbitrary values are invalid. E.g.: `group-focus/[]:` or `group-focus/():` - // ^^ ^^ - if (modifier !== null && parsedModifier === null) return null + if (subVariant.compounds === false) return null return { kind: 'compound', root, - modifier: parsedModifier, + modifier: modifier === null ? null : { kind: 'named', value: modifier }, variant: subVariant, + compounds: designSystem.variants.compounds(root), } } } @@ -870,6 +624,14 @@ 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. // @@ -878,7 +640,7 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable // `bg-red-500` -> No match // `bg-red` -> No match // `bg` -> Match - while (idx > 0) { + do { let maybeRoot = input.slice(0, idx) if (exists(maybeRoot)) { @@ -889,351 +651,9 @@ 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) - } - - // 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 + } while (idx > 0) } diff --git a/packages/tailwindcss/src/canonicalize-calc-expressions.test.ts b/packages/tailwindcss/src/canonicalize-calc-expressions.test.ts deleted file mode 100644 index f612e5f7b..000000000 --- a/packages/tailwindcss/src/canonicalize-calc-expressions.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect, it } from 'vitest' -import { canonicalizeCalcExpressions } from './canonicalize-calc-expressions' - -it.each([ - ['calc(-1 * var(--foo))', 'calc(var(--foo) * -1)'], - ['calc(1rem + var(--foo))', 'calc(var(--foo) + 1rem)'], - ['calc(2rem * calc(3px * var(--foo)))', 'calc(calc(var(--foo) * 3px) * 2rem)'], - ['calc(var(--b) + var(--a))', 'calc(var(--a) + var(--b))'], - ['calc(3px * 2rem)', 'calc(2rem * 3px)'], - ['calc(5px * 3px)', 'calc(3px * 5px)'], - ['calc(1 * 1rem)', 'calc(1rem * 1)'], - ['calc(10 + 2)', 'calc(2 + 10)'], -])('`%s` → `%s` (%#)', (input, expected) => { - expect(canonicalizeCalcExpressions(input)).toBe(expected) -}) - -it.each([ - ['calc(1rem - var(--foo))'], - ['calc(1rem / 2)'], - ['calc(var(--a) + 1rem)'], - ['calc(2rem * 3px)'], -])('should keep `%s` (%#)', (input) => { - expect(canonicalizeCalcExpressions(input)).toBe(input) -}) diff --git a/packages/tailwindcss/src/canonicalize-calc-expressions.ts b/packages/tailwindcss/src/canonicalize-calc-expressions.ts deleted file mode 100644 index 7e08f8622..000000000 --- a/packages/tailwindcss/src/canonicalize-calc-expressions.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { dimensions } from './utils/dimensions' -import * as ValueParser from './value-parser' -import { walk, WalkAction } from './walk' - -// Assumption: We already assume that we receive somewhat valid `calc()` -// expressions. So we will see `calc(1 + 1)` and not `calc(1+1)` -export function canonicalizeCalcExpressions(input: string): string { - let [canonicalized, valueAst] = canonicalizeCalcExpressionsAst(ValueParser.parse(input)) - - return canonicalized ? ValueParser.toCss(valueAst) : input -} - -export function canonicalizeCalcExpressionsAst( - ast: ValueParser.ValueAstNode[], -): [canonicalized: boolean, ast: ValueParser.ValueAstNode[]] { - let canonicalized = false - - walk(ast, { - exit(valueNode) { - // We are only interested in binary expressions in `calc(…)` and `(…)`, - // and only with the `*` or `+` operators. - if (valueNode.kind !== 'function') return - if (valueNode.value !== 'calc' && valueNode.value !== '') return - if (valueNode.nodes.length !== 5) return - if (valueNode.nodes[2].kind !== 'word') return - if (valueNode.nodes[2].value !== '*' && valueNode.nodes[2].value !== '+') return - - let lhs = valueNode.nodes[0] - let rhs = valueNode.nodes[4] - - if (shouldSwap(lhs, rhs)) { - canonicalized = true - - let replacement: ValueParser.ValueFunctionNode = { - kind: 'function', - value: valueNode.value, - nodes: [ - rhs, // Now lhs - valueNode.nodes[1], // Separator - valueNode.nodes[2], // Operator - valueNode.nodes[3], // Separator - lhs, // Now rhs - ], - } - - return WalkAction.ReplaceSkip(replacement) - } - }, - }) - - return [canonicalized, ast] -} - -function shouldSwap(lhs: ValueParser.ValueAstNode, rhs: ValueParser.ValueAstNode): boolean { - let lhsDimension = lhs.kind === 'word' ? dimensions.get(lhs.value) : null - let rhsDimension = rhs.kind === 'word' ? dimensions.get(rhs.value) : null - - if (lhsDimension !== null && rhsDimension === null) return true - if (lhsDimension === null && rhsDimension !== null) return false - - if (lhsDimension !== null && rhsDimension !== null) { - let [lhsValue, lhsUnit] = lhsDimension - let [rhsValue, rhsUnit] = rhsDimension - - // Within dimensions, keep unit-bearing values ahead of unitless numbers so - // `1rem` sorts before `1`. - if (lhsUnit === null && rhsUnit !== null) return true - if (lhsUnit !== null && rhsUnit === null) return false - - // Then sort dimensions numerically, and finally by unit for ties. - if (lhsValue !== rhsValue) { - return lhsValue - rhsValue > 0 - } - - if (lhsUnit !== rhsUnit) { - return (lhsUnit ?? '').localeCompare(rhsUnit ?? '') > 0 - } - } - - // Both nodes are unknown values (not dimensions), sort them as strings - return ValueParser.toCss([lhs]).localeCompare(ValueParser.toCss([rhs])) > 0 -} diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts deleted file mode 100644 index 567f0ad0d..000000000 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ /dev/null @@ -1,1663 +0,0 @@ -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 deleted file mode 100644 index d1dfc09de..000000000 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ /dev/null @@ -1,3062 +0,0 @@ -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 deleted file mode 100644 index f0f7bbba4..000000000 --- a/packages/tailwindcss/src/cartesian.ts +++ /dev/null @@ -1,45 +0,0 @@ -type CartesianInput = readonly unknown[][] - -type CartesianResult = T extends [ - infer Head extends unknown[], - ...infer Tail extends CartesianInput, -] - ? [Head[number], ...CartesianResult] - : [] - -export function* cartesian(...sets: T): Generator> { - let n = sets.length - if (n === 0) return - - // If any input set is empty, the Cartesian product is empty. - if (sets.some((set) => set.length === 0)) { - return - } - - // Index lookup - let idx = Array(n).fill(0) - - while (true) { - // Compute current combination - let result = [] as CartesianResult - for (let i = 0; i < n; i++) { - result[i] = sets[i][idx[i]] - } - yield result - - // Update index vector - let k = n - 1 - while (k >= 0) { - idx[k]++ - if (idx[k] < sets[k].length) { - break - } - idx[k] = 0 - k-- - } - - if (k < 0) { - return - } - } -} diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 036b9d88e..ff78bd5cf 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -1,17 +1,12 @@ -import { Features } from '..' -import { cssContext, styleRule, toCss, type AstNode } from '../ast' +import { rule, toCss, walk, WalkAction, 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 { applyKeyframesToAst } from './apply-keyframes-to-ast' import { createCompatConfig } from './config/create-compat-config' import { resolveConfig } from './config/resolve-config' import type { UserConfig } from './config/types' -import { registerContainerCompat } from './container' import { darkModePlugin } from './dark-mode' -import { registerLegacyUtilities } from './legacy-utilities' import { buildPluginApi, type CssPluginOptions, type Plugin } from './plugin-api' import { registerScreensConfig } from './screens-config' import { registerThemeVariantOverrides } from './theme-variants' @@ -23,7 +18,7 @@ export async function applyCompatibilityHooks({ base, ast, loadModule, - sources, + globs, }: { designSystem: DesignSystem base: string @@ -32,36 +27,22 @@ export async function applyCompatibilityHooks({ path: string, base: string, resourceHint: 'plugin' | 'config', - ) => Promise<{ - path: string - base: string - module: any - }> - sources: { base: string; pattern: string; negated: boolean }[] + ) => Promise<{ module: any; base: string }> + globs: { origin?: string; pattern: string }[] }) { - let features = Features.None - let pluginPaths: [ - { id: string; base: string; reference: boolean; src: SourceLocation | undefined }, - CssPluginOptions | null, - ][] = [] - let configPaths: { - id: string - base: string - reference: boolean - src: SourceLocation | undefined - }[] = [] + let pluginPaths: [{ id: string; base: string }, CssPluginOptions | null][] = [] + let configPaths: { id: string; base: string }[] = [] - walk(ast, (node, _ctx) => { - if (node.kind !== 'at-rule') return - let ctx = cssContext(_ctx) + walk(ast, (node, { parent, replaceWith, context }) => { + if (node.kind !== 'rule' || node.selector[0] !== '@') return // Collect paths from `@plugin` at-rules - if (node.name === '@plugin') { - if (ctx.parent !== null) { + if (node.selector === '@plugin' || node.selector.startsWith('@plugin ')) { + if (parent !== null) { throw new Error('`@plugin` cannot be nested.') } - let pluginPath = node.params.slice(1, -1) + let pluginPath = node.selector.slice(9, -1) if (pluginPath.length === 0) { throw new Error('`@plugin` must have a path.') } @@ -110,156 +91,123 @@ export async function applyCompatibilityHooks({ } pluginPaths.push([ - { - id: pluginPath, - base: ctx.context.base as string, - reference: !!ctx.context.reference, - src: node.src, - }, + { id: pluginPath, base: context.base }, Object.keys(options).length > 0 ? options : null, ]) - features |= Features.JsPluginCompat - return WalkAction.Replace([]) + replaceWith([]) + return } // Collect paths from `@config` at-rules - if (node.name === '@config') { + if (node.selector === '@config' || node.selector.startsWith('@config ')) { if (node.nodes.length > 0) { throw new Error('`@config` cannot have a body.') } - if (ctx.parent !== null) { + if (parent !== null) { throw new Error('`@config` cannot be nested.') } - configPaths.push({ - id: node.params.slice(1, -1), - base: ctx.context.base as string, - reference: !!ctx.context.reference, - src: node.src, - }) - features |= Features.JsPluginCompat - return WalkAction.Replace([]) + configPaths.push({ id: node.selector.slice(9, -1), base: context.base }) + replaceWith([]) + return } }) - registerLegacyUtilities(designSystem) - // Override `resolveThemeValue` with a version that is backwards compatible // with dot notation paths like `colors.red.500`. We could do this by default // in `resolveThemeValue` but handling it here keeps all backwards // compatibility concerns localized to our compatibility layer. let resolveThemeVariableValue = designSystem.resolveThemeValue - designSystem.resolveThemeValue = function resolveThemeValue(path: string, forceInline?: boolean) { + designSystem.resolveThemeValue = function resolveThemeValue(path: string) { if (path.startsWith('--')) { - return resolveThemeVariableValue(path, forceInline) + return resolveThemeVariableValue(path) } // If the theme value is not found in the simple resolver, we upgrade to the full backward // compatibility support implementation of the `resolveThemeValue` function. - features |= upgradeToFullPluginSupport({ + upgradeToFullPluginSupport({ designSystem, base, ast, - sources, + globs, configs: [], pluginDetails: [], }) - return designSystem.resolveThemeValue(path, forceInline) + return designSystem.resolveThemeValue(path) } // If there are no plugins or configs registered, we don't need to register // any additional backwards compatibility hooks. - if (!pluginPaths.length && !configPaths.length) return Features.None + if (!pluginPaths.length && !configPaths.length) return let [configs, pluginDetails] = await Promise.all([ Promise.all( - configPaths.map(async ({ id, base, reference, src }) => { + configPaths.map(async ({ id, base }) => { 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, src }, pluginOptions]) => { + pluginPaths.map(async ([{ id, base }, pluginOptions]) => { let loaded = await loadModule(id, base, 'plugin') return { path: id, base: loaded.base, plugin: loaded.module as Plugin, options: pluginOptions, - reference, - src, } }), ), ]) - features |= upgradeToFullPluginSupport({ + upgradeToFullPluginSupport({ designSystem, base, ast, - sources, + globs, configs, pluginDetails, }) - - return features } function upgradeToFullPluginSupport({ designSystem, base, ast, - sources, + globs, configs, pluginDetails, }: { designSystem: DesignSystem base: string ast: AstNode[] - sources: { base: string; pattern: string; negated: boolean }[] + globs: { origin?: string; pattern: string }[] configs: { path: string base: string config: UserConfig - reference: boolean - src: SourceLocation | undefined }[] pluginDetails: { path: string base: string plugin: Plugin options: CssPluginOptions | null - reference: boolean - src: SourceLocation | undefined }[] }) { - let features = Features.None let pluginConfigs = pluginDetails.map((detail) => { if (!detail.options) { - return { - config: { plugins: [detail.plugin] }, - base: detail.base, - reference: detail.reference, - src: detail.src, - } + return { config: { plugins: [detail.plugin] }, base: detail.base } } if ('__isOptionsFunction' in detail.plugin) { - return { - config: { plugins: [detail.plugin(detail.options)] }, - base: detail.base, - reference: detail.reference, - src: detail.src, - } + return { config: { plugins: [detail.plugin(detail.options)] }, base: detail.base } } throw new Error(`The plugin "${detail.path}" does not accept options`) @@ -267,100 +215,27 @@ function upgradeToFullPluginSupport({ let userConfig = [...pluginConfigs, ...configs] - let { resolvedConfig } = resolveConfig(designSystem, [ - { config: createCompatConfig(designSystem.theme), base, reference: true, src: undefined }, + let resolvedConfig = resolveConfig(designSystem, [ + { config: createCompatConfig(designSystem.theme), base }, ...userConfig, - { config: { plugins: [darkModePlugin] }, base, reference: true, src: undefined }, + { config: { plugins: [darkModePlugin] }, base }, ]) - let { resolvedConfig: resolvedUserConfig, replacedThemeKeys } = resolveConfig( - designSystem, - userConfig, - ) + let resolvedUserConfig = resolveConfig(designSystem, userConfig) - let pluginApiConfig = { - designSystem, - ast, - resolvedConfig, - featuresRef: { - set current(value: number) { - features |= value - }, - }, - } + let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig) - 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 - // upgrading dot-notation keys so we only use this version if plugins or - // config files are actually being used. In the future we may want to optimize - // this further by only doing this if plugins or config files _actually_ - // registered JS config objects. - let defaultResolveThemeValue = designSystem.resolveThemeValue - designSystem.resolveThemeValue = function resolveThemeValue(path: string, forceInline?: boolean) { - if (path[0] === '-' && path[1] === '-') { - return defaultResolveThemeValue(path, forceInline) - } - - let resolvedValue = sharedPluginApi.theme(path, undefined) - - // If we're dealing with an object that has the `DEFAULT` key, unwrap it to - // the default value first. This happens for namespace root lookups where - // the CSS theme defines a bare value, e.g. `--shadow` resolved via - // `theme('shadow')`. - if ( - typeof resolvedValue === 'object' && - resolvedValue !== null && - !Array.isArray(resolvedValue) && - 'DEFAULT' in resolvedValue - ) { - resolvedValue = resolvedValue.DEFAULT - } - - // When a tuple is returned, return the first element - if (Array.isArray(resolvedValue) && resolvedValue.length === 2) { - return resolvedValue[0] - } - - // Arrays get serialized into a comma-separated lists - else if (Array.isArray(resolvedValue)) { - return resolvedValue.join(', ') - } - - // 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 - } - } - - 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, - }) - - handler(api) + for (let { handler } of resolvedConfig.plugins) { + handler(pluginApi) } // 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) + applyConfigToTheme(designSystem, resolvedUserConfig) + applyKeyframesToAst(ast, resolvedUserConfig) registerThemeVariantOverrides(resolvedUserConfig, designSystem) registerScreensConfig(resolvedUserConfig, designSystem) - registerContainerCompat(resolvedUserConfig, designSystem) // If a prefix has already been set in CSS don't override it if (!designSystem.theme.prefix && resolvedConfig.prefix) { @@ -389,18 +264,18 @@ function upgradeToFullPluginSupport({ if (typeof resolvedConfig.important === 'string') { let wrappingSelector = resolvedConfig.important - walk(ast, (node, _ctx) => { - if (node.kind !== 'at-rule') return - if (node.name !== '@tailwind' || node.params !== 'utilities') return - - let ctx = cssContext(_ctx) + walk(ast, (node, { replaceWith, parent }) => { + if (node.kind !== 'rule') return + if (node.selector !== '@tailwind utilities') return // The AST node was already manually wrapped so there's nothing to do - if (ctx.parent?.kind === 'rule' && ctx.parent.selector === wrappingSelector) { + if (parent?.kind === 'rule' && parent.selector === wrappingSelector) { return WalkAction.Stop } - return WalkAction.ReplaceStop(styleRule(wrappingSelector, [node])) + replaceWith(rule(wrappingSelector, [node])) + + return WalkAction.Stop }) } @@ -408,6 +283,29 @@ function upgradeToFullPluginSupport({ designSystem.invalidCandidates.add(candidate) } + // 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 + // upgrading dot-notation keys so we only use this version if plugins or + // config files are actually being used. In the future we may want to optimize + // this further by only doing this if plugins or config files _actually_ + // registered JS config objects. + designSystem.resolveThemeValue = function resolveThemeValue(path: string, defaultValue?: string) { + let resolvedValue = pluginApi.theme(path, defaultValue) + + 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 + 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. + return resolvedValue + } + } + for (let file of resolvedConfig.content.files) { if ('raw' in file) { throw new Error( @@ -415,12 +313,6 @@ function upgradeToFullPluginSupport({ ) } - let negated = false - if (file.pattern[0] == '!') { - negated = true - file.pattern = file.pattern.slice(1) - } - sources.push({ ...file, negated }) + globs.push(file) } - return features } diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts index 12f195e94..41d24d184 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -1,14 +1,14 @@ -import { describe, expect, test } from 'vitest' +import { expect, test } from 'vitest' import { buildDesignSystem } from '../design-system' -import { Theme, ThemeOptions } from '../theme' -import { applyConfigToTheme, keyPathToCssProperty } from './apply-config-to-theme' +import { Theme } from '../theme' +import { applyConfigToTheme } from './apply-config-to-theme' import { resolveConfig } from './config/resolve-config' -test('config values can be merged into the theme', () => { +test('Config values can be merged into the theme', () => { let theme = new Theme() let design = buildDesignSystem(theme) - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ + let resolvedUserConfig = resolveConfig(design, [ { config: { theme: { @@ -23,24 +23,10 @@ test('config values can be merged into the theme', () => { sm: '1234px', }, - aspectRatio: { - retro: '4 / 3', - }, - boxShadow: { normal: '0 1px 3px black', }, - borderWidth: { - DEFAULT: '1.5px', - }, - outlineWidth: { - DEFAULT: '2.5px', - }, - ringWidth: { - DEFAULT: '3.5px', - }, - borderRadius: { sm: '0.33rem', }, @@ -62,258 +48,29 @@ 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: { - superWide: '0.25em', - }, - - lineHeight: { - superLoose: '3', - }, - - width: { - // Purposely setting to something different from the default - '1/2': '60%', - '0.5': '60%', - '100%': '100%', - }, - - maxWidth: { - '9xs': '6rem', - }, - - 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) + applyConfigToTheme(design, resolvedUserConfig) expect(theme.resolve('primary', ['--color'])).toEqual('#c0ffee') expect(theme.resolve('sm', ['--breakpoint'])).toEqual('1234px') expect(theme.resolve('normal', ['--shadow'])).toEqual('0 1px 3px black') - expect(theme.resolve('retro', ['--aspect'])).toEqual('4 / 3') expect(theme.resolve('sm', ['--radius'])).toEqual('0.33rem') expect(theme.resolve('blink', ['--animate'])).toEqual('blink 1s linear infinite') expect(theme.resolve('red-500', ['--color'])).toEqual('red') - expect(theme.resolve('sans', ['--font'])).toEqual('Inter, system-ui, sans-serif') - expect(theme.resolveWith('mono', ['--font'], ['--font-variation-settings'])).toEqual([ + expect(theme.resolve('sans', ['--font-family'])).toEqual('Inter, system-ui, sans-serif') + expect(theme.resolveWith('mono', ['--font-family'], ['--font-variation-settings'])).toEqual([ 'Potato Mono', { '--font-variation-settings': '"XHGT" 0.7' }, ]) - expect(theme.resolve('sm', ['--text'])).toEqual('0.875rem') - expect(theme.resolve('base', ['--text'])).toEqual('1rem') - expect(theme.resolveWith('base', ['--text'], ['--line-height'])).toEqual([ + expect(theme.resolve('sm', ['--font-size'])).toEqual('0.875rem') + expect(theme.resolve('base', ['--font-size'])).toEqual('1rem') + expect(theme.resolveWith('base', ['--font-size'], ['--line-height'])).toEqual([ '1rem', { '--line-height': '1.5' }, ]) - 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', () => { - let theme = new Theme() - let design = buildDesignSystem(theme) - - theme.add('--color-blue-400', 'lightblue', ThemeOptions.DEFAULT) - theme.add('--color-blue-500', 'blue', ThemeOptions.DEFAULT) - theme.add('--color-red-400', '#f87171') - theme.add('--color-red-500', '#ef4444') - - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ - { - config: { - theme: { - colors: { - blue: { - 500: '#3b82f6', - }, - red: { - 500: 'red', - }, - }, - extend: { - colors: { - blue: { - 600: '#2563eb', - }, - red: { - 600: '#dc2626', - }, - }, - }, - }, - }, - base: '/root', - reference: false, - src: undefined, - }, - ]) - applyConfigToTheme(design, resolvedConfig, replacedThemeKeys) - - expect(theme.namespace('--color')).toMatchInlineSnapshot(` - Map { - "red-400" => "#f87171", - "red-500" => "#ef4444", - "blue-500" => "#3b82f6", - "blue-600" => "#2563eb", - "red-600" => "#dc2626", - } - `) -}) - -test('invalid keys are not merged into the theme', () => { - let theme = new Theme() - let design = buildDesignSystem(theme) - - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ - { - config: { - theme: { - colors: { - 'primary color': '#86753099', - }, - }, - }, - base: '/root', - reference: false, - src: undefined, - }, - ]) - - applyConfigToTheme(design, resolvedConfig, replacedThemeKeys) - - let entries = Array.from(theme.entries()) - - expect(entries.length).toEqual(0) -}) - -describe('keyPathToCssProperty', () => { - test.each([ - [['width', '40', '2/5'], '--width-40-2/5'], - [['spacing', '0.5'], '--spacing-0_5'], - ])('converts %s to %s', (keyPath, expected) => { - expect(`--${keyPathToCssProperty(keyPath)}`).toEqual(expected) - }) -}) - -test('converts opacity modifiers from decimal to percentage values', () => { - let theme = new Theme() - let design = buildDesignSystem(theme) - - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ - { - config: { - theme: { - opacity: { - 0: '0', - 5: '0.05', - 10: '0.1', - 15: '0.15', - 20: 0.2, - 25: 0.25, - }, - }, - }, - base: '/root', - reference: false, - src: undefined, - }, - ]) - applyConfigToTheme(design, resolvedConfig, replacedThemeKeys) - - expect(theme.resolve('0', ['--opacity'])).toEqual('0%') - expect(theme.resolve('5', ['--opacity'])).toEqual('5%') - expect(theme.resolve('10', ['--opacity'])).toEqual('10%') - expect(theme.resolve('15', ['--opacity'])).toEqual('15%') - expect(theme.resolve('20', ['--opacity'])).toEqual('20%') - expect(theme.resolve('25', ['--opacity'])).toEqual('25%') -}) - -test('handles setting theme keys to null', async () => { - let theme = new Theme() - let design = buildDesignSystem(theme) - - theme.add('--color-blue-400', 'blue', ThemeOptions.DEFAULT) - theme.add('--color-blue-500', '#3b82f6') - theme.add('--color-red-400', 'red', ThemeOptions.DEFAULT) - theme.add('--color-red-500', '#ef4444') - - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ - { - config: { - theme: { - extend: { - colors: { - blue: null, - }, - }, - }, - }, - base: '/root', - reference: false, - src: undefined, - }, - ]) - applyConfigToTheme(design, resolvedConfig, replacedThemeKeys) - - expect(theme.namespace('--color')).toMatchInlineSnapshot(` - Map { - "blue-400" => "blue", - "blue-500" => "#3b82f6", - "red-400" => "red", - "red-500" => "#ef4444", - } - `) }) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index 8757520ee..f09d1770e 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -19,43 +19,15 @@ function resolveThemeValue(value: unknown, subValue: string | null = null): stri return null } -export function applyConfigToTheme( - designSystem: DesignSystem, - { theme }: ResolvedConfig, - replacedThemeKeys: Set, -) { - for (let replacedThemeKey of replacedThemeKeys) { - let name = keyPathToCssProperty([replacedThemeKey]) - if (!name) continue - - designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT) - } - +export function applyConfigToTheme(designSystem: DesignSystem, { theme }: ResolvedConfig) { for (let [path, value] of themeableValues(theme)) { if (typeof value !== 'string' && typeof value !== 'number') { continue } - - // Replace `` with `1` - if (typeof value === 'string') { - value = value.replace(//g, '1') - } - - // Convert `opacity` namespace from decimal to percentage values - if (path[0] === 'opacity' && (typeof value === 'number' || typeof value === 'string')) { - let numValue = typeof value === 'string' ? parseFloat(value) : value - - if (numValue >= 0 && numValue <= 1) { - value = numValue * 100 + '%' - } - } - let name = keyPathToCssProperty(path) - if (!name) continue - designSystem.theme.add( `--${name}`, - '' + value, + value as any, ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT, ) } @@ -72,7 +44,7 @@ export function applyConfigToTheme( // Replace `--default-font-*` with `fontFamily.sans` values { let fontFamily = resolveThemeValue(theme.fontFamily.sans) - if (fontFamily && designSystem.theme.hasDefault('--font-sans')) { + if (fontFamily && designSystem.theme.hasDefault('--font-family-sans')) { designSystem.theme.add('--default-font-family', fontFamily, options) designSystem.theme.add( '--default-font-feature-settings', @@ -90,7 +62,7 @@ export function applyConfigToTheme( // Replace `--default-mono-font-*` with `fontFamily.mono` values { let fontFamily = resolveThemeValue(theme.fontFamily.mono) - if (fontFamily && designSystem.theme.hasDefault('--font-mono')) { + if (fontFamily && designSystem.theme.hasDefault('--font-family-mono')) { designSystem.theme.add('--default-mono-font-family', fontFamily, options) designSystem.theme.add( '--default-mono-font-feature-settings', @@ -130,16 +102,7 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk } if (Array.isArray(value) && value.every((v) => typeof v === 'string')) { - 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(', ')]) - } - + toAdd.push([path, value.join(', ')]) return WalkAction.Skip } }) @@ -147,90 +110,26 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk return toAdd } -const SPECIAL_DEFAULT_KEYS: Record = { - borderWidth: 'border-width', - outlineWidth: 'outline-width', - ringColor: 'ring-color', - ringWidth: 'ring-width', - transitionDuration: 'transition-duration', - transitionTimingFunction: 'transition-timing-function', -} - -const OLD_TO_NEW_NAMESPACE: Record = { - animation: 'animate', - aspectRatio: 'aspect', - borderRadius: 'radius', - boxShadow: 'shadow', - colors: 'color', - containers: 'container', - fontFamily: 'font', - fontSize: 'text', - letterSpacing: 'tracking', - lineHeight: 'leading', - maxWidth: 'container', - screens: 'breakpoint', - transitionTimingFunction: 'ease', -} - -const IS_VALID_KEY = /^[a-zA-Z0-9-_%/.]+$/ - 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 - - 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 - } + if (path[0] === 'colors') path[0] = 'color' + if (path[0] === 'screens') path[0] = 'breakpoint' + if (path[0] === 'borderRadius') path[0] = 'radius' + if (path[0] === 'boxShadow') path[0] = 'shadow' + if (path[0] === 'animation') path[0] = 'animate' return ( path // [1] should move into the nested object tuple. To create the CSS variable // name for this, we replace it with an empty string that will result in two // subsequent dashes when joined. - // - // E.g.: - // - `fontSize.xs.1.lineHeight` -> `font-size-xs--line-height` - // - `spacing.1` -> `--spacing-1` - .map((path, idx, all) => (path === '1' && idx !== all.length - 1 ? '' : path)) + .map((path) => (path === '1' ? '' : path)) // Resolve the key path to a CSS variable segment - .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 - }) + .map((part) => + part + .replaceAll('.', '_') + .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`), + ) // Remove the `DEFAULT` key at the end of a path // We're reading from CSS anyway so it'll be a string @@ -264,7 +163,7 @@ function isValidThemeTuple(value: unknown): value is [string, Record { + let theme = new Theme() + let design = buildDesignSystem(theme) + + let ast: AstNode[] = [] + + let resolvedUserConfig = resolveConfig(design, [ + { + config: { + theme: { + keyframes: { + 'fade-in': { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + 'fade-out': { + '0%': { opacity: '1' }, + '100%': { opacity: '0' }, + }, + }, + }, + }, + base: '/root', + }, + ]) + applyKeyframesToAst(ast, resolvedUserConfig) + + expect(toCss(ast)).toMatchInlineSnapshot(` + "@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + @keyframes fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-ast.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-ast.ts new file mode 100644 index 000000000..2ca12ab50 --- /dev/null +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-ast.ts @@ -0,0 +1,11 @@ +import { rule, type AstNode } from '../ast' +import type { ResolvedConfig } from './config/types' +import { objectToAst } from './plugin-api' + +export function applyKeyframesToAst(ast: AstNode[], { theme }: ResolvedConfig) { + if ('keyframes' in theme) { + for (let [name, keyframe] of Object.entries(theme.keyframes)) { + ast.push(rule(`@keyframes ${name}`, objectToAst(keyframe as any))) + } + } +} diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts deleted file mode 100644 index aed761fce..000000000 --- a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { expect, test } from 'vitest' -import { atRule, decl, styleRule, toCss } from '../ast' -import { buildDesignSystem } from '../design-system' -import { Theme } from '../theme' -import { applyKeyframesToTheme } from './apply-keyframes-to-theme' -import { resolveConfig } from './config/resolve-config' - -test('keyframes can be merged into the theme', () => { - let theme = new Theme() - let design = buildDesignSystem(theme) - - let { resolvedConfig } = resolveConfig(design, [ - { - config: { - theme: { - extend: { - keyframes: { - 'fade-in': { - '0%': { opacity: '0' }, - '100%': { opacity: '1' }, - }, - 'fade-out': { - '0%': { opacity: '1' }, - '100%': { opacity: '0' }, - }, - }, - }, - }, - }, - base: '/root', - reference: false, - src: undefined, - }, - ]) - applyKeyframesToTheme(design, resolvedConfig) - - expect(toCss(design.theme.getKeyframes())).toMatchInlineSnapshot(` - "@keyframes fade-in { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } - } - @keyframes fade-out { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } - } - " - `) -}) - -test('will append to the default keyframes with new keyframes', () => { - let theme = new Theme() - let design = buildDesignSystem(theme) - - theme.addKeyframes( - atRule('@keyframes', 'slide-in', [ - styleRule('from', [decl('opacity', 'translateX(0%)')]), - styleRule('to', [decl('opacity', 'translateX(100%)')]), - ]), - ) - theme.addKeyframes( - atRule('@keyframes', 'slide-out', [ - styleRule('from', [decl('opacity', 'translateX(100%)')]), - styleRule('to', [decl('opacity', 'translateX(0%)')]), - ]), - ) - - let { resolvedConfig } = resolveConfig(design, [ - { - config: { - theme: { - keyframes: { - 'fade-in': { - '0%': { opacity: '0' }, - '100%': { opacity: '1' }, - }, - }, - extend: { - keyframes: { - 'fade-out': { - '0%': { opacity: '1' }, - '100%': { opacity: '0' }, - }, - }, - }, - }, - }, - base: '/root', - reference: false, - src: undefined, - }, - ]) - applyKeyframesToTheme(design, resolvedConfig) - - expect(toCss(design.theme.getKeyframes())).toMatchInlineSnapshot(` - "@keyframes slide-in { - from { - opacity: translateX(0%); - } - to { - opacity: translateX(100%); - } - } - @keyframes slide-out { - from { - opacity: translateX(100%); - } - to { - opacity: translateX(0%); - } - } - @keyframes fade-in { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } - } - @keyframes fade-out { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } - } - " - `) -}) diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts deleted file mode 100644 index d5855545f..000000000 --- a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { atRule, type AtRule } from '../ast' -import type { DesignSystem } from '../design-system' -import type { ResolvedConfig } from './config/types' -import { objectToAst } from './plugin-api' - -export function applyKeyframesToTheme( - designSystem: DesignSystem, - resolvedConfig: Pick, -) { - for (let rule of keyframesToRules(resolvedConfig)) { - designSystem.theme.addKeyframes(rule) - } -} - -export function keyframesToRules(resolvedConfig: Pick): AtRule[] { - let rules: AtRule[] = [] - if ('keyframes' in resolvedConfig.theme) { - for (let [name, keyframe] of Object.entries(resolvedConfig.theme.keyframes)) { - rules.push(atRule('@keyframes', name, objectToAst(keyframe as any))) - } - } - return rules -} diff --git a/packages/tailwindcss/src/compat/colors.ts b/packages/tailwindcss/src/compat/colors.ts index 64e3bc9c7..9489134e2 100644 --- a/packages/tailwindcss/src/compat/colors.ts +++ b/packages/tailwindcss/src/compat/colors.ts @@ -1,345 +1,293 @@ export default { inherit: 'inherit', - current: 'currentcolor', + current: 'currentColor', transparent: 'transparent', black: '#000', white: '#fff', slate: { - '50': 'oklch(98.4% 0.003 247.858)', - '100': 'oklch(96.8% 0.007 247.896)', - '200': 'oklch(92.9% 0.013 255.508)', - '300': 'oklch(86.9% 0.022 252.894)', - '400': 'oklch(70.4% 0.04 256.788)', - '500': 'oklch(55.4% 0.046 257.417)', - '600': 'oklch(44.6% 0.043 257.281)', - '700': 'oklch(37.2% 0.044 257.287)', - '800': 'oklch(27.9% 0.041 260.031)', - '900': 'oklch(20.8% 0.042 265.755)', - '950': 'oklch(12.9% 0.042 264.695)', + 50: '#f8fafc', + 100: '#f1f5f9', + 200: '#e2e8f0', + 300: '#cbd5e1', + 400: '#94a3b8', + 500: '#64748b', + 600: '#475569', + 700: '#334155', + 800: '#1e293b', + 900: '#0f172a', + 950: '#020617', }, gray: { - '50': 'oklch(98.5% 0.002 247.839)', - '100': 'oklch(96.7% 0.003 264.542)', - '200': 'oklch(92.8% 0.006 264.531)', - '300': 'oklch(87.2% 0.01 258.338)', - '400': 'oklch(70.7% 0.022 261.325)', - '500': 'oklch(55.1% 0.027 264.364)', - '600': 'oklch(44.6% 0.03 256.802)', - '700': 'oklch(37.3% 0.034 259.733)', - '800': 'oklch(27.8% 0.033 256.848)', - '900': 'oklch(21% 0.034 264.665)', - '950': 'oklch(13% 0.028 261.692)', + 50: '#f9fafb', + 100: '#f3f4f6', + 200: '#e5e7eb', + 300: '#d1d5db', + 400: '#9ca3af', + 500: '#6b7280', + 600: '#4b5563', + 700: '#374151', + 800: '#1f2937', + 900: '#111827', + 950: '#030712', }, zinc: { - '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)', - '400': 'oklch(70.5% 0.015 286.067)', - '500': 'oklch(55.2% 0.016 285.938)', - '600': 'oklch(44.2% 0.017 285.786)', - '700': 'oklch(37% 0.013 285.805)', - '800': 'oklch(27.4% 0.006 286.033)', - '900': 'oklch(21% 0.006 285.885)', - '950': 'oklch(14.1% 0.005 285.823)', + 50: '#fafafa', + 100: '#f4f4f5', + 200: '#e4e4e7', + 300: '#d4d4d8', + 400: '#a1a1aa', + 500: '#71717a', + 600: '#52525b', + 700: '#3f3f46', + 800: '#27272a', + 900: '#18181b', + 950: '#09090b', }, neutral: { - '50': 'oklch(98.5% 0 none)', - '100': 'oklch(97% 0 none)', - '200': 'oklch(92.2% 0 none)', - '300': 'oklch(87% 0 none)', - '400': 'oklch(70.8% 0 none)', - '500': 'oklch(55.6% 0 none)', - '600': 'oklch(43.9% 0 none)', - '700': 'oklch(37.1% 0 none)', - '800': 'oklch(26.9% 0 none)', - '900': 'oklch(20.5% 0 none)', - '950': 'oklch(14.5% 0 none)', + 50: '#fafafa', + 100: '#f5f5f5', + 200: '#e5e5e5', + 300: '#d4d4d4', + 400: '#a3a3a3', + 500: '#737373', + 600: '#525252', + 700: '#404040', + 800: '#262626', + 900: '#171717', + 950: '#0a0a0a', }, stone: { - '50': 'oklch(98.5% 0.001 106.423)', - '100': 'oklch(97% 0.001 106.424)', - '200': 'oklch(92.3% 0.003 48.717)', - '300': 'oklch(86.9% 0.005 56.366)', - '400': 'oklch(70.9% 0.01 56.259)', - '500': 'oklch(55.3% 0.013 58.071)', - '600': 'oklch(44.4% 0.011 73.639)', - '700': 'oklch(37.4% 0.01 67.558)', - '800': 'oklch(26.8% 0.007 34.298)', - '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)', + 50: '#fafaf9', + 100: '#f5f5f4', + 200: '#e7e5e4', + 300: '#d6d3d1', + 400: '#a8a29e', + 500: '#78716c', + 600: '#57534e', + 700: '#44403c', + 800: '#292524', + 900: '#1c1917', + 950: '#0c0a09', }, red: { - '50': 'oklch(97.1% 0.013 17.38)', - '100': 'oklch(93.6% 0.032 17.717)', - '200': 'oklch(88.5% 0.062 18.334)', - '300': 'oklch(80.8% 0.114 19.571)', - '400': 'oklch(70.4% 0.191 22.216)', - '500': 'oklch(63.7% 0.237 25.331)', - '600': 'oklch(57.7% 0.245 27.325)', - '700': 'oklch(50.5% 0.213 27.518)', - '800': 'oklch(44.4% 0.177 26.899)', - '900': 'oklch(39.6% 0.141 25.723)', - '950': 'oklch(25.8% 0.092 26.042)', + 50: '#fef2f2', + 100: '#fee2e2', + 200: '#fecaca', + 300: '#fca5a5', + 400: '#f87171', + 500: '#ef4444', + 600: '#dc2626', + 700: '#b91c1c', + 800: '#991b1b', + 900: '#7f1d1d', + 950: '#450a0a', }, orange: { - '50': 'oklch(98% 0.016 73.684)', - '100': 'oklch(95.4% 0.038 75.164)', - '200': 'oklch(90.1% 0.076 70.697)', - '300': 'oklch(83.7% 0.128 66.29)', - '400': 'oklch(75% 0.183 55.934)', - '500': 'oklch(70.5% 0.213 47.604)', - '600': 'oklch(64.6% 0.222 41.116)', - '700': 'oklch(55.3% 0.195 38.402)', - '800': 'oklch(47% 0.157 37.304)', - '900': 'oklch(40.8% 0.123 38.172)', - '950': 'oklch(26.6% 0.079 36.259)', + 50: '#fff7ed', + 100: '#ffedd5', + 200: '#fed7aa', + 300: '#fdba74', + 400: '#fb923c', + 500: '#f97316', + 600: '#ea580c', + 700: '#c2410c', + 800: '#9a3412', + 900: '#7c2d12', + 950: '#431407', }, amber: { - '50': 'oklch(98.7% 0.022 95.277)', - '100': 'oklch(96.2% 0.059 95.617)', - '200': 'oklch(92.4% 0.12 95.746)', - '300': 'oklch(87.9% 0.169 91.605)', - '400': 'oklch(82.8% 0.189 84.429)', - '500': 'oklch(76.9% 0.188 70.08)', - '600': 'oklch(66.6% 0.179 58.318)', - '700': 'oklch(55.5% 0.163 48.998)', - '800': 'oklch(47.3% 0.137 46.201)', - '900': 'oklch(41.4% 0.112 45.904)', - '950': 'oklch(27.9% 0.077 45.635)', + 50: '#fffbeb', + 100: '#fef3c7', + 200: '#fde68a', + 300: '#fcd34d', + 400: '#fbbf24', + 500: '#f59e0b', + 600: '#d97706', + 700: '#b45309', + 800: '#92400e', + 900: '#78350f', + 950: '#451a03', }, yellow: { - '50': 'oklch(98.7% 0.026 102.212)', - '100': 'oklch(97.3% 0.071 103.193)', - '200': 'oklch(94.5% 0.129 101.54)', - '300': 'oklch(90.5% 0.182 98.111)', - '400': 'oklch(85.2% 0.199 91.936)', - '500': 'oklch(79.5% 0.184 86.047)', - '600': 'oklch(68.1% 0.162 75.834)', - '700': 'oklch(55.4% 0.135 66.442)', - '800': 'oklch(47.6% 0.114 61.907)', - '900': 'oklch(42.1% 0.095 57.708)', - '950': 'oklch(28.6% 0.066 53.813)', + 50: '#fefce8', + 100: '#fef9c3', + 200: '#fef08a', + 300: '#fde047', + 400: '#facc15', + 500: '#eab308', + 600: '#ca8a04', + 700: '#a16207', + 800: '#854d0e', + 900: '#713f12', + 950: '#422006', }, lime: { - '50': 'oklch(98.6% 0.031 120.757)', - '100': 'oklch(96.7% 0.067 122.328)', - '200': 'oklch(93.8% 0.127 124.321)', - '300': 'oklch(89.7% 0.196 126.665)', - '400': 'oklch(84.1% 0.238 128.85)', - '500': 'oklch(76.8% 0.233 130.85)', - '600': 'oklch(64.8% 0.2 131.684)', - '700': 'oklch(53.2% 0.157 131.589)', - '800': 'oklch(45.3% 0.124 130.933)', - '900': 'oklch(40.5% 0.101 131.063)', - '950': 'oklch(27.4% 0.072 132.109)', + 50: '#f7fee7', + 100: '#ecfccb', + 200: '#d9f99d', + 300: '#bef264', + 400: '#a3e635', + 500: '#84cc16', + 600: '#65a30d', + 700: '#4d7c0f', + 800: '#3f6212', + 900: '#365314', + 950: '#1a2e05', }, green: { - '50': 'oklch(98.2% 0.018 155.826)', - '100': 'oklch(96.2% 0.044 156.743)', - '200': 'oklch(92.5% 0.084 155.995)', - '300': 'oklch(87.1% 0.15 154.449)', - '400': 'oklch(79.2% 0.209 151.711)', - '500': 'oklch(72.3% 0.219 149.579)', - '600': 'oklch(62.7% 0.194 149.214)', - '700': 'oklch(52.7% 0.154 150.069)', - '800': 'oklch(44.8% 0.119 151.328)', - '900': 'oklch(39.3% 0.095 152.535)', - '950': 'oklch(26.6% 0.065 152.934)', + 50: '#f0fdf4', + 100: '#dcfce7', + 200: '#bbf7d0', + 300: '#86efac', + 400: '#4ade80', + 500: '#22c55e', + 600: '#16a34a', + 700: '#15803d', + 800: '#166534', + 900: '#14532d', + 950: '#052e16', }, emerald: { - '50': 'oklch(97.9% 0.021 166.113)', - '100': 'oklch(95% 0.052 163.051)', - '200': 'oklch(90.5% 0.093 164.15)', - '300': 'oklch(84.5% 0.143 164.978)', - '400': 'oklch(76.5% 0.177 163.223)', - '500': 'oklch(69.6% 0.17 162.48)', - '600': 'oklch(59.6% 0.145 163.225)', - '700': 'oklch(50.8% 0.118 165.612)', - '800': 'oklch(43.2% 0.095 166.913)', - '900': 'oklch(37.8% 0.077 168.94)', - '950': 'oklch(26.2% 0.051 172.552)', + 50: '#ecfdf5', + 100: '#d1fae5', + 200: '#a7f3d0', + 300: '#6ee7b7', + 400: '#34d399', + 500: '#10b981', + 600: '#059669', + 700: '#047857', + 800: '#065f46', + 900: '#064e3b', + 950: '#022c22', }, teal: { - '50': 'oklch(98.4% 0.014 180.72)', - '100': 'oklch(95.3% 0.051 180.801)', - '200': 'oklch(91% 0.096 180.426)', - '300': 'oklch(85.5% 0.138 181.071)', - '400': 'oklch(77.7% 0.152 181.912)', - '500': 'oklch(70.4% 0.14 182.503)', - '600': 'oklch(60% 0.118 184.704)', - '700': 'oklch(51.1% 0.096 186.391)', - '800': 'oklch(43.7% 0.078 188.216)', - '900': 'oklch(38.6% 0.063 188.416)', - '950': 'oklch(27.7% 0.046 192.524)', + 50: '#f0fdfa', + 100: '#ccfbf1', + 200: '#99f6e4', + 300: '#5eead4', + 400: '#2dd4bf', + 500: '#14b8a6', + 600: '#0d9488', + 700: '#0f766e', + 800: '#115e59', + 900: '#134e4a', + 950: '#042f2e', }, cyan: { - '50': 'oklch(98.4% 0.019 200.873)', - '100': 'oklch(95.6% 0.045 203.388)', - '200': 'oklch(91.7% 0.08 205.041)', - '300': 'oklch(86.5% 0.127 207.078)', - '400': 'oklch(78.9% 0.154 211.53)', - '500': 'oklch(71.5% 0.143 215.221)', - '600': 'oklch(60.9% 0.126 221.723)', - '700': 'oklch(52% 0.105 223.128)', - '800': 'oklch(45% 0.085 224.283)', - '900': 'oklch(39.8% 0.07 227.392)', - '950': 'oklch(30.2% 0.056 229.695)', + 50: '#ecfeff', + 100: '#cffafe', + 200: '#a5f3fc', + 300: '#67e8f9', + 400: '#22d3ee', + 500: '#06b6d4', + 600: '#0891b2', + 700: '#0e7490', + 800: '#155e75', + 900: '#164e63', + 950: '#083344', }, sky: { - '50': 'oklch(97.7% 0.013 236.62)', - '100': 'oklch(95.1% 0.026 236.824)', - '200': 'oklch(90.1% 0.058 230.902)', - '300': 'oklch(82.8% 0.111 230.318)', - '400': 'oklch(74.6% 0.16 232.661)', - '500': 'oklch(68.5% 0.169 237.323)', - '600': 'oklch(58.8% 0.158 241.966)', - '700': 'oklch(50% 0.134 242.749)', - '800': 'oklch(44.3% 0.11 240.79)', - '900': 'oklch(39.1% 0.09 240.876)', - '950': 'oklch(29.3% 0.066 243.157)', + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + 950: '#082f49', }, blue: { - '50': 'oklch(97% 0.014 254.604)', - '100': 'oklch(93.2% 0.032 255.585)', - '200': 'oklch(88.2% 0.059 254.128)', - '300': 'oklch(80.9% 0.105 251.813)', - '400': 'oklch(70.7% 0.165 254.624)', - '500': 'oklch(62.3% 0.214 259.815)', - '600': 'oklch(54.6% 0.245 262.881)', - '700': 'oklch(48.8% 0.243 264.376)', - '800': 'oklch(42.4% 0.199 265.638)', - '900': 'oklch(37.9% 0.146 265.522)', - '950': 'oklch(28.2% 0.091 267.935)', + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + 950: '#172554', }, indigo: { - '50': 'oklch(96.2% 0.018 272.314)', - '100': 'oklch(93% 0.034 272.788)', - '200': 'oklch(87% 0.065 274.039)', - '300': 'oklch(78.5% 0.115 274.713)', - '400': 'oklch(67.3% 0.182 276.935)', - '500': 'oklch(58.5% 0.233 277.117)', - '600': 'oklch(51.1% 0.262 276.966)', - '700': 'oklch(45.7% 0.24 277.023)', - '800': 'oklch(39.8% 0.195 277.366)', - '900': 'oklch(35.9% 0.144 278.697)', - '950': 'oklch(25.7% 0.09 281.288)', + 50: '#eef2ff', + 100: '#e0e7ff', + 200: '#c7d2fe', + 300: '#a5b4fc', + 400: '#818cf8', + 500: '#6366f1', + 600: '#4f46e5', + 700: '#4338ca', + 800: '#3730a3', + 900: '#312e81', + 950: '#1e1b4b', }, violet: { - '50': 'oklch(96.9% 0.016 293.756)', - '100': 'oklch(94.3% 0.029 294.588)', - '200': 'oklch(89.4% 0.057 293.283)', - '300': 'oklch(81.1% 0.111 293.571)', - '400': 'oklch(70.2% 0.183 293.541)', - '500': 'oklch(60.6% 0.25 292.717)', - '600': 'oklch(54.1% 0.281 293.009)', - '700': 'oklch(49.1% 0.27 292.581)', - '800': 'oklch(43.2% 0.232 292.759)', - '900': 'oklch(38% 0.189 293.745)', - '950': 'oklch(28.3% 0.141 291.089)', + 50: '#f5f3ff', + 100: '#ede9fe', + 200: '#ddd6fe', + 300: '#c4b5fd', + 400: '#a78bfa', + 500: '#8b5cf6', + 600: '#7c3aed', + 700: '#6d28d9', + 800: '#5b21b6', + 900: '#4c1d95', + 950: '#2e1065', }, purple: { - '50': 'oklch(97.7% 0.014 308.299)', - '100': 'oklch(94.6% 0.033 307.174)', - '200': 'oklch(90.2% 0.063 306.703)', - '300': 'oklch(82.7% 0.119 306.383)', - '400': 'oklch(71.4% 0.203 305.504)', - '500': 'oklch(62.7% 0.265 303.9)', - '600': 'oklch(55.8% 0.288 302.321)', - '700': 'oklch(49.6% 0.265 301.924)', - '800': 'oklch(43.8% 0.218 303.724)', - '900': 'oklch(38.1% 0.176 304.987)', - '950': 'oklch(29.1% 0.149 302.717)', + 50: '#faf5ff', + 100: '#f3e8ff', + 200: '#e9d5ff', + 300: '#d8b4fe', + 400: '#c084fc', + 500: '#a855f7', + 600: '#9333ea', + 700: '#7e22ce', + 800: '#6b21a8', + 900: '#581c87', + 950: '#3b0764', }, fuchsia: { - '50': 'oklch(97.7% 0.017 320.058)', - '100': 'oklch(95.2% 0.037 318.852)', - '200': 'oklch(90.3% 0.076 319.62)', - '300': 'oklch(83.3% 0.145 321.434)', - '400': 'oklch(74% 0.238 322.16)', - '500': 'oklch(66.7% 0.295 322.15)', - '600': 'oklch(59.1% 0.293 322.896)', - '700': 'oklch(51.8% 0.253 323.949)', - '800': 'oklch(45.2% 0.211 324.591)', - '900': 'oklch(40.1% 0.17 325.612)', - '950': 'oklch(29.3% 0.136 325.661)', + 50: '#fdf4ff', + 100: '#fae8ff', + 200: '#f5d0fe', + 300: '#f0abfc', + 400: '#e879f9', + 500: '#d946ef', + 600: '#c026d3', + 700: '#a21caf', + 800: '#86198f', + 900: '#701a75', + 950: '#4a044e', }, pink: { - '50': 'oklch(97.1% 0.014 343.198)', - '100': 'oklch(94.8% 0.028 342.258)', - '200': 'oklch(89.9% 0.061 343.231)', - '300': 'oklch(82.3% 0.12 346.018)', - '400': 'oklch(71.8% 0.202 349.761)', - '500': 'oklch(65.6% 0.241 354.308)', - '600': 'oklch(59.2% 0.249 0.584)', - '700': 'oklch(52.5% 0.223 3.958)', - '800': 'oklch(45.9% 0.187 3.815)', - '900': 'oklch(40.8% 0.153 2.432)', - '950': 'oklch(28.4% 0.109 3.907)', + 50: '#fdf2f8', + 100: '#fce7f3', + 200: '#fbcfe8', + 300: '#f9a8d4', + 400: '#f472b6', + 500: '#ec4899', + 600: '#db2777', + 700: '#be185d', + 800: '#9d174d', + 900: '#831843', + 950: '#500724', }, rose: { - '50': 'oklch(96.9% 0.015 12.422)', - '100': 'oklch(94.1% 0.03 12.58)', - '200': 'oklch(89.2% 0.058 10.001)', - '300': 'oklch(81% 0.117 11.638)', - '400': 'oklch(71.2% 0.194 13.428)', - '500': 'oklch(64.5% 0.246 16.439)', - '600': 'oklch(58.6% 0.253 17.585)', - '700': 'oklch(51.4% 0.222 16.935)', - '800': 'oklch(45.5% 0.188 13.697)', - '900': 'oklch(41% 0.159 10.272)', - '950': 'oklch(27.1% 0.105 12.094)', + 50: '#fff1f2', + 100: '#ffe4e6', + 200: '#fecdd3', + 300: '#fda4af', + 400: '#fb7185', + 500: '#f43f5e', + 600: '#e11d48', + 700: '#be123c', + 800: '#9f1239', + 900: '#881337', + 950: '#4c0519', }, } diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 1f69ae446..ab72ab653 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1,8 +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' +import { flattenColorPalette } from './flatten-color-palette' const css = String.raw @@ -13,28 +12,25 @@ test('Config files can add content', async () => { ` let compiler = await compile(input, { - loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root', path: '' }), + loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root' }), }) - expect(compiler.sources).toEqual([{ base: '/root', pattern: './file.txt', negated: false }]) + expect(compiler.globs).toEqual([{ base: '/root', pattern: './file.txt' }]) }) test('Config files can change dark mode (media)', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root', path: '' }), - }, - ), - ).toMatchInlineSnapshot(` - " - @media (prefers-color-scheme: dark) { - .dark\\:underline { + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root' }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + @media (prefers-color-scheme: dark) { text-decoration-line: underline; } } @@ -43,80 +39,73 @@ test('Config files can change dark mode (media)', async () => { }) test('Config files can change dark mode (selector)', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root', path: '' }), - }, - ), - ).toMatchInlineSnapshot(` - " - .dark\\:underline:where(.dark, .dark *) { - text-decoration-line: underline; + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root' }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + &:where(.dark, .dark *) { + text-decoration-line: underline; + } } " `) }) test('Config files can change dark mode (variant)', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { darkMode: ['variant', '&:where(:not(.light))'] }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .dark\\:underline:where(:not(.light)) { - text-decoration-line: underline; + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { darkMode: ['variant', '&:where(:not(.light))'] }, + base: '/root', + }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + &:where(:not(.light)) { + text-decoration-line: underline; + } } " `) }) test('Config files can add plugins', async () => { - expect( - await run( - ['no-scrollbar'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - plugins: [ - plugin(function ({ addUtilities }) { - addUtilities({ - '.no-scrollbar': { - 'scrollbar-width': 'none', - }, - }) - }), - ], - }, - base: '/root', - path: '', - }), + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + plugins: [ + plugin(function ({ addUtilities }) { + addUtilities({ + '.no-scrollbar': { + 'scrollbar-width': 'none', + }, + }) + }), + ], }, - ), - ).toMatchInlineSnapshot(` - " - .no-scrollbar { + base: '/root', + }), + }) + + expect(compiler.build(['no-scrollbar'])).toMatchInlineSnapshot(` + ".no-scrollbar { scrollbar-width: none; } " @@ -124,181 +113,137 @@ test('Config files can add plugins', async () => { }) test('Plugins loaded from config files can contribute to the config', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - plugins: [ - plugin(() => {}, { - darkMode: ['variant', '&:where(:not(.light))'], - }), - ], - }, - base: '/root', - path: '', - }), + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + plugins: [ + plugin(() => {}, { + darkMode: ['variant', '&:where(:not(.light))'], + }), + ], }, - ), - ).toMatchInlineSnapshot(` - " - .dark\\:underline:where(:not(.light)) { - text-decoration-line: underline; + base: '/root', + }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + &:where(:not(.light)) { + text-decoration-line: underline; + } } " `) }) test('Config file presets can contribute to the config', async () => { - expect( - await run( - ['dark:underline'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { presets: [{ darkMode: ['variant', '&:where(:not(.light))'] }] }, - base: '/root', - path: '', - }), + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + presets: [ + { + darkMode: ['variant', '&:where(:not(.light))'], + }, + ], }, - ), - ).toMatchInlineSnapshot(` - " - .dark\\:underline:where(:not(.light)) { - text-decoration-line: underline; + base: '/root', + }), + }) + + expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` + ".dark\\:underline { + &:where(:not(.light)) { + text-decoration-line: underline; + } } " `) }) test('Config files can affect the theme', async () => { - expect( - await run( - ['bg-primary', 'scrollbar-primary'], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - colors: { - primary: '#c0ffee', - }, - }, + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + 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'), - }, - }) - }), - ], }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .scrollbar-primary { - scrollbar-color: #c0ffee; - } + }, - .bg-primary { + plugins: [ + plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.scrollbar-primary': { + scrollbarColor: theme('colors.primary'), + }, + }) + }), + ], + }, + base: '/root', + }), + }) + + expect(compiler.build(['bg-primary', 'scrollbar-primary'])).toMatchInlineSnapshot(` + ".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); + .scrollbar-primary { + scrollbar-color: #c0ffee; } " `) }) test('Variants in CSS overwrite variants from plugins', async () => { - expect( - await run( - ['dark:underline', 'light:underline'], - css` - @tailwind utilities; - @config "./config.js"; - @custom-variant dark (&:is(.my-dark)); - @custom-variant light (&:is(.my-light)); - `, - { - loadModule: async () => ({ - module: { - darkMode: ['variant', '&:is(.dark)'], - plugins: [ - plugin(function ({ addVariant }) { - addVariant('light', '&:is(.light)') - }), - ], - }, - base: '/root', - path: '', - }), + let input = css` + @tailwind utilities; + @config "./config.js"; + @variant dark (&:is(.my-dark)); + @variant light (&:is(.my-light)); + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + darkMode: ['variant', '&:is(.dark)'], + plugins: [ + plugin(function ({ addVariant }) { + addVariant('light', '&:is(.light)') + }), + ], }, - ), - ).toMatchInlineSnapshot(` - " - .dark\\:underline.my-dark, .light\\:underline.my-light { - text-decoration-line: underline; + 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; + } } " `) @@ -308,167 +253,157 @@ describe('theme callbacks', () => { test('tuple values from the config overwrite `@theme default` tuple-ish values from the CSS theme', async ({ expect, }) => { - expect( - await run( - ['leading-base', 'leading-md', 'leading-xl', 'prose'], - css` - @theme default { - --text-base: 0rem; - --text-base--line-height: 1rem; - --text-md: 0rem; - --text-md--line-height: 1rem; - --text-xl: 0rem; - --text-xl--line-height: 1rem; - } - @theme { - --text-base: 100rem; - --text-md--line-height: 101rem; - } - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - fontSize: { - base: ['200rem', { lineHeight: '201rem' }], - md: ['200rem', { lineHeight: '201rem' }], - xl: ['200rem', { lineHeight: '201rem' }], - }, + let input = css` + @theme default { + --font-size-base: 0rem; + --font-size-base--line-height: 1rem; + --font-size-md: 0rem; + --font-size-md--line-height: 1rem; + --font-size-xl: 0rem; + --font-size-xl--line-height: 1rem; + } + @theme { + --font-size-base: 100rem; + --font-size-md--line-height: 101rem; + } + @tailwind utilities; + @config "./config.js"; + ` - // Direct access - lineHeight: ({ theme }) => ({ - base: theme('fontSize.base[1].lineHeight'), - md: theme('fontSize.md[1].lineHeight'), - xl: theme('fontSize.xl[1].lineHeight'), - }), - - // Tuple access - typography: ({ theme }) => ({ - '[class~=lead-base]': { - fontSize: theme('fontSize.base')[0], - ...theme('fontSize.base')[1], - }, - '[class~=lead-md]': { - fontSize: theme('fontSize.md')[0], - ...theme('fontSize.md')[1], - }, - '[class~=lead-xl]': { - fontSize: theme('fontSize.xl')[0], - ...theme('fontSize.xl')[1], - }, - }), - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + fontSize: { + base: ['200rem', { lineHeight: '201rem' }], + md: ['200rem', { lineHeight: '201rem' }], + xl: ['200rem', { lineHeight: '201rem' }], }, - plugins: [ - plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.prose': { - ...theme('typography'), - }, - }) - }), - ], - } satisfies Config, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { - *, :before, :after, ::backdrop { - --tw-leading: initial; + // Direct access + lineHeight: ({ theme }) => ({ + base: theme('fontSize.base[1].lineHeight'), + md: theme('fontSize.md[1].lineHeight'), + xl: theme('fontSize.xl[1].lineHeight'), + }), + + // Tuple access + typography: ({ theme }) => ({ + '[class~=lead-base]': { + fontSize: theme('fontSize.base')[0], + ...theme('fontSize.base')[1], + }, + '[class~=lead-md]': { + fontSize: theme('fontSize.md')[0], + ...theme('fontSize.md')[1], + }, + '[class~=lead-xl]': { + fontSize: theme('fontSize.xl')[0], + ...theme('fontSize.xl')[1], + }, + }), + }, + }, + + plugins: [ + plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.prose': { + ...theme('typography'), + }, + }) + }), + ], + } satisfies Config, + base: '/root', + }), + }) + + expect(compiler.build(['leading-base', 'leading-md', 'leading-xl', 'prose'])) + .toMatchInlineSnapshot(` + ":root { + --font-size-base: 100rem; + --font-size-md--line-height: 101rem; + } + .prose { + [class~=lead-base] { + font-size: 100rem; + line-height: 201rem; + } + [class~=lead-md] { + font-size: 200rem; + line-height: 101rem; + } + [class~=lead-xl] { + font-size: 200rem; + line-height: 201rem; } } - } - - .prose [class~="lead-base"] { - font-size: 100rem; - line-height: 201rem; - } - - .prose [class~="lead-md"] { - font-size: 200rem; - line-height: 101rem; - } - - .prose [class~="lead-xl"] { - font-size: 200rem; - line-height: 201rem; - } - - .leading-base { - --tw-leading: 201rem; - line-height: 201rem; - } - - .leading-md { - --tw-leading: 101rem; - line-height: 101rem; - } - - .leading-xl { - --tw-leading: 201rem; - line-height: 201rem; - } - - @property --tw-leading { - syntax: "*"; - inherits: false - } - " - `) + .leading-base { + --tw-leading: 201rem; + line-height: 201rem; + } + .leading-md { + --tw-leading: 101rem; + line-height: 101rem; + } + .leading-xl { + --tw-leading: 201rem; + line-height: 201rem; + } + @supports (-moz-orient: inline) { + @layer base { + *, ::before, ::after, ::backdrop { + --tw-leading: initial; + } + } + } + @property --tw-leading { + syntax: "*"; + inherits: false; + } + " + `) }) }) describe('theme overrides order', () => { test('user theme > js config > default theme', async () => { - expect( - await run( - ['bg-red', 'bg-blue'], - css` - @theme default { - --color-red: red; - } - @theme { - --color-blue: blue; - } - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - colors: { - red: 'very-red', - blue: 'very-blue', - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { + let input = css` + @theme default { + --color-red: red; + } + @theme { --color-blue: blue; } + @tailwind utilities; + @config "./config.js"; + ` - .bg-blue { - background-color: var(--color-blue); + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + red: 'very-red', + blue: 'very-blue', + }, + }, + }, + }, + base: '/root', + }), + }) + + expect(compiler.build(['bg-red', 'bg-blue'])).toMatchInlineSnapshot(` + ":root { + --color-blue: blue; + } + .bg-blue { + background-color: var(--color-blue, blue); } - .bg-red { background-color: very-red; } @@ -477,134 +412,131 @@ describe('theme overrides order', () => { }) test('user theme > js config > default theme (with nested object)', async () => { - expect( - await run( - [ - 'bg-slate-100', - 'bg-slate-200', - 'bg-slate-300', - 'bg-slate-400', - 'bg-slate-500', - 'bg-slate-600', - 'hover-bg-slate-100', - 'hover-bg-slate-200', - 'hover-bg-slate-300', - 'hover-bg-slate-400', - 'hover-bg-slate-500', - 'hover-bg-slate-600', - ], - css` - @theme default { - --color-slate-100: #000100; - --color-slate-200: #000200; - --color-slate-300: #000300; - } - @theme { - --color-slate-400: #100400; - --color-slate-500: #100500; - } - @tailwind utilities; - @config "./config.js"; - @plugin "./plugin.js"; - `, - { - loadModule: async (id) => { - if (id.includes('config.js')) { - return { - module: { - theme: { - extend: { - colors: { - slate: { - 200: '#200200', - 400: '#200400', - 600: '#200600', - }, - }, + let input = css` + @theme default { + --color-slate-100: #000100; + --color-slate-200: #000200; + --color-slate-300: #000300; + } + @theme { + --color-slate-400: #100400; + --color-slate-500: #100500; + } + @tailwind utilities; + @config "./config.js"; + @plugin "./plugin.js"; + ` + + let compiler = await compile(input, { + loadModule: async (id) => { + if (id.includes('config.js')) { + return { + module: { + theme: { + extend: { + colors: { + slate: { + 200: '#200200', + 400: '#200400', + 600: '#200600', }, }, - } satisfies Config, - base: '/root', - path: '', - } - } else { - return { - module: plugin(({ matchUtilities, theme }) => { - matchUtilities( - { - 'hover-bg': (value) => { - return { - '&:hover': { - backgroundColor: value, - }, - } + }, + }, + } satisfies Config, + base: '/root', + } + } else { + return { + module: plugin(({ matchUtilities, theme }) => { + matchUtilities( + { + 'hover-bg': (value) => { + return { + '&:hover': { + backgroundColor: value, }, - }, - { values: flattenColorPalette(theme('colors')) }, - ) - }), - base: '/root', - path: '', - } - } - }, - }, - ), + } + }, + }, + { values: flattenColorPalette(theme('colors')) }, + ) + }), + base: '/root', + } + } + }, + }) + + expect( + compiler.build([ + 'bg-slate-100', + 'bg-slate-200', + 'bg-slate-300', + 'bg-slate-400', + 'bg-slate-500', + 'bg-slate-600', + 'hover-bg-slate-100', + 'hover-bg-slate-200', + 'hover-bg-slate-300', + 'hover-bg-slate-400', + 'hover-bg-slate-500', + 'hover-bg-slate-600', + ]), ).toMatchInlineSnapshot(` - " - :root, :host { + ":root { --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); + background-color: var(--color-slate-100, #000100); } - .bg-slate-200 { background-color: #200200; } - .bg-slate-300 { - background-color: var(--color-slate-300); + background-color: var(--color-slate-300, #000300); } - .bg-slate-400 { - background-color: var(--color-slate-400); + background-color: var(--color-slate-400, #100400); } - .bg-slate-500 { - background-color: var(--color-slate-500); + background-color: var(--color-slate-500, #100500); } - .bg-slate-600 { background-color: #200600; } - - .hover-bg-slate-100:hover { - background-color: #000100; + .hover-bg-slate-100 { + &:hover { + background-color: #000100; + } } - - .hover-bg-slate-200:hover { - background-color: #200200; + .hover-bg-slate-200 { + &:hover { + background-color: #200200; + } } - - .hover-bg-slate-300:hover { - background-color: #000300; + .hover-bg-slate-300 { + &:hover { + background-color: #000300; + } } - - .hover-bg-slate-400:hover { - background-color: #100400; + .hover-bg-slate-400 { + &:hover { + background-color: #100400; + } } - - .hover-bg-slate-500:hover { - background-color: #100500; + .hover-bg-slate-500 { + &:hover { + background-color: #100500; + } } - - .hover-bg-slate-600:hover { - background-color: #200600; + .hover-bg-slate-600 { + &:hover { + background-color: #200600; + } } " `) @@ -613,34 +545,35 @@ describe('theme overrides order', () => { describe('default font family compatibility', () => { test('overriding `fontFamily.sans` sets `--default-font-family`', async () => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: 'Potato Sans', - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: 'Potato Sans', }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Potato Sans; + --default-font-feature-settings: normal; + --default-font-variation-settings: normal; + } .font-sans { font-family: Potato Sans; } @@ -651,37 +584,38 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans[1].fontFeatureSettings` sets `--default-font-feature-settings`', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: ['Potato Sans', { fontFeatureSettings: '"cv06"' }], - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Potato Sans', { fontFeatureSettings: '"cv06"' }], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Potato Sans; + --default-font-feature-settings: "cv06"; + --default-font-variation-settings: normal; + } .font-sans { - font-feature-settings: "cv06"; font-family: Potato Sans; + font-feature-settings: "cv06"; } " `) @@ -690,37 +624,38 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans[1].fontVariationSettings` sets `--default-font-variation-settings`', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: ['Potato Sans', { fontVariationSettings: '"XHGT" 0.7' }], - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Potato Sans', { fontVariationSettings: '"XHGT" 0.7' }], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Potato Sans; + --default-font-feature-settings: normal; + --default-font-variation-settings: "XHGT" 0.7; + } .font-sans { - font-variation-settings: "XHGT" .7; font-family: Potato Sans; + font-variation-settings: "XHGT" 0.7; } " `) @@ -729,41 +664,42 @@ describe('default font family compatibility', () => { test('overriding `fontFeatureSettings` and `fontVariationSettings` for `fontFamily.sans` sets `--default-font-feature-settings` and `--default-font-variation-settings`', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: [ - 'Potato Sans', - { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, - ], - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: [ + 'Potato Sans', + { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, + ], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Potato Sans; + --default-font-feature-settings: "cv06"; + --default-font-variation-settings: "XHGT" 0.7; + } .font-sans { - font-feature-settings: "cv06"; - font-variation-settings: "XHGT" .7; font-family: Potato Sans; + font-feature-settings: "cv06"; + font-variation-settings: "XHGT" 0.7; } " `) @@ -772,43 +708,41 @@ describe('default font family compatibility', () => { test('overriding `--font-family-sans` in `@theme` without `default` preserves the original `--default-font-*` values', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @theme { - --font-sans: Sandwich Sans; - } - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: 'Potato Sans', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --font-sans: Sandwich Sans; + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); } + @config "./config.js"; + @theme { + --font-family-sans: Sandwich Sans; + } + @tailwind utilities; + ` + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: 'Potato Sans', + }, + }, + }, + base: '/root', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --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); + --font-family-sans: Sandwich Sans; + } .font-sans { - font-family: var(--font-sans); + font-family: var(--font-family-sans, Sandwich Sans); } " `) @@ -817,34 +751,35 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans` in a config file with an array sets `--default-font-family`', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'], - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Inter, system-ui, sans-serif; + --default-font-feature-settings: normal; + --default-font-variation-settings: normal; + } .font-sans { font-family: Inter, system-ui, sans-serif; } @@ -855,66 +790,69 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.sans` in a config file with an unexpected type is ignored', async ({ expect, }) => { - expect( - await run( - ['font-sans'], - css` - @theme default { - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - sans: { foo: 'bar', banana: 'sandwich' }, - }, - }, + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: { foo: 'bar', banana: 'sandwich' }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toEqual('') + base: '/root', + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --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); + } + " + `) }) test('overriding `fontFamily.mono` sets `--default-mono-font-family`', async () => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-family-mono); - --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); - --default-mono-font-variation-settings: var( - --font-family-mono--font-variation-settings - ); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: 'Potato Mono', - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: 'Potato Mono', }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: Potato Mono; + --default-mono-font-feature-settings: normal; + --default-mono-font-variation-settings: normal; + } .font-mono { font-family: Potato Mono; } @@ -925,39 +863,38 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.mono[1].fontFeatureSettings` sets `--default-mono-font-feature-settings`', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-family-mono); - --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); - --default-mono-font-variation-settings: var( - --font-family-mono--font-variation-settings - ); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: ['Potato Mono', { fontFeatureSettings: '"cv06"' }], - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: ['Potato Mono', { fontFeatureSettings: '"cv06"' }], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: Potato Mono; + --default-mono-font-feature-settings: "cv06"; + --default-mono-font-variation-settings: normal; + } .font-mono { - font-feature-settings: "cv06"; font-family: Potato Mono; + font-feature-settings: "cv06"; } " `) @@ -966,39 +903,38 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.mono[1].fontVariationSettings` sets `--default-mono-font-variation-settings`', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-family-mono); - --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); - --default-mono-font-variation-settings: var( - --font-family-mono--font-variation-settings - ); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: ['Potato Mono', { fontVariationSettings: '"XHGT" 0.7' }], - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: ['Potato Mono', { fontVariationSettings: '"XHGT" 0.7' }], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: Potato Mono; + --default-mono-font-feature-settings: normal; + --default-mono-font-variation-settings: "XHGT" 0.7; + } .font-mono { - font-variation-settings: "XHGT" .7; font-family: Potato Mono; + font-variation-settings: "XHGT" 0.7; } " `) @@ -1007,41 +943,42 @@ describe('default font family compatibility', () => { test('overriding `fontFeatureSettings` and `fontVariationSettings` for `fontFamily.mono` sets `--default-mono-font-feature-settings` and `--default-mono-font-variation-settings`', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-mono); - --default-mono-font-feature-settings: var(--font-mono--font-feature-settings); - --default-mono-font-variation-settings: var(--font-mono--font-variation-settings); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: [ - 'Potato Mono', - { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, - ], - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-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"', fontVariationSettings: '"XHGT" 0.7' }, + ], }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " + base: '/root', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: Potato Mono; + --default-mono-font-feature-settings: "cv06"; + --default-mono-font-variation-settings: "XHGT" 0.7; + } .font-mono { - font-feature-settings: "cv06"; - font-variation-settings: "XHGT" .7; font-family: Potato Mono; + font-feature-settings: "cv06"; + font-variation-settings: "XHGT" 0.7; } " `) @@ -1050,43 +987,41 @@ describe('default font family compatibility', () => { test('overriding `--font-family-mono` in `@theme` without `default` preserves the original `--default-mono-font-*` values', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-mono); - --default-mono-font-feature-settings: var(--font-mono--font-feature-settings); - --default-mono-font-variation-settings: var(--font-mono--font-variation-settings); - } - @config "./config.js"; - @theme { - --font-mono: Sandwich Mono; - } - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: 'Potato Mono', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --font-mono: Sandwich Mono; + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); } + @config "./config.js"; + @theme { + --font-family-mono: Sandwich Mono; + } + @tailwind utilities; + ` + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: 'Potato Mono', + }, + }, + }, + base: '/root', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --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); + --font-family-mono: Sandwich Mono; + } .font-mono { - font-family: var(--font-mono); + font-family: var(--font-family-mono, Sandwich Mono); } " `) @@ -1095,117 +1030,122 @@ describe('default font family compatibility', () => { test('overriding `fontFamily.mono` in a config file with an unexpected type is ignored', async ({ expect, }) => { - expect( - await run( - ['font-mono'], - css` - @theme default { - --default-mono-font-family: var(--font-family-mono); - --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); - --default-mono-font-variation-settings: var( - --font-family-mono--font-variation-settings - ); - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - fontFamily: { - mono: { foo: 'bar', banana: 'sandwich' }, - }, - }, + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: { foo: 'bar', banana: 'sandwich' }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toEqual('') + base: '/root', + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --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); + } + " + `) }) }) test('creates variants for `data`, `supports`, and `aria` theme options at the same level as the core utility ', async () => { - expect( - await run( - [ - 'aria-polite:underline', - 'supports-child-combinator:underline', - 'supports-foo:underline', - 'data-checked:underline', + let input = css` + @tailwind utilities; + @config "./config.js"; + ` - // Ensure core variants still work - 'aria-hidden:flex', - 'supports-grid:flex', - 'data-foo:flex', - - // The `print` variant should still be sorted last, even after registering - // the other custom variants. - 'print:flex', - ], - css` - @tailwind utilities; - @config "./config.js"; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - aria: { - polite: 'live="polite"', - }, - supports: { - 'child-combinator': 'selector(h2 > p)', - foo: 'bar', - }, - data: { - checked: 'ui~="checked"', - }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + aria: { + polite: 'live="polite"', + }, + supports: { + 'child-combinator': 'selector(h2 > p)', + foo: 'bar', + }, + data: { + checked: 'ui~="checked"', }, }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + }), + }) + + expect( + compiler.build([ + 'aria-polite:underline', + 'supports-child-combinator:underline', + 'supports-foo:underline', + 'data-checked:underline', + + // Ensure core variants still work + 'aria-hidden:flex', + 'supports-grid:flex', + 'data-foo:flex', + + // The `print` variant should still be sorted last, even after registering + // the other custom variants. + 'print:flex', + ]), ).toMatchInlineSnapshot(` - " - .aria-hidden\\:flex[aria-hidden="true"] { - display: flex; - } - - .aria-polite\\:underline[aria-live="polite"], .data-checked\\:underline[data-ui~="checked"] { - text-decoration-line: underline; - } - - .data-foo\\:flex[data-foo] { - display: flex; - } - - @supports selector(h2 > p) { - .supports-child-combinator\\:underline { + ".aria-polite\\:underline { + &[aria-live="polite"] { text-decoration-line: underline; } } - - @supports (bar: var(--tw)) { - .supports-foo\\:underline { - text-decoration-line: underline; - } - } - - @supports (grid: var(--tw)) { - .supports-grid\\:flex { + .aria-hidden\\:flex { + &[aria-hidden="true"] { display: flex; } } - - @media print { - .print\\:flex { + .data-checked\\:underline { + &[data-ui~="checked"] { + text-decoration-line: underline; + } + } + .data-foo\\:flex { + &[data-foo] { + display: flex; + } + } + .supports-child-combinator\\:underline { + @supports selector(h2 > p) { + text-decoration-line: underline; + } + } + .supports-foo\\:underline { + @supports (bar: var(--tw)) { + text-decoration-line: underline; + } + } + .supports-grid\\:flex { + @supports (grid: var(--tw)) { + display: flex; + } + } + .print\\:flex { + @media print { display: flex; } } @@ -1214,66 +1154,68 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s }) test('merges css breakpoints with js config screens', async () => { - expect( - await run( - ['sm:flex', 'md:flex', 'lg:flex', 'min-sm:max-md:underline'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @theme { - --breakpoint-md: 50rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: '44rem', - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @theme { + --breakpoint-md: 50rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '44rem', }, }, - base: '/root', - path: '', - }), + }, }, - ), - ).toMatchInlineSnapshot(` - " - @media (min-width: 44rem) { - .sm\\:flex { - display: flex; - } + base: '/root', + }), + }) - @media not all and (min-width: 50rem) { - .min-sm\\:max-md\\:underline { - text-decoration-line: underline; + expect(compiler.build(['sm:flex', 'md:flex', 'lg:flex', 'min-sm:max-md:underline'])) + .toMatchInlineSnapshot(` + ":root { + --breakpoint-md: 50rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + .sm\\:flex { + @media (width >= 44rem) { + display: flex; + } + } + .min-sm\\:max-md\\:underline { + @media (width >= 44rem) { + @media (width < 50rem) { + text-decoration-line: underline; + } } } - } - - @media (min-width: 50rem) { .md\\:flex { - display: flex; + @media (width >= 50rem) { + display: flex; + } } - } - - @media (min-width: 64rem) { .lg\\:flex { - display: flex; + @media (width >= 64rem) { + display: flex; + } } - } - " - `) + " + `) }) test('utilities must be prefixed', async () => { @@ -1286,70 +1228,72 @@ test('utilities must be prefixed', async () => { } ` - let options: Partial[1]> = { - loadModule: async (_id, base) => ({ - path: '', + let compiler = await compile(input, { + loadModule: async (id, base) => ({ base, module: { prefix: 'tw' }, }), - } + }) // Prefixed utilities are generated - expect(await run(['tw:underline', 'tw:hover:line-through', 'tw:custom'], input, options)) + expect(compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom'])) .toMatchInlineSnapshot(` - " - .tw\\:custom { - color: red; - } - - .tw\\:underline { - text-decoration-line: underline; - } - - @media (hover: hover) { - .tw\\:hover\\:line-through:hover { + ".tw\\:custom { + color: red; + } + .tw\\:underline { + text-decoration-line: underline; + } + .tw\\:hover\\:line-through { + &:hover { + @media (hover: hover) { text-decoration-line: line-through; } } - " - `) + } + " + `) // Non-prefixed utilities are ignored - expect(await run(['underline', 'hover:line-through', 'custom'], input, options)).toEqual('') + compiler = await compile(input, { + loadModule: async (id, base) => ({ + base, + module: { prefix: 'tw' }, + }), + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('') }) test('utilities used in @apply must be prefixed', async () => { - // Prefixed utilities are generated - expect( - await compileCss( - css` - @config "./config.js"; + let compiler = await compile( + css` + @config "./config.js"; - .my-underline { - @apply tw:underline; - } - `, - { - loadModule: async (_id, base) => ({ - path: '', - base, - module: { prefix: 'tw' }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .my-underline { + .my-underline { + @apply tw:underline; + } + `, + { + loadModule: async (id, base) => ({ + base, + module: { prefix: 'tw' }, + }), + }, + ) + + // Prefixed utilities are generated + expect(compiler.build([])).toMatchInlineSnapshot(` + ".my-underline { text-decoration-line: underline; } " `) // Non-prefixed utilities cause an error - await expect( + expect(() => compile( css` - @tailwind utilities; @config "./config.js"; .my-underline { @@ -1357,49 +1301,50 @@ test('utilities used in @apply must be prefixed', async () => { } `, { - loadModule: async (_id, base) => ({ - path: '', + loadModule: async (id, base) => ({ base, module: { prefix: 'tw' }, }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Cannot apply unprefixed utility class \`underline\`. Did you mean \`tw:underline\`?]`, + `[Error: Cannot apply unknown utility class: underline]`, ) }) test('Prefixes configured in CSS take precedence over those defined in JS configs', async () => { - expect( - await run( - ['wat:custom'], - css` - @theme prefix(wat) { - --color-red: #f00; - --color-green: #0f0; - --breakpoint-sm: 640px; + let compiler = await compile( + css` + @theme prefix(wat) { + --color-red: #f00; + --color-green: #0f0; + --breakpoint-sm: 640px; + } + + @config "./plugin.js"; + + @tailwind utilities; + + @utility custom { + color: red; + } + `, + { + async loadModule(id, base) { + return { + base, + module: { prefix: 'tw' }, } - - @config "./plugin.js"; - - @tailwind utilities; - - @utility custom { - color: red; - } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: { prefix: 'tw' }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " + }, + ) + + expect(compiler.build(['wat:custom'])).toMatchInlineSnapshot(` + ":root { + --wat-color-red: #f00; + --wat-color-green: #0f0; + --wat-breakpoint-sm: 640px; + } .wat\\:custom { color: red; } @@ -1414,9 +1359,8 @@ test('a prefix must be letters only', async () => { @config "./plugin.js"; `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: { prefix: '__' }, } @@ -1429,38 +1373,36 @@ test('a prefix must be letters only', async () => { }) test('important: `#app`', async () => { - expect( - await run( - ['underline', 'hover:line-through', 'custom'], - css` - @tailwind utilities; - @config "./config.js"; + let input = css` + @tailwind utilities; + @config "./config.js"; - @utility custom { - color: red; - } - `, - { - loadModule: async (_id, base) => ({ - path: '', - base, - module: { important: '#app' }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - #app .custom { + @utility custom { color: red; } + ` - #app .underline { - text-decoration-line: underline; - } + let compiler = await compile(input, { + loadModule: async (_, base) => ({ + base, + module: { important: '#app' }, + }), + }) - @media (hover: hover) { - #app .hover\\:line-through:hover { - text-decoration-line: line-through; + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` + "#app { + .custom { + color: red; + } + .underline { + text-decoration-line: underline; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } } } " @@ -1468,88 +1410,81 @@ test('important: `#app`', async () => { }) test('important: true', async () => { - expect( - await run( - ['underline', 'hover:line-through', 'custom'], - css` - @tailwind utilities; - @config "./config.js"; + let input = css` + @tailwind utilities; + @config "./config.js"; - @utility custom { - color: red; - } - `, - { - loadModule: async (_id, base) => ({ - path: '', - base, - module: { important: true }, - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .custom { + @utility custom { + color: red; + } + ` + + let compiler = await compile(input, { + loadModule: async (_, base) => ({ + base, + module: { important: true }, + }), + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` + ".custom { color: red !important; } - .underline { text-decoration-line: underline !important; } - - @media (hover: hover) { - .hover\\:line-through:hover { - text-decoration-line: line-through !important; + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through !important; + } } } " `) }) -test('blocklisted candidates are not generated', async () => { - let input = css` - @theme reference { - --color-white: #fff; - --breakpoint-md: 48rem; - } - @tailwind utilities; - @config "./config.js"; - ` - - let options: Partial[1]> = { - async loadModule(_id, base) { - return { - path: '', - base, - module: { - blocklist: ['bg-white'], - }, +test('blocklisted canddiates are not generated', async () => { + let compiler = await compile( + css` + @theme reference { + --color-white: #fff; + --breakpoint-md: 48rem; } + @tailwind utilities; + @config "./config.js"; + `, + { + async loadModule(id, base) { + return { + base, + module: { + blocklist: ['bg-white'], + }, + } + }, }, - } + ) // bg-white will not get generated - expect(await run(['bg-white'], input, options)).toEqual('') + expect(compiler.build(['bg-white'])).toEqual('') // underline will as will md:bg-white - expect(await run(['underline', 'bg-white', 'md:bg-white'], input, options)) - .toMatchInlineSnapshot(` - " - .underline { - text-decoration-line: underline; + expect(compiler.build(['underline', 'bg-white', 'md:bg-white'])).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline; + } + .md\\:bg-white { + @media (width >= 48rem) { + background-color: var(--color-white, #fff); } - - @media (min-width: 48rem) { - .md\\:bg-white { - background-color: var(--color-white, #fff); - } - } - " - `) + } + " + `) }) -test('blocklisted candidates cannot be used with `@apply`', async () => { - await expect( +test('blocklisted canddiates cannot be used with `@apply`', async () => { + await expect(() => compile( css` @theme reference { @@ -1563,9 +1498,8 @@ test('blocklisted candidates cannot be used with `@apply`', async () => { } `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: { blocklist: ['bg-white'], @@ -1575,12 +1509,12 @@ test('blocklisted candidates cannot be used with `@apply`', async () => { }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( - `[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]`, + `[Error: Cannot apply unknown utility class: bg-white]`, ) }) test('old theme values are merged with their renamed counterparts in the CSS theme', async () => { - using didCallPluginFn = vi.fn() + let didCallPluginFn = vi.fn() await compile( css` @@ -1599,26 +1533,13 @@ test('old theme values are merged with their renamed counterparts in the CSS the --animate-a: 1; --animate-b: 2; - - --aspect-a: 1; - --aspect-b: 2; - - --container-a: 1; - --container-b: 2; - - --tracking-a: 1; - --tracking-b: 2; - - --leading-a: 1; - --leading-b: 2; } @plugin "./plugin.js"; `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: plugin(function ({ theme }) { didCallPluginFn() @@ -1655,14 +1576,6 @@ test('old theme values are merged with their renamed counterparts in the CSS the expect(theme('animation.a')).toEqual('1') expect(theme('animation.b')).toEqual('2') - expect(theme('aspectRatio')).toMatchObject({ - a: '1', - b: '2', - }) - - expect(theme('aspectRatio.a')).toEqual('1') - expect(theme('aspectRatio.b')).toEqual('2') - expect(theme('boxShadow')).toMatchObject({ a: '1', b: '2', @@ -1670,30 +1583,6 @@ test('old theme values are merged with their renamed counterparts in the CSS the expect(theme('boxShadow.a')).toEqual('1') expect(theme('boxShadow.b')).toEqual('2') - - expect(theme('maxWidth')).toMatchObject({ - a: '1', - b: '2', - }) - - expect(theme('maxWidth.a')).toEqual('1') - expect(theme('maxWidth.b')).toEqual('2') - - expect(theme('letterSpacing.a')).toEqual('1') - expect(theme('letterSpacing.b')).toEqual('2') - - expect(theme('letterSpacing')).toMatchObject({ - a: '1', - b: '2', - }) - - expect(theme('lineHeight.a')).toEqual('1') - expect(theme('lineHeight.b')).toEqual('2') - - expect(theme('lineHeight')).toMatchObject({ - a: '1', - b: '2', - }) }), } }, @@ -1702,148 +1591,3 @@ test('old theme values are merged with their renamed counterparts in the CSS the expect(didCallPluginFn).toHaveBeenCalled() }) - -test('handles setting theme keys to null', async () => { - expect( - await run( - ['bg-red-50', 'bg-red-100', 'bg-red-200'], - css` - @theme default { - --color-red-50: oklch(0.971 0.013 17.38); - --color-red-100: oklch(0.936 0.032 17.717); - } - @config "./my-config.js"; - @tailwind utilities; - @theme { - --color-red-100: oklch(0.936 0.032 17.717); - --color-red-200: oklch(0.885 0.062 18.334); - } - `, - { - loadModule: async () => { - return { - module: { - theme: { - extend: { - colors: { - red: null, - }, - }, - }, - }, - base: '/root', - path: '', - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --color-red-50: oklch(97.1% .013 17.38); - --color-red-100: oklch(93.6% .032 17.717); - --color-red-200: oklch(88.5% .062 18.334); - } - - .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 deleted file mode 100644 index aa1b21cb7..000000000 --- a/packages/tailwindcss/src/compat/config/create-compat-config.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { buildDesignSystem } from '../../design-system' -import { Theme } from '../../theme' -import { createCompatConfig } from './create-compat-config' -import { resolveConfig } from './resolve-config' - -function buildCompatConfig(cssValues: Record) { - let theme = new Theme() - for (let [key, value] of Object.entries(cssValues)) { - theme.add(key, value) - } - let design = buildDesignSystem(theme) - - let { resolvedConfig } = resolveConfig(design, [ - { config: createCompatConfig(design.theme), base: '/root', reference: true, src: undefined }, - ]) - - return resolvedConfig -} - -describe('theme namespace lookups', () => { - test('a namespace that only defines a bare value resolves to `{ DEFAULT: … }`, not a string', () => { - // When the CSS theme only defines `--text` (a single bare value), - // `theme('text', {})` used to return the string `'1rem'`. - // - // Spreading that string, like the compat config does, produced char-indexed - // garbage (`{ '0': '1', '1': 'r', … }`) instead of `{ DEFAULT: '1rem' }`. - let config = buildCompatConfig({ '--text': '1rem' }) - - expect(config.theme?.fontSize?.DEFAULT).toBe('1rem') - expect(config.theme?.fontSize).not.toHaveProperty('0') - }) - - test('a bare value is kept alongside suffixed values in the same namespace', () => { - let config = buildCompatConfig({ - '--text': '1rem', - '--text-sm': '0.875rem', - }) - - expect(config.theme?.fontSize?.DEFAULT).toBe('1rem') - expect(config.theme?.fontSize?.sm).toBe('0.875rem') - }) - - test('a naive spread of a theme namespace in a user config is safe', () => { - let theme = new Theme() - theme.add('--text', '1rem') - let design = buildDesignSystem(theme) - - let { resolvedConfig } = resolveConfig(design, [ - { - config: { - theme: { - extend: { - fontSize: ({ theme }: any) => ({ ...theme('text', {}) }), - }, - }, - }, - base: '/root', - reference: true, - src: undefined, - }, - ]) - - expect(resolvedConfig.theme?.fontSize?.DEFAULT).toBe('1rem') - expect(resolvedConfig.theme?.fontSize).not.toHaveProperty('0') - }) - - test('namespace root lookups resolve to an object, key lookups resolve to the value', () => { - let theme = new Theme() - theme.add('--text', '1rem') - theme.add('--animate-spin', 'spin 1s linear infinite') - let design = buildDesignSystem(theme) - - let root: unknown - let rootWithDefault: unknown - let leaf: unknown - - resolveConfig(design, [ - { - config: { - theme: { - extend: { - fontSize: ({ theme }: any) => { - root = theme('text') - rootWithDefault = theme('text', {}) - leaf = theme('animate.spin') - return {} - }, - }, - }, - }, - base: '/root', - reference: true, - src: undefined, - }, - ]) - - // A namespace root lookup always resolves to an object, like in v3, even - // when the namespace only contains a bare value - expect(root).toMatchObject({ DEFAULT: '1rem' }) - expect(rootWithDefault).toMatchObject({ DEFAULT: '1rem' }) - - // Lookups of a specific key resolve to the value itself - expect(leaf).toBe('spin 1s linear infinite') - }) - - test('lookups of a specific key still resolve to the value itself', () => { - let theme = new Theme() - theme.add('--animate-spin', 'spin 1s linear infinite') - let design = buildDesignSystem(theme) - - let { resolvedConfig } = resolveConfig(design, [ - { - config: { - theme: { - extend: { - animation: ({ theme }: any) => ({ spin: theme('animate.spin') }), - }, - }, - }, - base: '/root', - reference: true, - src: undefined, - }, - ]) - - expect(resolvedConfig.theme?.animation?.spin).toBe('spin 1s linear infinite') - }) -}) - -describe('createCompatConfig namespace mappings', () => { - test('fontSize maps from CSS text namespace', () => { - let config = buildCompatConfig({ '--text-base': '1rem' }) - expect(config.theme?.fontSize?.base).toBe('1rem') - }) - - test('boxShadow maps from CSS shadow namespace', () => { - let config = buildCompatConfig({ '--shadow-md': '0 4px 6px #000' }) - expect(config.theme?.boxShadow?.md).toBe('0 4px 6px #000') - }) - - test('animation maps from CSS animate namespace', () => { - let config = buildCompatConfig({ '--animate-spin': 'spin 1s linear infinite' }) - expect(config.theme?.animation?.spin).toBe('spin 1s linear infinite') - }) - - test('aspectRatio maps from CSS aspect namespace', () => { - let config = buildCompatConfig({ '--aspect-square': '1 / 1' }) - expect(config.theme?.aspectRatio?.square).toBe('1 / 1') - }) - - test('borderRadius maps from CSS radius namespace', () => { - let config = buildCompatConfig({ '--radius-lg': '0.5rem' }) - expect(config.theme?.borderRadius?.lg).toBe('0.5rem') - }) - - test('screens maps from CSS breakpoint namespace', () => { - let config = buildCompatConfig({ '--breakpoint-sm': '640px' }) - expect(config.theme?.screens?.sm).toBe('640px') - }) - - test('letterSpacing maps from CSS tracking namespace', () => { - let config = buildCompatConfig({ '--tracking-wide': '0.025em' }) - expect(config.theme?.letterSpacing?.wide).toBe('0.025em') - }) - - test('lineHeight maps from CSS leading namespace', () => { - let config = buildCompatConfig({ '--leading-relaxed': '1.75' }) - expect(config.theme?.lineHeight?.relaxed).toBe('1.75') - }) - - test('maxWidth maps from the container namespace in the default theme', () => { - let config = buildCompatConfig({}) - // The default theme has container.sm = '24rem' - expect(config.theme?.maxWidth?.sm).toBe('24rem') - }) - - test('transitionDuration gets DEFAULT from --default-transition-duration', () => { - let config = buildCompatConfig({ '--default-transition-duration': '150ms' }) - expect(config.theme?.transitionDuration?.DEFAULT).toBe('150ms') - }) - - test('transitionTimingFunction gets DEFAULT from --default-transition-timing-function', () => { - let config = buildCompatConfig({ - '--default-transition-timing-function': 'cubic-bezier(0.4, 0, 0.2, 1)', - }) - expect(config.theme?.transitionTimingFunction?.DEFAULT).toBe('cubic-bezier(0.4, 0, 0.2, 1)') - }) -}) diff --git a/packages/tailwindcss/src/compat/config/create-compat-config.ts b/packages/tailwindcss/src/compat/config/create-compat-config.ts index 39bbec07d..2a06c95ba 100644 --- a/packages/tailwindcss/src/compat/config/create-compat-config.ts +++ b/packages/tailwindcss/src/compat/config/create-compat-config.ts @@ -13,50 +13,34 @@ export function createCompatConfig(cssTheme: Theme): UserConfig { // and only allow colors from the CSS theme. colors: ({ theme }) => theme('color', {}), - extend: { - fontSize: ({ theme }) => ({ - ...theme('text', {}), - }), + boxShadow: ({ theme }) => ({ + ...defaultTheme.boxShadow, + ...theme('shadow', {}), + }), - boxShadow: ({ theme }) => ({ - ...theme('shadow', {}), - }), + animation: ({ theme }) => ({ + ...defaultTheme.animation, + ...theme('animate', {}), + }), - animation: ({ theme }) => ({ - ...theme('animate', {}), - }), + borderRadius: ({ theme }) => ({ + ...defaultTheme.borderRadius, + ...theme('radius', {}), + }), - aspectRatio: ({ theme }) => ({ - ...theme('aspect', {}), - }), + screens: ({ theme }) => ({ + ...defaultTheme.screens, + ...theme('breakpoint', {}), + }), - borderRadius: ({ theme }) => ({ - ...theme('radius', {}), - }), + transitionDuration: { + ...defaultTheme.transitionDuration, + DEFAULT: cssTheme.get(['--default-transition-duration']) ?? null, + }, - screens: ({ theme }) => ({ - ...theme('breakpoint', {}), - }), - - letterSpacing: ({ theme }) => ({ - ...theme('tracking', {}), - }), - - lineHeight: ({ theme }) => ({ - ...theme('leading', {}), - }), - - transitionDuration: { - DEFAULT: cssTheme.get(['--default-transition-duration']) ?? null, - }, - - transitionTimingFunction: { - DEFAULT: cssTheme.get(['--default-transition-timing-function']) ?? null, - }, - - maxWidth: ({ theme }) => ({ - ...theme('container', {}), - }), + transitionTimingFunction: { + ...defaultTheme.transitionTimingFunction, + DEFAULT: cssTheme.get(['--default-transition-timing-function']) ?? null, }, }, } diff --git a/packages/tailwindcss/src/compat/config/deep-merge.ts b/packages/tailwindcss/src/compat/config/deep-merge.ts index 2bf15c9b3..3bbae51dd 100644 --- a/packages/tailwindcss/src/compat/config/deep-merge.ts +++ b/packages/tailwindcss/src/compat/config/deep-merge.ts @@ -11,7 +11,7 @@ export function deepMerge( target: T, sources: (Partial | null | undefined)[], customizer: (a: any, b: any, keypath: (keyof T)[]) => any, - path: (keyof T)[] = [], + parentPath: (keyof T)[] = [], ) { type Key = keyof T type Value = T[Key] @@ -22,17 +22,21 @@ export function deepMerge( } for (let k of Reflect.ownKeys(source) as Key[]) { - path.push(k) - let merged = customizer(target[k], source[k], path) + let currentParentPath = [...parentPath, k] + let merged = customizer(target[k], source[k], currentParentPath) if (merged !== undefined) { target[k] = merged } else if (!isPlainObject(target[k]) || !isPlainObject(source[k])) { target[k] = source[k] as Value } else { - target[k] = deepMerge({}, [target[k], source[k]], customizer, path as any) as Value + target[k] = deepMerge( + {}, + [target[k], source[k]], + customizer, + currentParentPath as any, + ) as Value } - path.pop() } } diff --git a/packages/tailwindcss/src/compat/config/resolve-config.test.ts b/packages/tailwindcss/src/compat/config/resolve-config.test.ts index d8cc21a10..d755cf8e1 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.test.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.test.ts @@ -6,7 +6,7 @@ import { resolveConfig } from './resolve-config' test('top level theme keys are replaced', () => { let design = buildDesignSystem(new Theme()) - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ + let config = resolveConfig(design, [ { config: { theme: { @@ -20,8 +20,6 @@ test('top level theme keys are replaced', () => { }, }, base: '/root', - reference: false, - src: undefined, }, { config: { @@ -32,8 +30,6 @@ test('top level theme keys are replaced', () => { }, }, base: '/root', - reference: false, - src: undefined, }, { config: { @@ -44,12 +40,10 @@ test('top level theme keys are replaced', () => { }, }, base: '/root', - reference: false, - src: undefined, }, ]) - expect(resolvedConfig).toMatchObject({ + expect(config).toMatchObject({ theme: { colors: { blue: 'blue', @@ -59,13 +53,12 @@ test('top level theme keys are replaced', () => { }, }, }) - expect(replacedThemeKeys).toEqual(new Set(['colors', 'fontFamily'])) }) test('theme can be extended', () => { let design = buildDesignSystem(new Theme()) - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ + let config = resolveConfig(design, [ { config: { theme: { @@ -79,8 +72,6 @@ test('theme can be extended', () => { }, }, base: '/root', - reference: false, - src: undefined, }, { config: { @@ -93,12 +84,10 @@ test('theme can be extended', () => { }, }, base: '/root', - reference: false, - src: undefined, }, ]) - expect(resolvedConfig).toMatchObject({ + expect(config).toMatchObject({ theme: { colors: { red: 'red', @@ -109,7 +98,6 @@ test('theme can be extended', () => { }, }, }) - expect(replacedThemeKeys).toEqual(new Set(['colors', 'fontFamily'])) }) test('theme keys can reference other theme keys using the theme function regardless of order', ({ @@ -117,7 +105,7 @@ test('theme keys can reference other theme keys using the theme function regardl }) => { let design = buildDesignSystem(new Theme()) - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ + let config = resolveConfig(design, [ { config: { theme: { @@ -130,8 +118,6 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, base: '/root', - reference: false, - src: undefined, }, { config: { @@ -145,8 +131,6 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, base: '/root', - reference: false, - src: undefined, }, { config: { @@ -159,12 +143,10 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, base: '/root', - reference: false, - src: undefined, }, ]) - expect(resolvedConfig).toMatchObject({ + expect(config).toMatchObject({ theme: { colors: { red: 'red', @@ -188,7 +170,6 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, }) - expect(replacedThemeKeys).toEqual(new Set(['colors', 'placeholderColor'])) }) test('theme keys can read from the CSS theme', () => { @@ -197,7 +178,7 @@ test('theme keys can read from the CSS theme', () => { let design = buildDesignSystem(theme) - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ + let config = resolveConfig(design, [ { config: { theme: { @@ -221,19 +202,13 @@ test('theme keys can read from the CSS theme', () => { // Gives access to the colors object directly primary: colors.green, }), - transitionColor: (theme: any) => ({ - // The parameter object is also the theme function - ...theme('colors'), - }), }, }, base: '/root', - reference: false, - src: undefined, }, ]) - expect(resolvedConfig).toMatchObject({ + expect(config).toMatchObject({ theme: { colors: { red: 'red', @@ -249,73 +224,19 @@ test('theme keys can read from the CSS theme', () => { }, caretColor: { primary: { - '100': 'oklch(96.2% 0.044 156.743)', - '200': 'oklch(92.5% 0.084 155.995)', - '300': 'oklch(87.1% 0.15 154.449)', - '400': 'oklch(79.2% 0.209 151.711)', - '50': 'oklch(98.2% 0.018 155.826)', - '500': 'oklch(72.3% 0.219 149.579)', - '600': 'oklch(62.7% 0.194 149.214)', - '700': 'oklch(52.7% 0.154 150.069)', - '800': 'oklch(44.8% 0.119 151.328)', - '900': 'oklch(39.3% 0.095 152.535)', - '950': 'oklch(26.6% 0.065 152.934)', + '50': '#f0fdf4', + '100': '#dcfce7', + '200': '#bbf7d0', + '300': '#86efac', + '400': '#4ade80', + '500': '#22c55e', + '600': '#16a34a', + '700': '#15803d', + '800': '#166534', + '900': '#14532d', + '950': '#052e16', }, }, - transitionColor: { - red: 'red', - green: 'green', - }, }, }) - expect(replacedThemeKeys).toEqual( - new Set(['colors', 'accentColor', 'placeholderColor', 'caretColor', 'transitionColor']), - ) -}) - -test('handles null as theme values', () => { - let theme = new Theme() - theme.add('--color-red-50', 'red') - theme.add('--color-red-100', 'red') - - let design = buildDesignSystem(theme) - - let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [ - { - config: { - theme: { - colors: ({ theme }) => ({ - // Reads from the --color-* namespace - ...theme('color'), - }), - }, - }, - base: '/root', - reference: false, - src: undefined, - }, - { - config: { - theme: { - extend: { - colors: { - red: null, - }, - }, - }, - }, - base: '/root', - reference: false, - src: undefined, - }, - ]) - - expect(resolvedConfig).toMatchObject({ - theme: { - colors: { - red: null, - }, - }, - }) - expect(replacedThemeKeys).toEqual(new Set(['colors'])) }) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 858f4bbbe..fbbf807ab 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -1,5 +1,4 @@ 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,8 +15,6 @@ export interface ConfigFile { path?: string base: string config: UserConfig - reference: boolean - src: SourceLocation | undefined } interface ResolutionContext { @@ -32,8 +29,6 @@ interface ResolutionContext { let minimal: ResolvedConfig = { blocklist: [], - future: {}, - experimental: {}, prefix: '', important: false, darkMode: null, @@ -44,10 +39,7 @@ let minimal: ResolvedConfig = { }, } -export function resolveConfig( - design: DesignSystem, - files: ConfigFile[], -): { resolvedConfig: ResolvedConfig; replacedThemeKeys: Set } { +export function resolveConfig(design: DesignSystem, files: ConfigFile[]): ResolvedConfig { let ctx: ResolutionContext = { design, configs: [], @@ -86,16 +78,13 @@ export function resolveConfig( } // Merge themes - let replacedThemeKeys = mergeTheme(ctx) + mergeTheme(ctx) return { - resolvedConfig: { - ...ctx.result, - content: ctx.content, - theme: ctx.theme as ResolvedConfig['theme'], - plugins: ctx.plugins, - }, - replacedThemeKeys, + ...ctx.result, + content: ctx.content, + theme: ctx.theme as ResolvedConfig['theme'], + plugins: ctx.plugins, } } @@ -127,15 +116,12 @@ export function mergeThemeExtension( return undefined } -export type PluginUtils = { - theme: (keypath: string, defaultValue?: any) => any +export interface PluginUtils { + theme(keypath: string, defaultValue?: any): any colors: typeof colors } -function extractConfigs( - ctx: ResolutionContext, - { config, base, path, reference, src }: ConfigFile, -): void { +function extractConfigs(ctx: ResolutionContext, { config, base, path }: ConfigFile): void { let plugins: PluginWithConfig[] = [] // Normalize plugins so they share the same shape @@ -143,17 +129,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, src }) + plugins.push(plugin()) } 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, src }) + plugins.push(plugin) } else { // Just a plain function without using the plugin(…) API - plugins.push({ handler: plugin, reference, src }) + plugins.push({ handler: plugin }) } } @@ -165,7 +151,7 @@ function extractConfigs( } for (let preset of config.presets ?? []) { - extractConfigs(ctx, { path, base, config: preset, reference, src }) + extractConfigs(ctx, { path, base, config: preset }) } // Apply configs from plugins @@ -173,13 +159,7 @@ function extractConfigs( ctx.plugins.push(plugin) if (plugin.config) { - extractConfigs(ctx, { - path, - base, - config: plugin.config, - reference: !!plugin.reference, - src: plugin.src ?? src, - }) + extractConfigs(ctx, { path, base, config: plugin.config }) } } @@ -195,18 +175,15 @@ function extractConfigs( ctx.configs.push(config) } -function mergeTheme(ctx: ResolutionContext): Set { - let replacedThemeKeys: Set = new Set() - - let themeFn = createThemeFn(ctx.design, () => ctx.theme, resolveValue) - let theme = Object.assign(themeFn, { - theme: themeFn, +function mergeTheme(ctx: ResolutionContext) { + let api: PluginUtils = { + theme: createThemeFn(ctx.design, () => ctx.theme, resolveValue), colors, - }) + } function resolveValue(value: ThemeValue | null | undefined): ResolvedThemeValue { if (typeof value === 'function') { - return value(theme) ?? null + return value(api) ?? null } return value ?? null @@ -216,14 +193,6 @@ function mergeTheme(ctx: ResolutionContext): Set { let theme = config.theme ?? {} let extend = theme.extend ?? {} - // Keep track of all theme keys that were reset - for (let key in theme) { - if (key === 'extend') { - continue - } - replacedThemeKeys.add(key) - } - // Shallow merge themes so latest "group" wins Object.assign(ctx.theme, theme) @@ -268,6 +237,4 @@ function mergeTheme(ctx: ResolutionContext): Set { ctx.theme.screens[key] = screen.min } } - - return replacedThemeKeys } diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts index 76851a397..313a7dba8 100644 --- a/packages/tailwindcss/src/compat/config/types.ts +++ b/packages/tailwindcss/src/compat/config/types.ts @@ -25,7 +25,7 @@ export interface ResolvedConfig { type ContentFile = string | { raw: string; extension?: string } export interface UserConfig { - content?: ContentFile[] | { relative?: boolean; files: ContentFile[] } + content?: ContentFile[] | { files: ContentFile[] } } type ResolvedContent = { base: string; pattern: string } | { raw: string; extension?: string } @@ -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 predictable behavior + // Use the `selector` strategy — same as `class` but uses `:where()` for more predicable behavior | 'selector' // Use the `selector` strategy with a custom selector instead of `.dark`. @@ -96,21 +96,3 @@ export interface UserConfig { export interface ResolvedConfig { important: boolean | string } - -// `future` key support -export interface UserConfig { - future?: 'all' | Record -} - -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 deleted file mode 100644 index 39a12557c..000000000 --- a/packages/tailwindcss/src/compat/container-config.test.ts +++ /dev/null @@ -1,757 +0,0 @@ -import { expect, test } from 'vitest' -import { run } from '../test-utils/run' - -const css = String.raw - -test('creates a custom utility to extend the built-in container', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - center: true, - padding: '2rem', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } - - @media (min-width: 40rem) { - .container { - max-width: 40rem; - } - } - - @media (min-width: 48rem) { - .container { - max-width: 48rem; - } - } - - @media (min-width: 64rem) { - .container { - max-width: 64rem; - } - } - - @media (min-width: 80rem) { - .container { - max-width: 80rem; - } - } - - @media (min-width: 96rem) { - .container { - max-width: 96rem; - } - } - - .container { - margin-inline: auto; - padding-inline: 2rem; - } - " - `) -}) - -test('allows padding to be defined at custom breakpoints', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - padding: { - // The order here is messed up on purpose - '2xl': '3rem', - DEFAULT: '1rem', - lg: '2rem', - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } - - @media (min-width: 40rem) { - .container { - max-width: 40rem; - } - } - - @media (min-width: 48rem) { - .container { - max-width: 48rem; - } - } - - @media (min-width: 64rem) { - .container { - max-width: 64rem; - } - } - - @media (min-width: 80rem) { - .container { - max-width: 80rem; - } - } - - @media (min-width: 96rem) { - .container { - max-width: 96rem; - } - } - - .container { - padding-inline: 1rem; - } - - @media (min-width: 64rem) { - .container { - padding-inline: 2rem; - } - } - - @media (min-width: 96rem) { - .container { - padding-inline: 3rem; - } - } - " - `) -}) - -test('allows breakpoints to be overwritten', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - screens: { - xl: '1280px', - '2xl': '1536px', - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } - - @media (min-width: 40rem) { - .container { - max-width: 40rem; - } - } - - @media (min-width: 48rem) { - .container { - max-width: 48rem; - } - } - - @media (min-width: 64rem) { - .container { - max-width: 64rem; - } - } - - @media (min-width: 80rem) { - .container { - max-width: 80rem; - } - } - - @media (min-width: 96rem) { - .container { - max-width: 96rem; - } - } - - @media (min-width: 40rem) { - .container { - max-width: none; - } - } - - @media (min-width: 1280px) { - .container { - max-width: 1280px; - } - } - - @media (min-width: 1536px) { - .container { - max-width: 1536px; - } - } - " - `) -}) - -test('padding applies to custom `container` screens', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - padding: { - sm: '2rem', - md: '3rem', - }, - screens: { - md: '48rem', - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } - - @media (min-width: 40rem) { - .container { - max-width: 40rem; - } - } - - @media (min-width: 48rem) { - .container { - max-width: 48rem; - } - } - - @media (min-width: 64rem) { - .container { - max-width: 64rem; - } - } - - @media (min-width: 80rem) { - .container { - max-width: 80rem; - } - } - - @media (min-width: 96rem) { - .container { - max-width: 96rem; - } - } - - @media (min-width: 40rem) { - .container { - max-width: none; - } - } - - @media (min-width: 48rem) { - .container { - max-width: 48rem; - padding-inline: 3rem; - } - } - " - `) -}) - -test("an empty `screen` config will undo all custom media screens and won't apply any breakpoint-specific padding", async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - padding: { - DEFAULT: '1rem', - sm: '2rem', - md: '3rem', - }, - screens: {}, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } - - @media (min-width: 40rem) { - .container { - max-width: 40rem; - } - } - - @media (min-width: 48rem) { - .container { - max-width: 48rem; - } - } - - @media (min-width: 64rem) { - .container { - max-width: 64rem; - } - } - - @media (min-width: 80rem) { - .container { - max-width: 80rem; - } - } - - @media (min-width: 96rem) { - .container { - max-width: 96rem; - } - } - - .container { - padding-inline: 1rem; - } - - @media (min-width: 40rem) { - .container { - max-width: none; - } - } - " - `) -}) - -test('legacy container component does not interfere with new --container variables', async () => { - expect( - await run( - ['max-w-sm'], - css` - @theme default { - --container-3xs: 16rem; - --container-2xs: 18rem; - --container-xs: 20rem; - --container-sm: 24rem; - --container-md: 28rem; - --container-lg: 32rem; - --container-xl: 36rem; - --container-2xl: 42rem; - --container-3xl: 48rem; - --container-4xl: 56rem; - --container-5xl: 64rem; - --container-6xl: 72rem; - --container-7xl: 80rem; - --container-prose: 65ch; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - center: true, - padding: '2rem', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --container-sm: 24rem; - } - - .max-w-sm { - max-width: var(--container-sm); - } - " - `) -}) - -test('combines custom padding and screen overwrites', async () => { - expect( - await run( - ['container', '!container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - center: true, - padding: { - DEFAULT: '2rem', - '2xl': '4rem', - }, - screens: { - md: '48rem', // Matches a default --breakpoint - xl: '1280px', - '2xl': '1536px', - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .\\!container { - width: 100% !important; - } - - @media (min-width: 40rem) { - .\\!container { - max-width: 40rem !important; - } - } - - @media (min-width: 48rem) { - .\\!container { - max-width: 48rem !important; - } - } - - @media (min-width: 64rem) { - .\\!container { - max-width: 64rem !important; - } - } - - @media (min-width: 80rem) { - .\\!container { - max-width: 80rem !important; - } - } - - @media (min-width: 96rem) { - .\\!container { - max-width: 96rem !important; - } - } - - .container { - width: 100%; - } - - @media (min-width: 40rem) { - .container { - max-width: 40rem; - } - } - - @media (min-width: 48rem) { - .container { - max-width: 48rem; - } - } - - @media (min-width: 64rem) { - .container { - max-width: 64rem; - } - } - - @media (min-width: 80rem) { - .container { - max-width: 80rem; - } - } - - @media (min-width: 96rem) { - .container { - max-width: 96rem; - } - } - - .\\!container { - margin-inline: auto !important; - padding-inline: 2rem !important; - } - - @media (min-width: 40rem) { - .\\!container { - max-width: none !important; - } - } - - @media (min-width: 48rem) { - .\\!container { - max-width: 48rem !important; - } - } - - @media (min-width: 1280px) { - .\\!container { - max-width: 1280px !important; - } - } - - @media (min-width: 1536px) { - .\\!container { - max-width: 1536px !important; - padding-inline: 4rem !important; - } - } - - .container { - margin-inline: auto; - padding-inline: 2rem; - } - - @media (min-width: 40rem) { - .container { - max-width: none; - } - } - - @media (min-width: 48rem) { - .container { - max-width: 48rem; - } - } - - @media (min-width: 1280px) { - .container { - max-width: 1280px; - } - } - - @media (min-width: 1536px) { - .container { - max-width: 1536px; - padding-inline: 4rem; - } - } - " - `) -}) - -test('filters out complex breakpoints', async () => { - expect( - await run( - ['container'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - container: { - center: true, - padding: { - DEFAULT: '2rem', - '2xl': '4rem', - }, - screens: { - sm: '20px', - md: { min: '100px' }, - lg: { max: '200px' }, - xl: { min: '300px', max: '400px' }, - '2xl': { raw: 'print' }, - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toMatchInlineSnapshot(` - " - .container { - width: 100%; - } - - @media (min-width: 40rem) { - .container { - max-width: 40rem; - } - } - - @media (min-width: 48rem) { - .container { - max-width: 48rem; - } - } - - @media (min-width: 64rem) { - .container { - max-width: 64rem; - } - } - - @media (min-width: 80rem) { - .container { - max-width: 80rem; - } - } - - @media (min-width: 96rem) { - .container { - max-width: 96rem; - } - } - - .container { - margin-inline: auto; - padding-inline: 2rem; - } - - @media (min-width: 40rem) { - .container { - max-width: none; - } - } - - @media (min-width: 20px) { - .container { - max-width: 20px; - } - } - - @media (min-width: 100px) { - .container { - max-width: 100px; - } - } - - @media (min-width: 300px) { - .container { - max-width: 300px; - } - } - " - `) -}) diff --git a/packages/tailwindcss/src/compat/container.ts b/packages/tailwindcss/src/compat/container.ts deleted file mode 100644 index 98f23ce7a..000000000 --- a/packages/tailwindcss/src/compat/container.ts +++ /dev/null @@ -1,120 +0,0 @@ -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' - -export function registerContainerCompat(userConfig: ResolvedConfig, designSystem: DesignSystem) { - let container = userConfig.theme.container || {} - - if (typeof container !== 'object' || container === null) { - return - } - - let rules = buildCustomContainerUtilityRules(container, designSystem) - - if (rules.length === 0) { - return - } - - designSystem.utilities.static('container', () => rules.map(cloneAstNode)) -} - -export function buildCustomContainerUtilityRules( - { - center, - padding, - screens, - }: { - center?: boolean - padding?: string | {} - screens?: {} - }, - designSystem: DesignSystem, -): AstNode[] { - let rules = [] - let breakpointOverwrites: null | Map = null - - if (center) { - rules.push(decl('margin-inline', 'auto')) - } - - if ( - typeof padding === 'string' || - (typeof padding === 'object' && padding !== null && 'DEFAULT' in padding) - ) { - rules.push( - decl('padding-inline', typeof padding === 'string' ? padding : (padding.DEFAULT as string)), - ) - } - - if (typeof screens === 'object' && screens !== null) { - breakpointOverwrites = new Map() - - // 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. - - let breakpoints = Array.from(designSystem.theme.namespace('--breakpoint').entries()) - breakpoints.sort((a, z) => compareBreakpoints(a[1], z[1], 'asc')) - if (breakpoints.length > 0) { - let [key] = breakpoints[0] - // Unset all default breakpoints - rules.push( - atRule('@media', `(width >= --theme(--breakpoint-${key}))`, [decl('max-width', 'none')]), - ) - } - - for (let [key, value] of Object.entries(screens)) { - if (typeof value === 'object') { - if ('min' in value) { - value = value.min - } else { - continue - } - } - - // We're inlining the breakpoint values because the screens configured in - // the `container` option do not have to match the ones defined in the - // root `screen` setting. - breakpointOverwrites.set( - key, - atRule('@media', `(width >= ${value})`, [decl('max-width', value)]), - ) - } - } - - if (typeof padding === 'object' && padding !== null) { - let breakpoints = Object.entries(padding) - .filter(([key]) => key !== 'DEFAULT') - .map(([key, value]) => { - return [key, designSystem.theme.resolveValue(key, ['--breakpoint']), value] - }) - .filter(Boolean) as [string, string, string][] - breakpoints.sort((a, z) => compareBreakpoints(a[1], z[1], 'asc')) - - for (let [key, , value] of breakpoints) { - if (breakpointOverwrites && breakpointOverwrites.has(key)) { - let overwrite = breakpointOverwrites.get(key)! - overwrite.nodes.push(decl('padding-inline', value)) - } else if (breakpointOverwrites) { - // The breakpoint does not exist in the overwritten breakpoints list, so - // we skip rendering it. - continue - } else { - rules.push( - atRule('@media', `(width >= theme(--breakpoint-${key}))`, [ - decl('padding-inline', value), - ]), - ) - } - } - } - - if (breakpointOverwrites) { - for (let [, rule] of breakpointOverwrites) { - rules.push(rule) - } - } - - return rules -} diff --git a/packages/tailwindcss/src/compat/default-theme.ts b/packages/tailwindcss/src/compat/default-theme.ts index 90e699c1c..85924057e 100644 --- a/packages/tailwindcss/src/compat/default-theme.ts +++ b/packages/tailwindcss/src/compat/default-theme.ts @@ -156,7 +156,7 @@ export default { '3xl': '64px', }, borderColor: ({ theme }) => ({ - DEFAULT: 'currentcolor', + DEFAULT: 'currentColor', ...theme('colors'), }), borderOpacity: ({ theme }) => theme('opacity'), @@ -353,13 +353,8 @@ export default { }, fontFamily: { sans: [ - '-apple-system', - 'BlinkMacSystemFont', - '"Segoe UI"', - 'Roboto', - '"Helvetica Neue"', - '"Noto Sans"', - 'Arial', + 'ui-sans-serif', + 'system-ui', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', @@ -830,7 +825,7 @@ export default { placeholderColor: ({ theme }) => theme('colors'), placeholderOpacity: ({ theme }) => theme('opacity'), ringColor: ({ theme }) => ({ - DEFAULT: 'currentcolor', + DEFAULT: 'currentColor', ...theme('colors'), }), ringOffsetColor: ({ theme }) => theme('colors'), @@ -1024,9 +1019,8 @@ export default { none: 'none', all: 'all', DEFAULT: - 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter', - colors: - 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke', + 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter', + colors: 'color, background-color, border-color, text-decoration-color, fill, stroke', opacity: 'opacity', shadow: 'box-shadow', transform: 'transform', diff --git a/packages/tailwindcss/src/compat/flatten-color-palette.cts b/packages/tailwindcss/src/compat/flatten-color-palette.cts deleted file mode 100644 index d8b4be144..000000000 --- a/packages/tailwindcss/src/compat/flatten-color-palette.cts +++ /dev/null @@ -1,8 +0,0 @@ -import flattenColorPalette from './flatten-color-palette.ts' - -// This file exists so that `flatten-color-palette.ts` can be written one time -// but be compatible with both CJS and ESM. Without it we get a `.default` -// export when using `require` in CJS. - -// @ts-ignore -export = flattenColorPalette diff --git a/packages/tailwindcss/src/compat/flatten-color-palette.test.ts b/packages/tailwindcss/src/compat/flatten-color-palette.test.ts deleted file mode 100644 index 7ddda5bd4..000000000 --- a/packages/tailwindcss/src/compat/flatten-color-palette.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { expect, test } from 'vitest' -import flattenColorPalette from './flatten-color-palette' - -test('it should handle private __CSS_VALUES__ to resolve to the right value', () => { - expect( - flattenColorPalette({ - 'slate-100': '#000100', - 'slate-200': '#000200', - 'slate-300': '#000300', - 'slate-400': '#100400', - 'slate-500': '#100500', - __CSS_VALUES__: { - 'slate-100': 4, - 'slate-200': 4, - 'slate-300': 4, - 'slate-400': 0, - 'slate-500': 0, - } as any, - slate: { '200': '#200200', '400': '#200400', '600': '#200600' }, - 'slate-600': '#200600', - }), - ).toMatchInlineSnapshot(` - { - "slate-100": "#000100", - "slate-200": "#200200", - "slate-300": "#000300", - "slate-400": "#100400", - "slate-500": "#100500", - "slate-600": "#200600", - } - `) -}) diff --git a/packages/tailwindcss/src/compat/flatten-color-palette.ts b/packages/tailwindcss/src/compat/flatten-color-palette.ts index ca8845688..198ae060d 100644 --- a/packages/tailwindcss/src/compat/flatten-color-palette.ts +++ b/packages/tailwindcss/src/compat/flatten-color-palette.ts @@ -1,14 +1,11 @@ -import { ThemeOptions } from '../theme' - type Colors = { [key: string | number]: string | Colors } -export default function flattenColorPalette(colors: Colors) { +export function flattenColorPalette(colors: Colors) { let result: Record = {} for (let [root, children] of Object.entries(colors ?? {})) { - if (root === '__CSS_VALUES__') continue if (typeof children === 'object' && children !== null) { for (let [parent, value] of Object.entries(flattenColorPalette(children))) { result[`${root}${parent === 'DEFAULT' ? '' : `-${parent}`}`] = value @@ -18,13 +15,5 @@ export default function flattenColorPalette(colors: Colors) { } } - if ('__CSS_VALUES__' in colors) { - for (let [key, value] of Object.entries(colors.__CSS_VALUES__)) { - if ((Number(value) & ThemeOptions.DEFAULT) === 0) { - result[key] = colors[key] as string - } - } - } - return result } diff --git a/packages/tailwindcss/src/compat/legacy-utilities.test.ts b/packages/tailwindcss/src/compat/legacy-utilities.test.ts deleted file mode 100644 index c90eae845..000000000 --- a/packages/tailwindcss/src/compat/legacy-utilities.test.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { expect, test } from 'vitest' -import { run } from '../test-utils/run' - -const css = String.raw - -test('bg-gradient-*', async () => { - expect( - await run([ - 'bg-gradient-to-t', - 'bg-gradient-to-tr', - 'bg-gradient-to-r', - 'bg-gradient-to-br', - 'bg-gradient-to-b', - 'bg-gradient-to-bl', - 'bg-gradient-to-l', - 'bg-gradient-to-tl', - ]), - ).toMatchInlineSnapshot(` - " - .bg-gradient-to-b { - --tw-gradient-position: to bottom in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - - .bg-gradient-to-bl { - --tw-gradient-position: to bottom left in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - - .bg-gradient-to-br { - --tw-gradient-position: to bottom right in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - - .bg-gradient-to-l { - --tw-gradient-position: to left in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - - .bg-gradient-to-r { - --tw-gradient-position: to right in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - - .bg-gradient-to-t { - --tw-gradient-position: to top in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - - .bg-gradient-to-tl { - --tw-gradient-position: to top left in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - - .bg-gradient-to-tr { - --tw-gradient-position: to top right in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - " - `) -}) - -test('max-w-screen', async () => { - expect( - await run( - [ - 'max-w-screen-sm', - 'max-w-screen-md', - 'max-w-screen-lg', - 'max-w-screen-xl', - 'max-w-screen-2xl', - ], - css` - @theme { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @tailwind utilities; - `, - ), - ).toMatchInlineSnapshot(` - " - :root, :host { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - - .max-w-screen-2xl { - max-width: var(--breakpoint-2xl); - } - - .max-w-screen-lg { - max-width: var(--breakpoint-lg); - } - - .max-w-screen-md { - max-width: var(--breakpoint-md); - } - - .max-w-screen-sm { - max-width: var(--breakpoint-sm); - } - - .max-w-screen-xl { - max-width: var(--breakpoint-xl); - } - " - `) - expect( - await run([ - 'max-w-screen-oh', - 'max-w-screen-4', - 'max-w-screen-[4px]', - '-max-w-screen-sm', - '-max-w-screen-[4px]', - 'max-w-screen-none/foo', - 'max-w-screen-full/foo', - 'max-w-screen-max/foo', - 'max-w-screen-max/foo', - 'max-w-screen-fit/foo', - 'max-w-screen-4/foo', - 'max-w-screen-xl/foo', - 'max-w-screen-[4px]/foo', - ]), - ).toEqual('') -}) - -test('box-decoration', async () => { - expect(await run(['decoration-slice', 'decoration-clone'])).toMatchInlineSnapshot(` - " - .decoration-clone { - -webkit-box-decoration-break: clone; - box-decoration-break: clone; - } - - .decoration-slice { - -webkit-box-decoration-break: slice; - box-decoration-break: slice; - } - " - `) -}) - -test('overflow-ellipsis', async () => { - expect(await run(['overflow-ellipsis'])).toMatchInlineSnapshot(` - " - .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: 1; - } - - .flex-grow-0 { - flex-grow: 0; - } - - .flex-grow-\\[123\\] { - flex-grow: 123; - } - " - `) - expect( - await run([ - '-flex-grow', - 'flex-grow--1', - 'flex-grow-1.5', - '-flex-grow-0', - '-flex-grow-[123]', - 'flex-grow-unknown', - 'flex-grow/foo', - 'flex-grow-0/foo', - 'flex-grow-[123]/foo', - ]), - ).toEqual('') -}) - -test('flex-shrink', async () => { - expect(await run(['flex-shrink', 'flex-shrink-0', 'flex-shrink-[123]'])).toMatchInlineSnapshot(` - " - .flex-shrink { - flex-shrink: 1; - } - - .flex-shrink-0 { - flex-shrink: 0; - } - - .flex-shrink-\\[123\\] { - flex-shrink: 123; - } - " - `) - expect( - await run([ - '-flex-shrink', - 'flex-shrink--1', - 'flex-shrink-1.5', - '-flex-shrink-0', - '-flex-shrink-[123]', - 'flex-shrink-unknown', - 'flex-shrink/foo', - 'flex-shrink-0/foo', - 'flex-shrink-[123]/foo', - ]), - ).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 deleted file mode 100644 index 8e7829035..000000000 --- a/packages/tailwindcss/src/compat/legacy-utilities.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { decl } from '../ast' -import type { DesignSystem } from '../design-system' -import { isPositiveInteger, isValidSpacingMultiplier } from '../utils/infer-data-type' -import { segment } from '../utils/segment' - -export function registerLegacyUtilities(designSystem: DesignSystem) { - for (let [value, direction] of [ - ['t', 'top'], - ['tr', 'top right'], - ['r', 'right'], - ['br', 'bottom right'], - ['b', 'bottom'], - ['bl', 'bottom left'], - ['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))`), - ]) - } - - // 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 - let value = designSystem.theme.resolve(candidate.value.value, ['--breakpoint']) - if (!value) return - 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 - - if (!candidate.value) { - return [decl('flex-shrink', '1')] - } - - if (candidate.value.kind === 'arbitrary') { - return [decl('flex-shrink', candidate.value.value)] - } - - if (isPositiveInteger(candidate.value.value)) { - return [decl('flex-shrink', candidate.value.value)] - } - }) - - designSystem.utilities.suggest('flex-grow', () => []) - designSystem.utilities.functional('flex-grow', (candidate) => { - if (candidate.modifier) return - - if (!candidate.value) { - return [decl('flex-grow', '1')] - } - - if (candidate.value.kind === 'arbitrary') { - return [decl('flex-grow', candidate.value.value)] - } - - if (isPositiveInteger(candidate.value.value)) { - 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 5fcacb5cd..1f5a1aec9 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test, vi } from 'vitest' import { compile } from '..' import plugin from '../plugin' -import { compileCss, run } from '../test-utils/run' +import { optimizeCss } from '../test-utils/run' import defaultTheme from './default-theme' import type { CssInJs, PluginAPI } from './plugin-api' @@ -9,61 +9,57 @@ const css = String.raw describe('theme', async () => { test('plugin theme can contain objects', async () => { - expect( - await compileCss( - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ addBase, theme }) { - addBase({ - '@keyframes enter': theme('keyframes.enter'), - '@keyframes exit': theme('keyframes.exit'), - }) - }, - { - theme: { - extend: { - keyframes: { - enter: { - from: { - opacity: 'var(--tw-enter-opacity, 1)', - transform: - 'translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))', - }, - }, - exit: { - to: { - opacity: 'var(--tw-exit-opacity, 1)', - transform: - 'translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))', - }, - }, + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ addBase, theme }) { + addBase({ + '@keyframes enter': theme('keyframes.enter'), + '@keyframes exit': theme('keyframes.exit'), + }) + }, + { + theme: { + extend: { + keyframes: { + enter: { + from: { + opacity: 'var(--tw-enter-opacity, 1)', + transform: + 'translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))', + }, + }, + exit: { + to: { + opacity: 'var(--tw-exit-opacity, 1)', + transform: + 'translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))', }, }, }, }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { + }, + }, + ), + } + }, + }) + + expect(compiler.build([])).toMatchInlineSnapshot(` + "@layer base { @keyframes enter { from { opacity: var(--tw-enter-opacity, 1); transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0)); } } - @keyframes exit { to { opacity: var(--tw-exit-opacity, 1); @@ -71,36 +67,16 @@ describe('theme', async () => { } } } - " - `) - }) - - test('keyframes added via addUtilities are appended to the AST', async () => { - expect( - await compileCss( - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ addUtilities }) { - addUtilities({ - '@keyframes enter': { from: { opacity: 'var(--tw-enter-opacity, 1)' } }, - }) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " @keyframes enter { 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); + 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)); } } " @@ -108,40 +84,47 @@ describe('theme', async () => { }) test('plugin theme can extend colors', async () => { - expect( - await run( - ['scrollbar-red-500', 'scrollbar-russet-700'], - css` - @theme reference { - --color-red-500: #ef4444; - } - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { values: theme('colors') }, - ) + let input = css` + @theme reference { + --color-red-500: #ef4444; + } + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), }, - { theme: { extend: { colors: { 'russet-700': '#7a4724' } } } }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .scrollbar-red-500 { + { + values: theme('colors'), + }, + ) + }, + { + theme: { + extend: { + colors: { + 'russet-700': '#7a4724', + }, + }, + }, + }, + ), + } + }, + }) + + expect(compiler.build(['scrollbar-red-500', 'scrollbar-russet-700'])).toMatchInlineSnapshot(` + ".scrollbar-red-500 { scrollbar-color: #ef4444; } - .scrollbar-russet-700 { scrollbar-color: #7a4724; } @@ -152,43 +135,45 @@ describe('theme', async () => { test('plugin theme values can reference legacy theme keys that have been replaced with bare value support', async ({ expect, }) => { - expect( - await run( - ['animate-duration-316'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'animate-duration': (value) => ({ 'animation-duration': value }) }, - { values: theme('animationDuration') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-duration': value }), }, { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => { - return { ...theme('transitionDuration') } - }, - }, + values: theme('animationDuration'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => { + return { + ...theme('transitionDuration'), + } }, }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animate-duration-316 { - animation-duration: .316s; + }, + }, + ), + } + }, + }) + + expect(compiler.build(['animate-duration-316'])).toMatchInlineSnapshot(` + ".animate-duration-316 { + animation-duration: 316ms; } " `) @@ -197,402 +182,352 @@ describe('theme', async () => { test('plugin theme values that support bare values are merged with other values for that theme key', async ({ expect, }) => { - expect( - await run( - ['animate-duration-316', 'animate-duration-slow'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'animate-duration': (value) => ({ 'animation-duration': value }) }, - { values: theme('animationDuration') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-duration': value }), }, { - theme: { - extend: { - transitionDuration: { slow: '800ms' }, - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), - }, - }, + values: theme('animationDuration'), }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animate-duration-316 { - animation-duration: .316s; - } + ) + }, + { + theme: { + extend: { + transitionDuration: { + slow: '800ms', + }, + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + }, + }, + }, + ), + } + }, + }) + + expect(compiler.build(['animate-duration-316', 'animate-duration-slow'])) + .toMatchInlineSnapshot(` + ".animate-duration-316 { + animation-duration: 316ms; + } .animate-duration-slow { - animation-duration: .8s; + animation-duration: 800ms; } " `) }) test('plugin theme can have opacity modifiers', async () => { - expect( - await run( - ['percentage', 'fraction', 'variable'], - css` - @tailwind utilities; - @theme { - --color-red-500: #ef4444; - } - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.percentage': { color: theme('colors.red.500 / 50%') }, - '.fraction': { color: theme('colors.red.500 / 0.5') }, - '.variable': { color: theme('colors.red.500 / var(--opacity)') }, - }) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .fraction, .percentage { - color: oklab(63.6834% .187 .088 / .5); + let input = css` + @tailwind utilities; + @theme { + --color-red-500: #ef4444; } + @plugin "my-plugin"; + ` + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + 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 srgb, #ef4444 50%, transparent); + } + .percentage { + color: color-mix(in srgb, #ef4444 50%, transparent); + } .variable { - color: #ef4444; + color: color-mix(in srgb, #ef4444 calc(var(--opacity) * 100%), transparent); } - - @supports (color: color-mix(in lab, red, red)) { - .variable { - color: color-mix(in oklab, #ef4444 var(--opacity), transparent); - } + :root { + --color-red-500: #ef4444; } " `) }) - - test('plugin theme colors can use ', async () => { - expect( - await run( - [ - 'bg-custom', - 'css-percentage', - 'css-fraction', - 'css-variable', - 'js-percentage', - 'js-fraction', - 'js-variable', - ], - css` - @tailwind utilities; - @theme { - /* This should not work */ - --color-custom-css: rgba(255 0 0 / ); - } - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ addUtilities, theme }) { - addUtilities({ - '.css-percentage': { color: theme('colors.custom-css / 50%') }, - '.css-fraction': { color: theme('colors.custom-css / 0.5') }, - '.css-variable': { color: theme('colors.custom-css / var(--opacity)') }, - '.js-percentage': { color: theme('colors.custom-js / 50%') }, - '.js-fraction': { color: theme('colors.custom-js / 0.5') }, - '.js-variable': { color: theme('colors.custom-js / var(--opacity)') }, - }) - }, - { - theme: { - colors: { - /* This should work */ - 'custom-js': 'rgb(255 0 0 / )', - }, - }, - }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .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)) { - .css-variable { - color: color-mix(in oklab, rgba(255 0 0 / ) var(--opacity), transparent); - } - } - - .js-fraction, .js-percentage { - color: oklab(62.7955% .224 .125 / .5); - } - - .js-variable { - color: red; - } - - @supports (color: color-mix(in lab, red, red)) { - .js-variable { - color: color-mix(in oklab, red var(--opacity), transparent); - } - } - " - `) - }) - test('theme value functions are resolved correctly regardless of order', async () => { - expect( - await run( - ['animate-delay-316', 'animate-delay-slow'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'animate-delay': (value) => ({ 'animation-delay': value }) }, - { values: theme('animationDelay') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-delay': (value) => ({ 'animation-delay': value }), }, { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), - animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('animationDuration'), - }), - transitionDuration: { slow: '800ms' }, - }, + values: theme('animationDelay'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + + animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('animationDuration'), + }), + + transitionDuration: { + slow: '800ms', }, }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animate-delay-316 { - animation-delay: .316s; - } + }, + }, + ), + } + }, + }) + expect(compiler.build(['animate-delay-316', 'animate-delay-slow'])).toMatchInlineSnapshot(` + ".animate-delay-316 { + animation-delay: 316ms; + } .animate-delay-slow { - animation-delay: .8s; + animation-delay: 800ms; } " `) }) test('plugins can override the default key', async () => { - expect( - await run( - ['animate-duration'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'animate-duration': (value) => ({ 'animation-delay': value }) }, - { values: theme('transitionDuration') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-delay': value }), }, - { theme: { extend: { transitionDuration: { DEFAULT: '1500ms' } } } }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animate-duration { - animation-delay: 1.5s; + { + values: theme('transitionDuration'), + }, + ) + }, + { + theme: { + extend: { + transitionDuration: { + DEFAULT: '1500ms', + }, + }, + }, + }, + ), + } + }, + }) + + expect(compiler.build(['animate-duration'])).toMatchInlineSnapshot(` + ".animate-duration { + animation-delay: 1500ms; } " `) }) test('plugins can read CSS theme keys using the old theme key notation', async () => { - expect( - await run( - ['animation-spin', 'animation', 'animation2', 'animation2-twist'], - css` - @tailwind utilities; - @plugin "my-plugin"; - @theme reference { - --animate: pulse 1s linear infinite; - --animate-spin: spin 1s linear infinite; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ matchUtilities, theme }) { - matchUtilities( - { animation: (value) => ({ animation: value }) }, - { values: theme('animation') }, - ) - - matchUtilities( - { animation2: (value) => ({ animation: value }) }, - { - values: { DEFAULT: theme('animation.DEFAULT'), twist: theme('animation.spin') }, - }, - ) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animation { - animation: 1s linear infinite pulse; + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + @theme reference { + --animation: pulse 1s linear infinite; + --animation-spin: spin 1s linear infinite; } + ` - .animation-spin { - animation: 1s linear infinite spin; - } + let compiler = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ animation: value }), + }, + { + values: theme('animation'), + }, + ) - .animation2 { - animation: 1s linear infinite pulse; - } + matchUtilities( + { + animation2: (value) => ({ animation: value }), + }, + { + values: { + DEFAULT: theme('animation.DEFAULT'), + twist: theme('animation.spin'), + }, + }, + ) + }), + } + }, + }) - .animation2-twist { - animation: 1s linear infinite spin; - } - " - `) + expect(compiler.build(['animation-spin', 'animation', 'animation2', 'animation2-twist'])) + .toMatchInlineSnapshot(` + ".animation { + animation: pulse 1s linear infinite; + } + .animation-spin { + animation: spin 1s linear infinite; + } + .animation2 { + animation: pulse 1s linear infinite; + } + .animation2-twist { + animation: spin 1s linear infinite; + } + " + `) }) test('CSS theme values are merged with JS theme values', async () => { - expect( - await run( - ['animation', 'animation-spin', 'animation-bounce'], - css` - @tailwind utilities; - @plugin "my-plugin"; - @theme reference { - --animate: pulse 1s linear infinite; - --animate-spin: spin 1s linear infinite; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { animation: (value) => ({ '--animation': value }) }, - { values: theme('animation') }, - ) - }, - { theme: { extend: { animation: { bounce: 'bounce 1s linear infinite' } } } }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animation { + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + @theme reference { --animation: pulse 1s linear infinite; + --animation-spin: spin 1s linear infinite; } + ` - .animation-bounce { - --animation: bounce 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-spin { - --animation: spin 1s linear infinite; - } - " - `) + expect(compiler.build(['animation', 'animation-spin', 'animation-bounce'])) + .toMatchInlineSnapshot(` + ".animation { + --animation: pulse 1s linear infinite; + } + .animation-bounce { + --animation: bounce 1s linear infinite; + } + .animation-spin { + --animation: spin 1s linear infinite; + } + " + `) }) test('CSS theme defaults take precedence over JS theme defaults', async () => { - expect( - await run( - ['animation'], - css` - @tailwind utilities; - @plugin "my-plugin"; - @theme reference { - --animate: pulse 1s linear infinite; - --animate-spin: spin 1s linear infinite; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { animation: (value) => ({ '--animation': value }) }, - { values: theme('animation') }, - ) + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + @theme reference { + --animation: pulse 1s linear infinite; + --animation-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 }), }, - { theme: { extend: { animation: { DEFAULT: 'twist 1s linear infinite' } } } }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .animation { + { + values: theme('animation'), + }, + ) + }, + { + theme: { + extend: { + animation: { + DEFAULT: 'twist 1s linear infinite', + }, + }, + }, + }, + ), + } + }, + }) + + expect(compiler.build(['animation'])).toMatchInlineSnapshot(` + ".animation { --animation: pulse 1s linear infinite; } " @@ -604,24 +539,29 @@ describe('theme', async () => { @tailwind utilities; @plugin "my-plugin"; @theme reference { - --animate-simple-spin: spin 1s linear infinite; - --animate-simple-bounce: bounce 1s linear infinite; + --animation-simple-spin: spin 1s linear infinite; + --animation-simple-bounce: bounce 1s linear infinite; } ` - using fn = vi.fn() + let 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', + }, + }, + }, }, ), } @@ -635,259 +575,240 @@ describe('theme', async () => { }) }) - test('theme() resolves the DEFAULT value when a bare CSS theme key shares a prefix with a sibling key', async () => { - expect( - await run( - ['example-foo', 'example-foo-bar'], - css` - @tailwind utilities; - @theme { - --color-foo: red; - --color-foo-bar: blue; - } - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.example-foo': { color: theme('colors.foo') }, - '.example-foo-bar': { color: theme('colors.foo-bar') }, - }) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .example-foo { - color: red; - } - - .example-foo-bar { - color: #00f; - } - " - `) - }) - test('all necessary theme keys support bare values', async () => { - expect( - await run( - [ - 'my-aspect-2/5', - 'my-backdrop-brightness-1', - 'my-backdrop-contrast-1', - 'my-backdrop-grayscale-1', - 'my-backdrop-hue-rotate-1', - 'my-backdrop-invert-1', - 'my-backdrop-opacity-1', - 'my-backdrop-saturate-1', - 'my-backdrop-sepia-1', - 'my-border-width-1', - 'my-brightness-1', - 'my-columns-1', - 'my-contrast-1', - 'my-divide-width-1', - 'my-flex-grow-1', - 'my-flex-shrink-1', - 'my-gradient-color-stop-positions-1', - 'my-grayscale-1', - 'my-grid-row-end-1', - 'my-grid-row-start-1', - 'my-grid-template-columns-1', - 'my-grid-template-rows-1', - 'my-hue-rotate-1', - 'my-invert-1', - 'my-line-clamp-1', - 'my-opacity-1', - 'my-order-1', - 'my-outline-offset-1', - 'my-outline-width-1', - 'my-ring-offset-width-1', - 'my-ring-width-1', - 'my-rotate-1', - 'my-saturate-1', - 'my-scale-1', - 'my-sepia-1', - 'my-skew-1', - 'my-stroke-width-1', - 'my-text-decoration-thickness-1', - 'my-text-underline-offset-1', - 'my-transition-delay-1', - 'my-transition-duration-1', - 'my-z-index-1', - ], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ matchUtilities, theme }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - { values: theme(themeKey) }, - ) - } + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` - utility('my-aspect', 'aspectRatio') - utility('my-backdrop-brightness', 'backdropBrightness') - utility('my-backdrop-contrast', 'backdropContrast') - utility('my-backdrop-grayscale', 'backdropGrayscale') - utility('my-backdrop-hue-rotate', 'backdropHueRotate') - utility('my-backdrop-invert', 'backdropInvert') - utility('my-backdrop-opacity', 'backdropOpacity') - utility('my-backdrop-saturate', 'backdropSaturate') - utility('my-backdrop-sepia', 'backdropSepia') - utility('my-border-width', 'borderWidth') - utility('my-brightness', 'brightness') - utility('my-columns', 'columns') - utility('my-contrast', 'contrast') - utility('my-divide-width', 'divideWidth') - utility('my-flex-grow', 'flexGrow') - utility('my-flex-shrink', 'flexShrink') - utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') - utility('my-grayscale', 'grayscale') - utility('my-grid-row-end', 'gridRowEnd') - utility('my-grid-row-start', 'gridRowStart') - utility('my-grid-template-columns', 'gridTemplateColumns') - utility('my-grid-template-rows', 'gridTemplateRows') - utility('my-hue-rotate', 'hueRotate') - utility('my-invert', 'invert') - utility('my-line-clamp', 'lineClamp') - utility('my-opacity', 'opacity') - utility('my-order', 'order') - utility('my-outline-offset', 'outlineOffset') - utility('my-outline-width', 'outlineWidth') - utility('my-ring-offset-width', 'ringOffsetWidth') - utility('my-ring-width', 'ringWidth') - utility('my-rotate', 'rotate') - utility('my-saturate', 'saturate') - utility('my-scale', 'scale') - utility('my-sepia', 'sepia') - utility('my-skew', 'skew') - utility('my-stroke-width', 'strokeWidth') - utility('my-text-decoration-thickness', 'textDecorationThickness') - utility('my-text-underline-offset', 'textUnderlineOffset') - utility('my-transition-delay', 'transitionDelay') - utility('my-transition-duration', 'transitionDuration') - utility('my-z-index', 'zIndex') - }), + let { build } = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ matchUtilities, theme }) { + function utility(name: string, themeKey: string) { + matchUtilities( + { [name]: (value) => ({ '--value': value }) }, + { values: theme(themeKey) }, + ) } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .my-aspect-2\\/5 { + + utility('my-aspect', 'aspectRatio') + utility('my-backdrop-brightness', 'backdropBrightness') + utility('my-backdrop-contrast', 'backdropContrast') + utility('my-backdrop-grayscale', 'backdropGrayscale') + utility('my-backdrop-hue-rotate', 'backdropHueRotate') + utility('my-backdrop-invert', 'backdropInvert') + utility('my-backdrop-opacity', 'backdropOpacity') + utility('my-backdrop-saturate', 'backdropSaturate') + utility('my-backdrop-sepia', 'backdropSepia') + utility('my-border-width', 'borderWidth') + utility('my-brightness', 'brightness') + utility('my-columns', 'columns') + utility('my-contrast', 'contrast') + utility('my-divide-width', 'divideWidth') + utility('my-flex-grow', 'flexGrow') + utility('my-flex-shrink', 'flexShrink') + utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') + utility('my-grayscale', 'grayscale') + utility('my-grid-row-end', 'gridRowEnd') + utility('my-grid-row-start', 'gridRowStart') + utility('my-grid-template-columns', 'gridTemplateColumns') + utility('my-grid-template-rows', 'gridTemplateRows') + utility('my-hue-rotate', 'hueRotate') + utility('my-invert', 'invert') + utility('my-line-clamp', 'lineClamp') + utility('my-opacity', 'opacity') + utility('my-order', 'order') + utility('my-outline-offset', 'outlineOffset') + utility('my-outline-width', 'outlineWidth') + utility('my-ring-offset-width', 'ringOffsetWidth') + utility('my-ring-width', 'ringWidth') + utility('my-rotate', 'rotate') + utility('my-saturate', 'saturate') + utility('my-scale', 'scale') + utility('my-sepia', 'sepia') + utility('my-skew', 'skew') + utility('my-stroke-width', 'strokeWidth') + utility('my-text-decoration-thickness', 'textDecorationThickness') + utility('my-text-underline-offset', 'textUnderlineOffset') + utility('my-transition-delay', 'transitionDelay') + utility('my-transition-duration', 'transitionDuration') + utility('my-z-index', 'zIndex') + }), + } + }, + }) + + let output = build([ + 'my-aspect-2/5', + 'my-backdrop-brightness-1', + 'my-backdrop-contrast-1', + 'my-backdrop-grayscale-1', + 'my-backdrop-hue-rotate-1', + 'my-backdrop-invert-1', + 'my-backdrop-opacity-1', + 'my-backdrop-saturate-1', + 'my-backdrop-sepia-1', + 'my-border-width-1', + 'my-brightness-1', + 'my-columns-1', + 'my-contrast-1', + 'my-divide-width-1', + 'my-flex-grow-1', + 'my-flex-shrink-1', + 'my-gradient-color-stop-positions-1', + 'my-grayscale-1', + 'my-grid-row-end-1', + 'my-grid-row-start-1', + 'my-grid-template-columns-1', + 'my-grid-template-rows-1', + 'my-hue-rotate-1', + 'my-invert-1', + 'my-line-clamp-1', + 'my-opacity-1', + 'my-order-1', + 'my-outline-offset-1', + 'my-outline-width-1', + 'my-ring-offset-width-1', + 'my-ring-width-1', + 'my-rotate-1', + 'my-saturate-1', + 'my-scale-1', + 'my-sepia-1', + 'my-skew-1', + 'my-stroke-width-1', + 'my-text-decoration-thickness-1', + 'my-text-underline-offset-1', + 'my-transition-delay-1', + 'my-transition-duration-1', + 'my-z-index-1', + ]) + + expect(output).toMatchInlineSnapshot(` + ".my-aspect-2\\/5 { --value: 2/5; } - - .my-backdrop-brightness-1, .my-backdrop-contrast-1, .my-backdrop-grayscale-1 { + .my-backdrop-brightness-1 { + --value: 1%; + } + .my-backdrop-contrast-1 { + --value: 1%; + } + .my-backdrop-grayscale-1 { --value: 1%; } - .my-backdrop-hue-rotate-1 { --value: 1deg; } - - .my-backdrop-invert-1, .my-backdrop-opacity-1, .my-backdrop-saturate-1, .my-backdrop-sepia-1 { + .my-backdrop-invert-1 { + --value: 1%; + } + .my-backdrop-opacity-1 { + --value: 1%; + } + .my-backdrop-saturate-1 { + --value: 1%; + } + .my-backdrop-sepia-1 { --value: 1%; } - .my-border-width-1 { --value: 1px; } - .my-brightness-1 { --value: 1%; } - .my-columns-1 { --value: 1; } - .my-contrast-1 { --value: 1%; } - .my-divide-width-1 { --value: 1px; } - - .my-flex-grow-1, .my-flex-shrink-1 { + .my-flex-grow-1 { --value: 1; } - - .my-gradient-color-stop-positions-1, .my-grayscale-1 { + .my-flex-shrink-1 { + --value: 1; + } + .my-gradient-color-stop-positions-1 { --value: 1%; } - - .my-grid-row-end-1, .my-grid-row-start-1 { + .my-grayscale-1 { + --value: 1%; + } + .my-grid-row-end-1 { --value: 1; } - - .my-grid-template-columns-1, .my-grid-template-rows-1 { + .my-grid-row-start-1 { + --value: 1; + } + .my-grid-template-columns-1 { + --value: repeat(1, minmax(0, 1fr)); + } + .my-grid-template-rows-1 { --value: repeat(1, minmax(0, 1fr)); } - .my-hue-rotate-1 { --value: 1deg; } - .my-invert-1 { --value: 1%; } - .my-line-clamp-1 { --value: 1; } - .my-opacity-1 { --value: 1%; } - .my-order-1 { --value: 1; } - - .my-outline-offset-1, .my-outline-width-1, .my-ring-offset-width-1, .my-ring-width-1 { + .my-outline-offset-1 { + --value: 1px; + } + .my-outline-width-1 { + --value: 1px; + } + .my-ring-offset-width-1 { + --value: 1px; + } + .my-ring-width-1 { --value: 1px; } - .my-rotate-1 { --value: 1deg; } - - .my-saturate-1, .my-scale-1, .my-sepia-1 { + .my-saturate-1 { + --value: 1%; + } + .my-scale-1 { + --value: 1%; + } + .my-sepia-1 { --value: 1%; } - .my-skew-1 { --value: 1deg; } - .my-stroke-width-1 { --value: 1; } - - .my-text-decoration-thickness-1, .my-text-underline-offset-1 { + .my-text-decoration-thickness-1 { --value: 1px; } - - .my-transition-delay-1, .my-transition-duration-1 { + .my-text-underline-offset-1 { + --value: 1px; + } + .my-transition-delay-1 { + --value: 1ms; + } + .my-transition-duration-1 { --value: 1ms; } - .my-z-index-1 { --value: 1; } @@ -905,12 +826,11 @@ describe('theme', async () => { } ` - using fn = vi.fn() + let fn = vi.fn() await compile(input, { - loadModule: async (_id, base) => { + loadModule: async (id, base) => { return { - path: '', base, module: plugin( ({ theme }) => { @@ -943,17 +863,16 @@ describe('theme', async () => { @tailwind utilities; @plugin "my-plugin"; @theme { - --ease-in: ease-in; - --ease-out: ease-out; + --transition-timing-function-in: ease-in; + --transition-timing-function-out: ease-out; } ` - using fn = vi.fn() + let fn = vi.fn() await compile(input, { - loadModule: async (_id, base) => { + loadModule: async (id, base) => { return { - path: '', base, module: plugin(({ theme }) => { fn(theme('transitionTimingFunction.DEFAULT')) @@ -980,12 +899,11 @@ describe('theme', async () => { } ` - using fn = vi.fn() + let fn = vi.fn() await compile(input, { - loadModule: async (_id, base) => { + loadModule: async (id, base) => { return { - path: '', base, module: plugin(({ theme }) => { fn(theme('color.red.100')) @@ -1009,12 +927,11 @@ describe('theme', async () => { @plugin "my-plugin"; ` - using fn = vi.fn() + let fn = vi.fn() await compile(input, { - loadModule: async (_id, base) => { + loadModule: async (id, base) => { return { - path: '', base, module: plugin(({ theme }) => { fn(theme('i.do.not.exist')) @@ -1033,806 +950,421 @@ describe('theme', async () => { }) test('Candidates can match multiple utility definitions', async () => { - expect( - await run( - ['foo-bar'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(({ addUtilities, matchUtilities }) => { - addUtilities({ '.foo-bar': { color: 'red' } }) - matchUtilities( - { foo: (value) => ({ '--my-prop': value }) }, - { values: { bar: 'bar-valuer', baz: 'bar-valuer' } }, - ) - addUtilities({ '.foo-bar': { backgroundColor: 'red' } }) - }), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let { build } = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin(({ addUtilities, matchUtilities }) => { + addUtilities({ + '.foo-bar': { + color: 'red', + }, + }) + + matchUtilities( + { + foo: (value) => ({ + '--my-prop': value, + }), + }, + { + values: { + bar: 'bar-valuer', + baz: 'bar-valuer', + }, + }, + ) + + addUtilities({ + '.foo-bar': { + backgroundColor: 'red', + }, + }) + }), + } + }, + }) + + expect(build(['foo-bar'])).toMatchInlineSnapshot(` + ".foo-bar { + background-color: red; + } .foo-bar { color: red; + } + .foo-bar { --my-prop: bar-valuer; - background-color: red; } " `) }) test('spreading `tailwindcss/defaultTheme` exports keeps bare values', async () => { - expect( - await run( - [ - 'my-aspect-2/5', - // 'my-backdrop-brightness-1', - // 'my-backdrop-contrast-1', - // 'my-backdrop-grayscale-1', - // 'my-backdrop-hue-rotate-1', - // 'my-backdrop-invert-1', - // 'my-backdrop-opacity-1', - // 'my-backdrop-saturate-1', - // 'my-backdrop-sepia-1', - // 'my-divide-width-1', - 'my-border-width-1', - 'my-brightness-1', - 'my-columns-1', - 'my-contrast-1', - 'my-flex-grow-1', - 'my-flex-shrink-1', - 'my-gradient-color-stop-positions-1', - 'my-grayscale-1', - 'my-grid-row-end-1', - 'my-grid-row-start-1', - 'my-grid-template-columns-1', - 'my-grid-template-rows-1', - 'my-hue-rotate-1', - 'my-invert-1', - 'my-line-clamp-1', - 'my-opacity-1', - 'my-order-1', - 'my-outline-offset-1', - 'my-outline-width-1', - 'my-ring-offset-width-1', - 'my-ring-width-1', - 'my-rotate-1', - 'my-saturate-1', - 'my-scale-1', - 'my-sepia-1', - 'my-skew-1', - 'my-stroke-width-1', - 'my-text-decoration-thickness-1', - 'my-text-underline-offset-1', - 'my-transition-delay-1', - 'my-transition-duration-1', - 'my-z-index-1', - ], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ matchUtilities }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - // @ts-ignore - { values: defaultTheme[themeKey] }, - ) - } + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` - utility('my-aspect', 'aspectRatio') - // The following keys deliberately doesn't work as these are exported - // as functions from the compat config. - // - // utility('my-backdrop-brightness', 'backdropBrightness') - // utility('my-backdrop-contrast', 'backdropContrast') - // utility('my-backdrop-grayscale', 'backdropGrayscale') - // utility('my-backdrop-hue-rotate', 'backdropHueRotate') - // utility('my-backdrop-invert', 'backdropInvert') - // utility('my-backdrop-opacity', 'backdropOpacity') - // utility('my-backdrop-saturate', 'backdropSaturate') - // utility('my-backdrop-sepia', 'backdropSepia') - // utility('my-divide-width', 'divideWidth') - utility('my-border-width', 'borderWidth') - utility('my-brightness', 'brightness') - utility('my-columns', 'columns') - utility('my-contrast', 'contrast') - utility('my-flex-grow', 'flexGrow') - utility('my-flex-shrink', 'flexShrink') - utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') - utility('my-grayscale', 'grayscale') - utility('my-grid-row-end', 'gridRowEnd') - utility('my-grid-row-start', 'gridRowStart') - utility('my-grid-template-columns', 'gridTemplateColumns') - utility('my-grid-template-rows', 'gridTemplateRows') - utility('my-hue-rotate', 'hueRotate') - utility('my-invert', 'invert') - utility('my-line-clamp', 'lineClamp') - utility('my-opacity', 'opacity') - utility('my-order', 'order') - utility('my-outline-offset', 'outlineOffset') - utility('my-outline-width', 'outlineWidth') - utility('my-ring-offset-width', 'ringOffsetWidth') - utility('my-ring-width', 'ringWidth') - utility('my-rotate', 'rotate') - utility('my-saturate', 'saturate') - utility('my-scale', 'scale') - utility('my-sepia', 'sepia') - utility('my-skew', 'skew') - utility('my-stroke-width', 'strokeWidth') - utility('my-text-decoration-thickness', 'textDecorationThickness') - utility('my-text-underline-offset', 'textUnderlineOffset') - utility('my-transition-delay', 'transitionDelay') - utility('my-transition-duration', 'transitionDuration') - utility('my-z-index', 'zIndex') - }), + let { build } = await compile(input, { + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ matchUtilities }) { + function utility(name: string, themeKey: string) { + matchUtilities( + { [name]: (value) => ({ '--value': value }) }, + // @ts-ignore + { values: defaultTheme[themeKey] }, + ) } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .my-aspect-2\\/5 { + + utility('my-aspect', 'aspectRatio') + // The following keys deliberately doesn't work as these are exported + // as functions from the compat config. + // + // utility('my-backdrop-brightness', 'backdropBrightness') + // utility('my-backdrop-contrast', 'backdropContrast') + // utility('my-backdrop-grayscale', 'backdropGrayscale') + // utility('my-backdrop-hue-rotate', 'backdropHueRotate') + // utility('my-backdrop-invert', 'backdropInvert') + // utility('my-backdrop-opacity', 'backdropOpacity') + // utility('my-backdrop-saturate', 'backdropSaturate') + // utility('my-backdrop-sepia', 'backdropSepia') + // utility('my-divide-width', 'divideWidth') + utility('my-border-width', 'borderWidth') + utility('my-brightness', 'brightness') + utility('my-columns', 'columns') + utility('my-contrast', 'contrast') + utility('my-flex-grow', 'flexGrow') + utility('my-flex-shrink', 'flexShrink') + utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') + utility('my-grayscale', 'grayscale') + utility('my-grid-row-end', 'gridRowEnd') + utility('my-grid-row-start', 'gridRowStart') + utility('my-grid-template-columns', 'gridTemplateColumns') + utility('my-grid-template-rows', 'gridTemplateRows') + utility('my-hue-rotate', 'hueRotate') + utility('my-invert', 'invert') + utility('my-line-clamp', 'lineClamp') + utility('my-opacity', 'opacity') + utility('my-order', 'order') + utility('my-outline-offset', 'outlineOffset') + utility('my-outline-width', 'outlineWidth') + utility('my-ring-offset-width', 'ringOffsetWidth') + utility('my-ring-width', 'ringWidth') + utility('my-rotate', 'rotate') + utility('my-saturate', 'saturate') + utility('my-scale', 'scale') + utility('my-sepia', 'sepia') + utility('my-skew', 'skew') + utility('my-stroke-width', 'strokeWidth') + utility('my-text-decoration-thickness', 'textDecorationThickness') + utility('my-text-underline-offset', 'textUnderlineOffset') + utility('my-transition-delay', 'transitionDelay') + utility('my-transition-duration', 'transitionDuration') + utility('my-z-index', 'zIndex') + }), + } + }, + }) + + let output = build([ + 'my-aspect-2/5', + // 'my-backdrop-brightness-1', + // 'my-backdrop-contrast-1', + // 'my-backdrop-grayscale-1', + // 'my-backdrop-hue-rotate-1', + // 'my-backdrop-invert-1', + // 'my-backdrop-opacity-1', + // 'my-backdrop-saturate-1', + // 'my-backdrop-sepia-1', + // 'my-divide-width-1', + 'my-border-width-1', + 'my-brightness-1', + 'my-columns-1', + 'my-contrast-1', + 'my-flex-grow-1', + 'my-flex-shrink-1', + 'my-gradient-color-stop-positions-1', + 'my-grayscale-1', + 'my-grid-row-end-1', + 'my-grid-row-start-1', + 'my-grid-template-columns-1', + 'my-grid-template-rows-1', + 'my-hue-rotate-1', + 'my-invert-1', + 'my-line-clamp-1', + 'my-opacity-1', + 'my-order-1', + 'my-outline-offset-1', + 'my-outline-width-1', + 'my-ring-offset-width-1', + 'my-ring-width-1', + 'my-rotate-1', + 'my-saturate-1', + 'my-scale-1', + 'my-sepia-1', + 'my-skew-1', + 'my-stroke-width-1', + 'my-text-decoration-thickness-1', + 'my-text-underline-offset-1', + 'my-transition-delay-1', + 'my-transition-duration-1', + 'my-z-index-1', + ]) + + expect(output).toMatchInlineSnapshot(` + ".my-aspect-2\\/5 { --value: 2/5; } - .my-border-width-1 { --value: 1px; } - .my-brightness-1 { --value: 1%; } - .my-columns-1 { --value: 1; } - .my-contrast-1 { --value: 1%; } - - .my-flex-grow-1, .my-flex-shrink-1 { + .my-flex-grow-1 { --value: 1; } - - .my-gradient-color-stop-positions-1, .my-grayscale-1 { + .my-flex-shrink-1 { + --value: 1; + } + .my-gradient-color-stop-positions-1 { --value: 1%; } - - .my-grid-row-end-1, .my-grid-row-start-1 { + .my-grayscale-1 { + --value: 1%; + } + .my-grid-row-end-1 { --value: 1; } - - .my-grid-template-columns-1, .my-grid-template-rows-1 { + .my-grid-row-start-1 { + --value: 1; + } + .my-grid-template-columns-1 { + --value: repeat(1, minmax(0, 1fr)); + } + .my-grid-template-rows-1 { --value: repeat(1, minmax(0, 1fr)); } - .my-hue-rotate-1 { --value: 1deg; } - .my-invert-1 { --value: 1%; } - .my-line-clamp-1 { --value: 1; } - .my-opacity-1 { --value: 1%; } - .my-order-1 { --value: 1; } - - .my-outline-offset-1, .my-outline-width-1, .my-ring-offset-width-1, .my-ring-width-1 { + .my-outline-offset-1 { + --value: 1px; + } + .my-outline-width-1 { + --value: 1px; + } + .my-ring-offset-width-1 { + --value: 1px; + } + .my-ring-width-1 { --value: 1px; } - .my-rotate-1 { --value: 1deg; } - - .my-saturate-1, .my-scale-1, .my-sepia-1 { + .my-saturate-1 { + --value: 1%; + } + .my-scale-1 { + --value: 1%; + } + .my-sepia-1 { --value: 1%; } - .my-skew-1 { --value: 1deg; } - .my-stroke-width-1 { --value: 1; } - - .my-text-decoration-thickness-1, .my-text-underline-offset-1 { + .my-text-decoration-thickness-1 { --value: 1px; } - - .my-transition-delay-1, .my-transition-duration-1 { + .my-text-underline-offset-1 { + --value: 1px; + } + .my-transition-delay-1 { + --value: 1ms; + } + .my-transition-duration-1 { --value: 1ms; } - .my-z-index-1 { --value: 1; } " `) }) - - test('can use escaped JS variables in theme values', async () => { - expect( - await run( - ['my-width-1', 'my-width-1/2', 'my-width-1.5'], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { 'my-width': (value) => ({ width: value }) }, - { values: theme('width') }, - ) - }, - { - theme: { - extend: { - width: { - '1': '0.25rem', - // Purposely setting to something different from the v3 default - '1/2': '60%', - '1.5': '0.375rem', - }, - }, - }, - }, - ), - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .my-width-1 { - width: .25rem; - } - - .my-width-1\\.5 { - width: .375rem; - } - - .my-width-1\\/2 { - width: 60%; - } - " - `) - }) - - test('can use escaped CSS variables in theme values', async () => { - expect( - await run( - ['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5'], - css` - @tailwind utilities; - @plugin "my-plugin"; - - @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; - } - - .my-width-1\\.5 { - width: .375rem; - } - - .my-width-1\\/2 { - width: 60%; - } - - .my-width-2\\.5 { - width: .625rem; - } - " - `) - }) - - test('can use escaped CSS variables in referenced theme namespace', async () => { - expect( - await run( - ['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5'], - css` - @tailwind utilities; - @plugin "my-plugin"; - - @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; - } - - .my-width-1\\.5 { - width: .375rem; - } - - .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 () => { - 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; - } - } - " - `) - }) - - test('does not modify CSS variables', async () => { - expect( - await compileCss( - css` - @plugin "my-plugin"; - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: plugin(function ({ addBase }) { - addBase({ ':root': { '--PascalCase': '1', '--camelCase': '1', '--UPPERCASE': '1' } }) - }), - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { - :root { - --PascalCase: 1; - --camelCase: 1; - --UPPERCASE: 1; - } - } - " - `) - }) - - test('@variant is replaced inside rules using addBase', async () => { - expect( - await compileCss( - css` - @plugin "my-plugin"; - @theme { - --breakpoint-supertiny: 128px; - } - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: plugin(function ({ addBase }) { - addBase({ - ':root': { - '@variant supertiny': { - '--PascalCase': '1', - '--camelCase': '1', - '--UPPERCASE': '1', - }, - '@variant data-enabled:focus, disabled': { - '--x': '1', - }, - }, - }) - }), - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { - @media (min-width: 128px) { - :root { - --PascalCase: 1; - --camelCase: 1; - --UPPERCASE: 1; - } - } - - :root[data-enabled]:focus, :root:disabled { - --x: 1; - } - } - " - `) - }) - - test('@variant inside addBase works with custom variants', async () => { - expect( - await compileCss( - css` - @plugin "my-plugin"; - - @custom-variant custom (&.custom); - `, - { - loadModule: async () => ({ - path: '', - base: '/root', - module: plugin(function ({ addBase }) { - addBase({ - ':root': { - '@variant custom': { - '--x': '1', - }, - }, - }) - }), - }), - }, - ), - ).toMatchInlineSnapshot(` - " - @layer base { - :root.custom { - --x: 1; - } - } - " - `) - }) }) describe('addVariant', () => { test('addVariant with string selector', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', '&:hover, &:focus') + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', '&:hover, &:focus') - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) { + }, + ) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " - `) - }) - - test('addVariant with `@scope`', async () => { - expect( - await run( - ['blue:underline', 'green:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('blue', '@scope ([data-theme=blue]) to ([data-theme])') - addVariant('green', '@scope ([data-theme=green]) to ([data-theme]) { & }') - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope ([data-theme="blue"]) to ([data-theme]) { - .blue\\:underline { - text-decoration-line: underline; - } - } - - @scope ([data-theme="green"]) to ([data-theme]) { - .green\\:underline { - text-decoration-line: underline; - } - } - } - " - `) - }) - - test('matchVariant with `@scope`', async () => { - expect( - await run( - ['theme-[blue]:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('theme', (value) => `@scope ([data-theme=${value}]) to ([data-theme])`) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @scope ([data-theme="blue"]) to ([data-theme]) { - .theme-\\[blue\\]\\:underline { - text-decoration-line: underline; - } - } - } - " + }" `) }) test('addVariant with array of selectors', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', ['&:hover', '&:focus']) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', ['&:hover', '&:focus']) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) { + }, + ) + + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('addVariant with object syntax and @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '&:hover': '@slot', + '&:focus': '@slot', + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&:hover': '@slot', - '&:focus': '@slot', - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - :is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) { + }, + ) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) { display: flex; } .hocus\\:underline:hover, .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('addVariant with object syntax, media, nesting and multiple @slot', async () => { - expect( - await run( - ['hocus:underline', 'group-hocus:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '@media (hover: hover)': { + '&:hover': '@slot', + }, + '&:focus': '@slot', + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '@media (hover: hover)': { - '&:hover': '@slot', - }, - '&:focus': '@slot', - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['hocus:underline', 'group-hocus:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (hover: hover) { - :is(:where(.group):hover .group-hocus\\:flex) { + .group-hocus\\:flex:is(:where(.group):hover *) { display: flex; } } - :is(:where(.group):focus .group-hocus\\:flex) { + .group-hocus\\:flex:is(:where(.group):focus *) { display: flex; } @@ -1845,86 +1377,84 @@ describe('addVariant', () => { .hocus\\:underline:focus { text-decoration-line: underline; } - } - " + }" `) }) test('addVariant with at-rules and placeholder', async () => { - expect( - await run( - ['potato:underline', 'potato:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant( + 'potato', + '@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }', + ) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant( - 'potato', - '@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }', - ) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (max-width: 400px) { + }, + ) + let compiled = build(['potato:underline', 'potato:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + @media (width <= 400px) { @supports (font: bold) { .potato\\:flex:large-potato { display: flex; } + } + } + @media (width <= 400px) { + @supports (font: bold) { .potato\\:underline:large-potato { text-decoration-line: underline; } } } - } - " + }" `) }) test('@slot is preserved when used as a custom property value', async () => { - expect( - await run( - ['hocus:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + 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', + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&': { - '--custom-property': '@slot', - '&:hover': '@slot', - '&:focus': '@slot', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['hocus:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .hocus\\:underline { --custom-property: @slot; } @@ -1932,73 +1462,35 @@ 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 () => { - expect( - await run( - ['potato-[yellow]:underline', 'potato-[baked]:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `.potato-${flavor} &`) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `.potato-${flavor} &`) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['potato-[yellow]:underline', 'potato-[baked]:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .potato-baked .potato-\\[baked\\]\\:flex { display: flex; } @@ -2006,36 +1498,33 @@ describe('matchVariant', () => { .potato-yellow .potato-\\[yellow\\]\\:underline { text-decoration-line: underline; } - } - " + }" `) }) test('partial arbitrary variants with at-rules', async () => { - expect( - await run( - ['potato-[yellow]:underline', 'potato-[baked]:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['potato-[yellow]:underline', 'potato-[baked]:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (potato: baked) { .potato-\\[baked\\]\\:flex { display: flex; @@ -2047,38 +1536,37 @@ describe('matchVariant', () => { text-decoration-line: underline; } } - } - " + }" `) }) test('partial arbitrary variants with at-rules and placeholder', async () => { - expect( - await run( - ['potato-[yellow]:underline', 'potato-[baked]:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant( + 'potato', + (flavor) => + `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }`, + ) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => { - return `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }` - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['potato-[yellow]:underline', 'potato-[baked]:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { @media (potato: baked) { @supports (font: bold) { .potato-\\[baked\\]\\:flex:large-potato { @@ -2094,41 +1582,38 @@ describe('matchVariant', () => { } } } - } - " + }" `) }) test('partial arbitrary variants with default values', async () => { - expect( - await run( - ['tooltip-bottom:underline', 'tooltip-top:flex'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('tooltip', (side) => `&${side}`, { + values: { + bottom: '[data-location="bottom"]', + top: '[data-location="top"]', + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('tooltip', (side) => `&${side}`, { - values: { - bottom: '[data-location="bottom"]', - top: '[data-location="top"]', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['tooltip-bottom:underline', 'tooltip-top:flex']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .tooltip-bottom\\:underline[data-location="bottom"] { text-decoration-line: underline; } @@ -2136,1096 +1621,947 @@ describe('matchVariant', () => { .tooltip-top\\:flex[data-location="top"] { display: flex; } - } - " + }" `) }) test('matched variant values maintain the sort order they are registered in', async () => { - expect( - await run( - [ - 'alphabet-c:underline', - 'alphabet-a:underline', - 'alphabet-d:underline', - 'alphabet-b:underline', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('alphabet', (side) => `&${side}`, { + values: { + d: '[data-order="1"]', + a: '[data-order="2"]', + c: '[data-order="3"]', + b: '[data-order="4"]', + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('alphabet', (side) => `&${side}`, { - values: { - d: '[data-order="1"]', - a: '[data-order="2"]', - c: '[data-order="3"]', - b: '[data-order="4"]', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - .alphabet-d\\:underline[data-order="1"], .alphabet-a\\:underline[data-order="2"], .alphabet-c\\:underline[data-order="3"], .alphabet-b\\:underline[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 { + .alphabet-d\\:underline[data-order="1"] { text-decoration-line: underline; } - } - " + + .alphabet-a\\:underline[data-order="2"] { + text-decoration-line: underline; + } + + .alphabet-c\\:underline[data-order="3"] { + text-decoration-line: underline; + } + + .alphabet-b\\:underline[data-order="4"] { + text-decoration-line: underline; + } + }" `) }) test('matchVariant can return an array of format strings from the function', async () => { - expect( - await run( - ['test-[a,b,c]:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('test', (selector) => + selector.split(',').map((selector) => `&.${selector} > *`), + ) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('test', (selector) => - selector.split(',').map((selector) => `&.${selector} > *`), - ) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['test-[a,b,c]:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .test-\\[a\\,b\\,c\\]\\:underline.a > *, .test-\\[a\\,b\\,c\\]\\:underline.b > *, .test-\\[a\\,b\\,c\\]\\:underline.c > * { text-decoration-line: underline; } - } - " + }" `) }) test('should be possible to sort variants', async () => { - expect( - await run( - ['testmin-[600px]:flex', 'testmin-[500px]:underline', 'testmin-[700px]:italic'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (min-width: 500px) { + }, + ) + let compiled = build([ + 'testmin-[600px]:flex', + 'testmin-[500px]:underline', + 'testmin-[700px]:italic', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + @media (width >= 500px) { .testmin-\\[500px\\]\\:underline { text-decoration-line: underline; } } - @media (min-width: 600px) { + @media (width >= 600px) { .testmin-\\[600px\\]\\:flex { display: flex; } } - @media (min-width: 700px) { + @media (width >= 700px) { .testmin-\\[700px\\]\\:italic { font-style: italic; } } - } - " + }" `) }) test('should be possible to compare arbitrary variants and hardcoded variants', async () => { - expect( - await run( - ['testmin-[700px]:italic', 'testmin-example:italic', 'testmin-[500px]:italic'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + values: { + example: '600px', + }, + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - values: { example: '600px' }, - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (min-width: 500px) { + }, + ) + let compiled = build([ + 'testmin-[700px]:italic', + 'testmin-example:italic', + 'testmin-[500px]:italic', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + @media (width >= 500px) { .testmin-\\[500px\\]\\:italic { font-style: italic; } } - @media (min-width: 600px) { + @media (width >= 600px) { .testmin-example\\:italic { font-style: italic; } } - @media (min-width: 700px) { + @media (width >= 700px) { .testmin-\\[700px\\]\\:italic { font-style: italic; } } - } - " + }" `) }) test('should be possible to sort stacked arbitrary variants correctly', async () => { - expect( - await run( - [ - 'testmin-[150px]:testmax-[400px]:order-2', - 'testmin-[100px]:testmax-[350px]:order-3', - 'testmin-[100px]:testmax-[300px]:order-4', - 'testmin-[100px]:testmax-[400px]:order-1', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) - }, - } - }, + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (min-width: 100px) { - @media (max-width: 400px) { + }, + ) + + let compiled = build([ + 'testmin-[150px]:testmax-[400px]:order-2', + 'testmin-[100px]:testmax-[350px]:order-3', + 'testmin-[100px]:testmax-[300px]:order-4', + 'testmin-[100px]:testmax-[400px]:order-1', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + @media (width >= 100px) { + @media (width <= 400px) { .testmin-\\[100px\\]\\:testmax-\\[400px\\]\\:order-1 { order: 1; } } } - @media (min-width: 150px) { - @media (max-width: 400px) { + @media (width >= 150px) { + @media (width <= 400px) { .testmin-\\[150px\\]\\:testmax-\\[400px\\]\\:order-2 { order: 2; } } } - @media (min-width: 100px) { - @media (max-width: 350px) { + @media (width >= 100px) { + @media (width <= 350px) { .testmin-\\[100px\\]\\:testmax-\\[350px\\]\\:order-3 { order: 3; } } + } - @media (max-width: 300px) { + @media (width >= 100px) { + @media (width <= 300px) { .testmin-\\[100px\\]\\:testmax-\\[300px\\]\\:order-4 { order: 4; } } } - } - " + }" `) }) test('should maintain sort from other variants, if sort functions of arbitrary variants return 0', async () => { - // Expect :focus to come after :hover - expect( - await run( - [ - 'testmin-[100px]:testmax-[200px]:focus:underline', - 'testmin-[100px]:testmax-[200px]:hover:underline', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) - }, - } - }, + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (min-width: 100px) { - @media (max-width: 200px) { + }, + ) + let compiled = build([ + 'testmin-[100px]:testmax-[200px]:focus:underline', + 'testmin-[100px]:testmax-[200px]:hover:underline', + ]) + + // Expect :focus to come after :hover + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + @media (width >= 100px) { + @media (width <= 200px) { @media (hover: hover) { .testmin-\\[100px\\]\\:testmax-\\[200px\\]\\:hover\\:underline:hover { text-decoration-line: underline; } } + } + } + @media (width >= 100px) { + @media (width <= 200px) { .testmin-\\[100px\\]\\:testmax-\\[200px\\]\\:focus\\:underline:focus { text-decoration-line: underline; } } } - } - " + }" `) }) test('should sort arbitrary variants left to right (1)', async () => { - expect( - await run( - [ - 'testmin-[200px]:testmax-[400px]:order-2', - 'testmin-[200px]:testmax-[300px]:order-4', - 'testmin-[100px]:testmax-[400px]:order-1', - 'testmin-[100px]:testmax-[300px]:order-3', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (min-width: 100px) { - @media (max-width: 400px) { + }, + ) + let compiled = build([ + 'testmin-[200px]:testmax-[400px]:order-2', + 'testmin-[200px]:testmax-[300px]:order-4', + 'testmin-[100px]:testmax-[400px]:order-1', + 'testmin-[100px]:testmax-[300px]:order-3', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + @media (width >= 100px) { + @media (width <= 400px) { .testmin-\\[100px\\]\\:testmax-\\[400px\\]\\:order-1 { order: 1; } } } - @media (min-width: 200px) { - @media (max-width: 400px) { + @media (width >= 200px) { + @media (width <= 400px) { .testmin-\\[200px\\]\\:testmax-\\[400px\\]\\:order-2 { order: 2; } } } - @media (min-width: 100px) { - @media (max-width: 300px) { + @media (width >= 100px) { + @media (width <= 300px) { .testmin-\\[100px\\]\\:testmax-\\[300px\\]\\:order-3 { order: 3; } } } - @media (min-width: 200px) { - @media (max-width: 300px) { + @media (width >= 200px) { + @media (width <= 300px) { .testmin-\\[200px\\]\\:testmax-\\[300px\\]\\:order-4 { order: 4; } } } - } - " + }" `) }) test('should sort arbitrary variants left to right (2)', async () => { - expect( - await run( - [ - 'testmax-[400px]:testmin-[200px]:underline', - 'testmax-[300px]:testmin-[200px]:underline', - 'testmax-[400px]:testmin-[100px]:underline', - 'testmax-[300px]:testmin-[100px]:underline', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (max-width: 400px) { - @media (min-width: 100px) { + }, + ) + let compiled = build([ + 'testmax-[400px]:testmin-[200px]:underline', + 'testmax-[300px]:testmin-[200px]:underline', + 'testmax-[400px]:testmin-[100px]:underline', + 'testmax-[300px]:testmin-[100px]:underline', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + @media (width <= 400px) { + @media (width >= 100px) { .testmax-\\[400px\\]\\:testmin-\\[100px\\]\\:underline { text-decoration-line: underline; } } + } - @media (min-width: 200px) { + @media (width <= 400px) { + @media (width >= 200px) { .testmax-\\[400px\\]\\:testmin-\\[200px\\]\\:underline { text-decoration-line: underline; } } } - @media (max-width: 300px) { - @media (min-width: 100px) { + @media (width <= 300px) { + @media (width >= 100px) { .testmax-\\[300px\\]\\:testmin-\\[100px\\]\\:underline { text-decoration-line: underline; } } + } - @media (min-width: 200px) { + @media (width <= 300px) { + @media (width >= 200px) { .testmax-\\[300px\\]\\:testmin-\\[200px\\]\\:underline { text-decoration-line: underline; } } } - } - " + }" `) }) test('should guarantee that we are not passing values from other variants to the wrong function', async () => { - expect( - await run( - [ - 'testmin-[200px]:testmax-[400px]:order-2', - 'testmin-[200px]:testmax-[300px]:order-4', - 'testmin-[100px]:testmax-[400px]:order-1', - 'testmin-[100px]:testmax-[300px]:order-3', - ], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + let lookup = ['100px', '200px'] + if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { + throw new Error('We are seeing values that should not be there!') + } + return lookup.indexOf(a.value) - lookup.indexOf(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + let lookup = ['300px', '400px'] + if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { + throw new Error('We are seeing values that should not be there!') + } + return lookup.indexOf(z.value) - lookup.indexOf(a.value) + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - let lookup = ['100px', '200px'] - if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { - throw new Error('We are seeing values that should not be there!') - } - return lookup.indexOf(a.value) - lookup.indexOf(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - let lookup = ['300px', '400px'] - if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { - throw new Error('We are seeing values that should not be there!') - } - return lookup.indexOf(z.value) - lookup.indexOf(a.value) - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - @media (min-width: 100px) { - @media (max-width: 400px) { + }, + ) + let compiled = build([ + 'testmin-[200px]:testmax-[400px]:order-2', + 'testmin-[200px]:testmax-[300px]:order-4', + 'testmin-[100px]:testmax-[400px]:order-1', + 'testmin-[100px]:testmax-[300px]:order-3', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + @media (width >= 100px) { + @media (width <= 400px) { .testmin-\\[100px\\]\\:testmax-\\[400px\\]\\:order-1 { order: 1; } } } - @media (min-width: 200px) { - @media (max-width: 400px) { + @media (width >= 200px) { + @media (width <= 400px) { .testmin-\\[200px\\]\\:testmax-\\[400px\\]\\:order-2 { order: 2; } } } - @media (min-width: 100px) { - @media (max-width: 300px) { + @media (width >= 100px) { + @media (width <= 300px) { .testmin-\\[100px\\]\\:testmax-\\[300px\\]\\:order-3 { order: 3; } } } - @media (min-width: 200px) { - @media (max-width: 300px) { + @media (width >= 200px) { + @media (width <= 300px) { .testmin-\\[200px\\]\\:testmax-\\[300px\\]\\:order-4 { order: 4; } } } - } - " + }" `) }) test('should default to the DEFAULT value for variants', async () => { - expect( - await run( - ['foo:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value} &`, { + values: { + DEFAULT: '.bar', + }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`, { - values: { DEFAULT: '.bar' }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['foo:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo.bar .foo\\:underline { text-decoration-line: underline; } - } - " + }" `) }) test('should not generate anything if the matchVariant does not have a DEFAULT value configured', async () => { - expect( - await run( - ['foo:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value} &`) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities; - " - `) + }, + ) + let compiled = build(['foo:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`"@layer utilities;"`) }) test('should be possible to use `null` as a DEFAULT value', async () => { - expect( - await run( - ['foo:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { + values: { DEFAULT: null }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { - values: { DEFAULT: null }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['foo:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo-good .foo\\:underline { text-decoration-line: underline; } - } - " + }" `) }) test('should be possible to use `undefined` as a DEFAULT value', async () => { - expect( - await run( - ['foo:underline'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { + values: { DEFAULT: undefined }, + }) + }, } - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { - values: { DEFAULT: undefined }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + let compiled = build(['foo:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo-good .foo\\:underline { text-decoration-line: underline; } - } - " + }" `) }) test('should be called with eventual modifiers', async () => { - expect( - await run( - ['my-container-[250px]:underline', 'my-container-[250px]/placement:underline'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: ({ matchVariant }: PluginAPI) => { - matchVariant('my-container', (value, { modifier }) => { - function parseValue(value: string) { - const numericValue = value.match(/^(\d+\.\d+|\d+|\.\d+)\D+/)?.[1] ?? null - if (numericValue === null) return null + let { build } = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('my-container', (value, { modifier }) => { + function parseValue(value: string) { + const numericValue = value.match(/^(\d+\.\d+|\d+|\.\d+)\D+/)?.[1] ?? null + if (numericValue === null) return null - return parseFloat(value) - } + return parseFloat(value) + } - const parsed = parseValue(value) - return parsed !== null ? `@container ${modifier ?? ''} (min-width: ${value})` : [] - }) - }, - } - }, + const parsed = parseValue(value) + return parsed !== null ? `@container ${modifier ?? ''} (min-width: ${value})` : [] + }) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - @container (min-width: 250px) { + }, + ) + let compiled = build([ + 'my-container-[250px]:underline', + 'my-container-[250px]/placement:underline', + ]) + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@container (width >= 250px) { .my-container-\\[250px\\]\\:underline { text-decoration-line: underline; } } - @container placement (min-width: 250px) { + @container placement (width >= 250px) { .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 () => { - expect( - await run( - ['text-trim', 'lg:text-trim'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } - @theme reference { - --breakpoint-lg: 1024px; + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, + }) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + + expect(optimizeCss(compiled.build(['text-trim', 'lg:text-trim'])).trim()) + .toMatchInlineSnapshot(` + "@layer utilities { .text-trim { text-box-trim: both; text-box-edge: cap alphabetic; } - @media (min-width: 1024px) { + @media (width >= 1024px) { .lg\\:text-trim { text-box-trim: both; text-box-edge: cap alphabetic; } } - } - " + }" `) }) test('return multiple rule objects from a custom utility', async () => { - expect( - await run( - ['text-trim'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.text-trim': [ - { 'text-box-trim': 'both' }, - { 'text-box-edge': 'cap alphabetic' }, - ], - }, - ]) - }, - } - }, + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.text-trim': [ + { 'text-box-trim': 'both' }, + { 'text-box-edge': 'cap alphabetic' }, + ], + }, + ]) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .text-trim { + }, + ) + + expect(optimizeCss(compiled.build(['text-trim'])).trim()).toMatchInlineSnapshot(` + ".text-trim { text-box-trim: both; text-box-edge: cap alphabetic; - } - " + }" `) }) test('define multiple utilities with array syntax', async () => { - expect( - await run( - ['text-trim', 'text-trim-2'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.text-trim': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.text-trim': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', }, - { - '.text-trim-2': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, + }, + { + '.text-trim-2': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', }, - ]) - }, - } - }, + }, + ]) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .text-trim, .text-trim-2 { + }, + ) + + expect(optimizeCss(compiled.build(['text-trim', 'text-trim-2'])).trim()).toMatchInlineSnapshot(` + ".text-trim, .text-trim-2 { text-box-trim: both; text-box-edge: cap alphabetic; - } - " + }" `) }) test('utilities can use arrays for fallback declaration values', async () => { - expect( - await run( - ['outlined'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.outlined': { - outline: ['1px solid ButtonText', '1px auto -webkit-focus-ring-color'], - }, + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.outlined': { + outline: ['1px solid ButtonText', '1px auto -webkit-focus-ring-color'], }, - ]) - }, - } - }, + }, + ]) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .outlined { + }, + ) + + expect(optimizeCss(compiled.build(['outlined'])).trim()).toMatchInlineSnapshot(` + ".outlined { outline: 1px solid buttontext; outline: 1px auto -webkit-focus-ring-color; - } - " + }" `) }) test('camel case properties are converted to kebab-case', async () => { - expect( - await run( - ['text-trim'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim': { + WebkitAppearance: 'none', + textBoxTrim: 'both', + textBoxEdge: 'cap alphabetic', + }, + }) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim': { - WebkitAppearance: 'none', - textBoxTrim: 'both', - textBoxEdge: 'cap alphabetic', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + + expect(optimizeCss(compiled.build(['text-trim'])).trim()).toMatchInlineSnapshot(` + "@layer utilities { .text-trim { -webkit-appearance: none; text-box-trim: both; text-box-edge: cap alphabetic; } - } - " + }" `) }) test('custom static utilities support `@apply`', async () => { - expect( - await run( - ['foo', 'lg:foo'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } - @theme reference { - --breakpoint-lg: 1024px; + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.foo': { + '@apply flex dark:underline': {}, + }, + }) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.foo': { - '@apply flex dark:underline': {}, - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { + }, + ) + + expect(optimizeCss(compiled.build(['foo', 'lg:foo'])).trim()).toMatchInlineSnapshot(` + "@layer utilities { .foo { display: flex; } @@ -3236,7 +2572,7 @@ describe('addUtilities()', () => { } } - @media (min-width: 1024px) { + @media (width >= 1024px) { .lg\\:foo { display: flex; } @@ -3247,8 +2583,7 @@ describe('addUtilities()', () => { } } } - } - " + }" `) }) @@ -3266,13 +2601,12 @@ describe('addUtilities()', () => { } `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ - ':hover > *': { + '.text-trim > *': { 'text-box-trim': 'both', 'text-box-edge': 'cap alphabetic', }, @@ -3282,336 +2616,148 @@ describe('addUtilities()', () => { }, }, ) - }).rejects.toThrow(/invalid utility selector/) + }).rejects.toThrowError(/invalid utility selector/) }) test('supports multiple selector names', async () => { - expect( - await run( - ['form-input', 'lg:form-textarea'], - css` - @plugin "my-plugin"; - @tailwind utilities; + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.form-input, .form-textarea': { + appearance: 'none', + 'background-color': '#fff', + }, + }) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-textarea': { - appearance: 'none', - 'background-color': '#fff', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - .form-input { - appearance: none; - background-color: #fff; - } + }, + ) - @media (min-width: 1024px) { - .lg\\:form-textarea { + expect(optimizeCss(compiled.build(['form-input', 'lg:form-textarea'])).trim()) + .toMatchInlineSnapshot(` + ".form-input { appearance: none; background-color: #fff; } - } - " - `) + + @media (width >= 1024px) { + .lg\\:form-textarea { + appearance: none; + background-color: #fff; + } + }" + `) }) test('supports pseudo classes and pseudo elements', async () => { - expect( - await run( - ['form-input', 'lg:form-textarea'], - css` - @plugin "my-plugin"; - @tailwind utilities; + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.form-input, .form-input::placeholder, .form-textarea:hover:focus': { + 'background-color': 'red', + }, + }) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-input::placeholder, .form-textarea:hover:focus': { - 'background-color': 'red', - }, - }) - }, - } - }, }, - ), - ).toMatchInlineSnapshot(` - " - .form-input, .form-input::placeholder { - background-color: red; - } + }, + ) - @media (min-width: 1024px) { - .lg\\:form-textarea:hover:focus { + expect(optimizeCss(compiled.build(['form-input', 'lg:form-textarea'])).trim()) + .toMatchInlineSnapshot(` + ".form-input { 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; + .form-input::placeholder { + background-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; + @media (width >= 1024px) { + .lg\\:form-textarea:hover:focus { + background-color: red; } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.foo:nth-child(2 of .mark)': { color: 'red' }, - }) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - @layer utilities { - .foo:nth-child(2 of .mark) { - color: red; - } - } - " - `) - }) - - 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"; + async function run(candidates: string[]) { + let compiled = await compile( + css` + @plugin "my-plugin"; - @tailwind utilities; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; - } - ` + @theme reference { + --breakpoint-lg: 1024px; + } + `, - expect( - await run( - [ - 'border-block', - 'border-block-2', - 'border-block-[35px]', - 'border-block-[var(--foo)]', - 'lg:border-block-2', - ], - input, { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( - { 'border-block': (value) => ({ 'border-block-width': value }) }, - { values: { DEFAULT: '1px', '2': '2px' } }, + { + 'border-block': (value) => ({ 'border-block-width': value }), + }, + { + values: { + DEFAULT: '1px', + '2': '2px', + }, + }, ) }, } }, }, - ), + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run([ + 'border-block', + 'border-block-2', + 'border-block-[35px]', + 'border-block-[var(--foo)]', + 'lg:border-block-2', + ]), + ).trim(), ).toMatchInlineSnapshot(` - " - .border-block { + ".border-block { border-block-width: 1px; } @@ -3627,122 +2773,71 @@ describe('matchUtilities()', () => { border-block-width: var(--foo); } - @media (min-width: 1024px) { + @media (width >= 1024px) { .lg\\:border-block-2 { border-block-width: 2px; } - } - " + }" `) expect( - await run( - [ + optimizeCss( + await run([ '-border-block', '-border-block-2', 'lg:-border-block-2', 'border-block-unknown', 'border-block/1', - ], - input, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { 'border-block': (value) => ({ 'border-block-width': value }) }, - { values: { DEFAULT: '1px', '2': '2px' } }, - ) - }, - } - }, - }, - ), + ]), + ).trim(), ).toEqual('') }) - test('custom functional utilities can start with @', async () => { - expect( - await run( - ['@w-1', 'hover:@w-1'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities({ '@w': (value) => ({ width: value }) }, { values: { 1: '1px' } }) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .\\@w-1 { - 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( - { - 'all-but-order-bottom-left-radius': (value) => - [ - { 'border-top-left-radius': value }, - { 'border-top-right-radius': value }, - { 'border-bottom-right-radius': value }, - ] as CssInJs[], + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'all-but-order-bottom-left-radius': (value) => + [ + { 'border-top-left-radius': value }, + { 'border-top-right-radius': value }, + { 'border-bottom-right-radius': value }, + ] as CssInJs[], + }, + { + values: { + DEFAULT: '1px', }, - { values: { DEFAULT: '1px' } }, - ) - }, - } - }, + }, + ) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .all-but-order-bottom-left-radius { - border-top-left-radius: 1px; - border-top-right-radius: 1px; - border-bottom-right-radius: 1px; - } - " - `) + }, + ) + + expect(optimizeCss(compiled.build(['all-but-order-bottom-left-radius'])).trim()) + .toMatchInlineSnapshot(` + ".all-but-order-bottom-left-radius { + border-top-left-radius: 1px; + border-top-right-radius: 1px; + border-bottom-right-radius: 1px; + }" + `) }) test('custom functional utility with any modifier', async () => { - expect( - await run( - ['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo'], + async function run(candidates: string[]) { + let compiled = await compile( css` @plugin "my-plugin"; @@ -3752,10 +2847,10 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3765,16 +2860,30 @@ describe('matchUtilities()', () => { 'border-block-width': value, }), }, - { values: { DEFAULT: '1px', '2': '2px' }, modifiers: 'any' }, + { + values: { + DEFAULT: '1px', + '2': '2px', + }, + + modifiers: 'any', + }, ) }, } }, }, - ), + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']), + ).trim(), ).toMatchInlineSnapshot(` - " - .border-block { + ".border-block { --my-modifier: none; border-block-width: 1px; } @@ -3792,30 +2901,26 @@ describe('matchUtilities()', () => { .border-block\\/foo { --my-modifier: foo; border-block-width: 1px; - } - " + }" `) }) test('custom functional utility with known modifier', async () => { - let input = css` - @plugin "my-plugin"; + async function run(candidates: string[]) { + let compiled = await compile( + css` + @plugin "my-plugin"; - @tailwind utilities; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; - } - ` + @theme reference { + --breakpoint-lg: 1024px; + } + `, - expect( - await run( - ['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo'], - input, { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3826,18 +2931,31 @@ describe('matchUtilities()', () => { }), }, { - values: { DEFAULT: '1px', '2': '2px' }, - modifiers: { foo: 'foo' }, + values: { + DEFAULT: '1px', + '2': '2px', + }, + + modifiers: { + foo: 'foo', + }, }, ) }, } }, }, - ), + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']), + ).trim(), ).toMatchInlineSnapshot(` - " - .border-block { + ".border-block { --my-modifier: none; border-block-width: 1px; } @@ -3855,33 +2973,11 @@ describe('matchUtilities()', () => { .border-block\\/foo { --my-modifier: foo; border-block-width: 1px; - } - " + }" `) expect( - await run(['border-block/unknown', 'border-block-2/unknown'], input, { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value, { modifier }) => ({ - '--my-modifier': modifier ?? 'none', - 'border-block-width': value, - }), - }, - { - values: { DEFAULT: '1px', '2': '2px' }, - modifiers: { foo: 'foo' }, - }, - ) - }, - } - }, - }), + optimizeCss(await run(['border-block/unknown', 'border-block-2/unknown'])).trim(), ).toEqual('') }) @@ -3889,127 +2985,190 @@ describe('matchUtilities()', () => { // future don't be afraid to change what should happen in this scenario. describe('plugins that handle a specific arbitrary value type prevent falling through to other plugins if the result is invalid for that plugin', () => { test('implicit color modifier', async () => { - let input = css` - @tailwind utilities; - @plugin "my-plugin"; - ` - - let options: Partial[1]> = { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { type: ['color', 'any'] }, - ) - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-width': value }) }, - { type: ['length'] }, - ) - }, - } - }, - } - - expect( - await run(['scrollbar-[2px]', 'scrollbar-[#08c]', 'scrollbar-[#08c]/50'], input, options), - ).toMatchInlineSnapshot(` - " - .scrollbar-\\[2px\\] { - scrollbar-width: 2px; - } - - .scrollbar-\\[\\#08c\\] { - scrollbar-color: #08c; - } - - .scrollbar-\\[\\#08c\\]\\/50 { - scrollbar-color: oklab(59.9824% -.067 -.124 / .5); - } - " - `) - expect(await run(['scrollbar-[2px]/50'], input, options)).toEqual('') - }) - - test('no modifiers are supported by the plugins', async () => { - expect( - await run( - ['scrollbar-[2px]/50'], + async function run(candidates: string[]) { + let compiled = await compile( css` @tailwind utilities; @plugin "my-plugin"; `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( - { scrollbar: (value) => ({ '--scrollbar-angle': value }) }, - { type: ['angle', 'any'] }, + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { type: ['color', 'any'] }, ) matchUtilities( - { scrollbar: (value) => ({ '--scrollbar-width': value }) }, + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, { type: ['length'] }, ) }, } }, }, - ), - ).toEqual('') + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run(['scrollbar-[2px]', 'scrollbar-[#08c]', 'scrollbar-[#08c]/50']), + ).trim(), + ).toMatchInlineSnapshot(` + ".scrollbar-\\[\\#08c\\] { + scrollbar-color: #08c; + } + + .scrollbar-\\[\\#08c\\]\\/50 { + scrollbar-color: #0088cc80; + } + + .scrollbar-\\[2px\\] { + scrollbar-width: 2px; + }" + `) + expect(optimizeCss(await run(['scrollbar-[2px]/50'])).trim()).toEqual('') }) - test('invalid named modifier', async () => { - expect( - await run( - ['scrollbar-[2px]/foo'], + test('no modifiers are supported by the plugins', async () => { + async function run(candidates: string[]) { + let compiled = await compile( css` @tailwind utilities; @plugin "my-plugin"; `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, + { + scrollbar: (value) => ({ '--scrollbar-angle': value }), + }, + { type: ['angle', 'any'] }, + ) + + matchUtilities( + { + scrollbar: (value) => ({ '--scrollbar-width': value }), + }, + { type: ['length'] }, + ) + }, + } + }, + }, + ) + + return compiled.build(candidates) + } + + expect(optimizeCss(await run(['scrollbar-[2px]/50'])).trim()).toEqual('') + }) + + test('invalid named modifier', async () => { + async function run(candidates: string[]) { + let compiled = await compile( + css` + @tailwind utilities; + @plugin "my-plugin"; + `, + + { + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, { type: ['color', 'any'], modifiers: { foo: 'foo' } }, ) matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-width': value }) }, + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, { type: ['length'], modifiers: { bar: 'bar' } }, ) }, } }, }, - ), - ).toEqual('') + ) + + return compiled.build(candidates) + } + + expect(optimizeCss(await run(['scrollbar-[2px]/foo'])).trim()).toEqual('') }) }) test('custom functional utilities with different types', async () => { - let input = css` - @plugin "my-plugin"; + async function run(candidates: string[]) { + let compiled = await compile( + css` + @plugin "my-plugin"; - @tailwind utilities; + @tailwind utilities; - @theme reference { - --breakpoint-lg: 1024px; - } - ` + @theme reference { + --breakpoint-lg: 1024px; + } + `, + + { + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { + type: ['color', 'any'], + values: { + black: 'black', + }, + }, + ) + + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, + { + type: ['length'], + values: { + 2: '2px', + }, + }, + ) + }, + } + }, + }, + ) + + return compiled.build(candidates) + } expect( - await run( - [ + optimizeCss( + await run([ 'scrollbar-black', 'scrollbar-black/50', 'scrollbar-2', @@ -4021,64 +3180,43 @@ describe('matchUtilities()', () => { 'scrollbar-[color:var(--my-color)]', 'scrollbar-[color:var(--my-color)]/50', 'scrollbar-[length:var(--my-width)]', - ], - input, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { type: ['color', 'any'], values: { black: 'black' } }, - ) - - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-width': value }) }, - { type: ['length'], values: { 2: '2px' } }, - ) - }, - } - }, - }, - ), + ]), + ).trim(), ).toMatchInlineSnapshot(` - " - .scrollbar-2, .scrollbar-\\[2px\\] { + ".scrollbar-2 { scrollbar-width: 2px; } - .scrollbar-\\[length\\:var\\(--my-width\\)\\] { - scrollbar-width: var(--my-width); - } - .scrollbar-\\[\\#fff\\] { scrollbar-color: #fff; } .scrollbar-\\[\\#fff\\]\\/50 { - scrollbar-color: oklab(100% 0 5.96046e-8 / .5); + scrollbar-color: #ffffff80; } - .scrollbar-\\[color\\:var\\(--my-color\\)\\], .scrollbar-\\[color\\:var\\(--my-color\\)\\]\\/50 { + .scrollbar-\\[2px\\] { + scrollbar-width: 2px; + } + + .scrollbar-\\[color\\:var\\(--my-color\\)\\] { scrollbar-color: var(--my-color); } - @supports (color: color-mix(in lab, red, red)) { - .scrollbar-\\[color\\:var\\(--my-color\\)\\]\\/50 { - scrollbar-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } + .scrollbar-\\[color\\:var\\(--my-color\\)\\]\\/50 { + scrollbar-color: color-mix(in srgb, var(--my-color) 50%, transparent); } - .scrollbar-\\[var\\(--my-color\\)\\], .scrollbar-\\[var\\(--my-color\\)\\]\\/50 { + .scrollbar-\\[length\\:var\\(--my-width\\)\\] { + scrollbar-width: var(--my-width); + } + + .scrollbar-\\[var\\(--my-color\\)\\] { scrollbar-color: var(--my-color); } - @supports (color: color-mix(in lab, red, red)) { - .scrollbar-\\[var\\(--my-color\\)\\]\\/50 { - scrollbar-color: color-mix(in oklab, var(--my-color) 50%, transparent); - } + .scrollbar-\\[var\\(--my-color\\)\\]\\/50 { + scrollbar-color: color-mix(in srgb, var(--my-color) 50%, transparent); } .scrollbar-black { @@ -4086,49 +3224,24 @@ describe('matchUtilities()', () => { } .scrollbar-black\\/50 { - scrollbar-color: oklab(0% none none / .5); - } - " + scrollbar-color: #00000080; + }" `) expect( - await run( - ['scrollbar-2/50', 'scrollbar-[2px]/50', 'scrollbar-[length:var(--my-width)]/50'], - input, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { type: ['color', 'any'], values: { black: 'black' } }, - ) - - matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-width': value }) }, - { type: ['length'], values: { 2: '2px' } }, - ) - }, - } - }, - }, - ), + optimizeCss( + await run([ + 'scrollbar-2/50', + 'scrollbar-[2px]/50', + 'scrollbar-[length:var(--my-width)]/50', + ]), + ).trim(), ).toEqual('') }) test('functional utilities with `type: color` automatically support opacity', async () => { - expect( - await run( - [ - 'scrollbar-current', - 'scrollbar-current/45', - 'scrollbar-black', - 'scrollbar-black/33', - 'scrollbar-black/[50%]', - 'scrollbar-[var(--my-color)]/[25%]', - ], + async function run(candidates: string[]) { + let compiled = await compile( css` @plugin "my-plugin"; @@ -4138,31 +3251,46 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( - { scrollbar: (value) => ({ 'scrollbar-color': value }) }, - { type: ['color', 'any'], values: { black: 'black' } }, + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { + type: ['color', 'any'], + values: { + black: 'black', + }, + }, ) }, } }, }, - ), - ).toMatchInlineSnapshot(` - " - .scrollbar-\\[var\\(--my-color\\)\\]\\/\\[25\\%\\] { - scrollbar-color: var(--my-color); - } + ) - @supports (color: color-mix(in lab, red, red)) { - .scrollbar-\\[var\\(--my-color\\)\\]\\/\\[25\\%\\] { - scrollbar-color: color-mix(in oklab, var(--my-color) 25%, transparent); - } + 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-color: color-mix(in srgb, var(--my-color) 25%, transparent); } .scrollbar-black { @@ -4170,30 +3298,26 @@ describe('matchUtilities()', () => { } .scrollbar-black\\/33 { - scrollbar-color: oklab(0% none none / .33); + scrollbar-color: #00000054; } .scrollbar-black\\/\\[50\\%\\] { - scrollbar-color: oklab(0% none none / .5); + scrollbar-color: #00000080; } - .scrollbar-current, .scrollbar-current\\/45 { - scrollbar-color: currentcolor; + .scrollbar-current { + scrollbar-color: currentColor; } - @supports (color: color-mix(in lab, red, red)) { - .scrollbar-current\\/45 { - scrollbar-color: color-mix(in oklab, currentcolor 45%, transparent); - } - } - " + .scrollbar-current\\/45 { + scrollbar-color: color-mix(in srgb, currentColor 45%, transparent); + }" `) }) test('functional utilities with explicit modifiers', async () => { - expect( - await run( - ['scrollbar-[12px]', 'scrollbar-[12px]/foo', 'scrollbar-[12px]/bar'], + async function run(candidates: string[]) { + let compiled = await compile( css` @plugin "my-plugin"; @@ -4204,10 +3328,10 @@ describe('matchUtilities()', () => { --opacity-my-opacity: 0.5; } `, + { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -4217,16 +3341,29 @@ describe('matchUtilities()', () => { 'scrollbar-width': value, }), }, - { type: ['any'], values: {}, modifiers: { foo: 'foo' } }, + { + type: ['any'], + values: {}, + modifiers: { + foo: 'foo', + }, + }, ) }, } }, }, - ), + ) + + return compiled.build(candidates) + } + + expect( + optimizeCss( + await run(['scrollbar-[12px]', 'scrollbar-[12px]/foo', 'scrollbar-[12px]/bar']), + ).trim(), ).toMatchInlineSnapshot(` - " - .scrollbar-\\[12px\\] { + ".scrollbar-\\[12px\\] { --modifier: none; scrollbar-width: 12px; } @@ -4234,43 +3371,50 @@ describe('matchUtilities()', () => { .scrollbar-\\[12px\\]\\/foo { --modifier: foo; scrollbar-width: 12px; - } - " + }" `) }) test('functional utilities support `@apply`', async () => { - expect( - await run( - ['foo-bar', 'lg:foo-bar', 'foo-[12px]', 'lg:foo-[12px]'], - css` - @plugin "my-plugin"; - @layer utilities { - @tailwind utilities; - } + let compiled = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } - @theme reference { - --breakpoint-lg: 1024px; + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + foo: (value) => ({ + '--foo': value, + [`@apply flex`]: {}, + }), + }, + { + values: { + bar: 'bar', + }, + }, + ) + }, } - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { foo: (value) => ({ '--foo': value, [`@apply flex`]: {} }) }, - { values: { bar: 'bar' } }, - ) - }, - } - }, }, - ), + }, + ) + + expect( + optimizeCss(compiled.build(['foo-bar', 'lg:foo-bar', 'foo-[12px]', 'lg:foo-[12px]'])).trim(), ).toMatchInlineSnapshot(` - " - @layer utilities { + "@layer utilities { .foo-\\[12px\\] { --foo: 12px; display: flex; @@ -4281,19 +3425,20 @@ describe('matchUtilities()', () => { display: flex; } - @media (min-width: 1024px) { + @media (width >= 1024px) { .lg\\:foo-\\[12px\\] { --foo: 12px; display: flex; } + } + @media (width >= 1024px) { .lg\\:foo-bar { --foo: bar; display: flex; } } - } - " + }" `) }) @@ -4312,9 +3457,8 @@ describe('matchUtilities()', () => { `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities({ @@ -4328,103 +3472,52 @@ describe('matchUtilities()', () => { }, }, ) - }).rejects.toThrow(/invalid utility name/) - }) - - test('replaces the class name with variants in nested selectors', async () => { - expect( - await run( - ['foo-red', 'md:foo-red', 'not-hover:md:foo-red'], - css` - @plugin "my-plugin"; - @theme { - --breakpoint-md: 768px; - } - @tailwind utilities; - `, - { - async loadModule(base) { - return { - path: '', - base, - module: ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { foo: (value) => ({ ':where(.foo > :first-child)': { color: value } }) }, - { values: { red: 'red' } }, - ) - }, - } - }, - }, - ), - ).toMatchInlineSnapshot(` - " - .foo-red :where(.foo-red > :first-child) { - color: red; - } - - @media (min-width: 768px) { - .md\\:foo-red :where(.md\\:foo-red > :first-child), .not-hover\\:md\\:foo-red:not(:hover) :where(.not-hover\\:md\\:foo-red > :first-child) { - color: red; - } - } - - @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; - } - } - } - " - `) + }).rejects.toThrowError(/invalid utility name/) }) }) describe('addComponents()', () => { test('is an alias for addUtilities', async () => { - expect( - await run( - ['btn', 'btn-blue', 'btn-red'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ addComponents }: PluginAPI) => { - addComponents({ - '.btn': { - padding: '.5rem 1rem', - borderRadius: '.25rem', - fontWeight: '600', + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + base, + module: ({ addComponents }: PluginAPI) => { + addComponents({ + '.btn': { + padding: '.5rem 1rem', + borderRadius: '.25rem', + fontWeight: '600', + }, + '.btn-blue': { + backgroundColor: '#3490dc', + color: '#fff', + '&:hover': { + backgroundColor: '#2779bd', }, - '.btn-blue': { - backgroundColor: '#3490dc', - color: '#fff', - '&:hover': { - backgroundColor: '#2779bd', - }, + }, + '.btn-red': { + backgroundColor: '#e3342f', + color: '#fff', + '&:hover': { + backgroundColor: '#cc1f1a', }, - '.btn-red': { - backgroundColor: '#e3342f', - color: '#fff', - '&:hover': { - backgroundColor: '#cc1f1a', - }, - }, - }) - }, - } - }, + }, + }) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .btn { + }, + ) + + expect(optimizeCss(compiled.build(['btn', 'btn-blue', 'btn-red'])).trim()) + .toMatchInlineSnapshot(` + ".btn { border-radius: .25rem; padding: .5rem 1rem; font-weight: 600; @@ -4446,39 +3539,44 @@ describe('addComponents()', () => { .btn-red:hover { background-color: #cc1f1a; - } - " + }" `) }) }) describe('matchComponents()', () => { test('is an alias for matchUtilities', async () => { - expect( - await run( - ['prose', 'sm:prose-sm', 'hover:prose-lg'], - css` - @plugin "my-plugin"; - @tailwind utilities; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ matchComponents }: PluginAPI) => { - matchComponents( - { prose: (value) => ({ '--container-size': value }) }, - { values: { DEFAULT: 'normal', sm: 'sm', lg: 'lg' } }, - ) - }, - } - }, + let compiled = await compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + base, + module: ({ matchComponents }: PluginAPI) => { + matchComponents( + { + prose: (value) => ({ '--container-size': value }), + }, + { + values: { + DEFAULT: 'normal', + sm: 'sm', + lg: 'lg', + }, + }, + ) + }, + } }, - ), - ).toMatchInlineSnapshot(` - " - .prose { + }, + ) + + expect(optimizeCss(compiled.build(['prose', 'sm:prose-sm', 'hover:prose-lg'])).trim()) + .toMatchInlineSnapshot(` + ".prose { --container-size: normal; } @@ -4486,23 +3584,21 @@ describe('matchComponents()', () => { .hover\\:prose-lg:hover { --container-size: lg; } - } - " + }" `) }) }) describe('prefix()', () => { test('is an identity function', async () => { - using fn = vi.fn() + let fn = vi.fn() await compile( css` @plugin "my-plugin"; `, { - async loadModule(_id, base) { + async loadModule(id, base) { return { - path: '', base, module: ({ prefix }: PluginAPI) => { fn(prefix('btn')) @@ -4515,117 +3611,3 @@ describe('prefix()', () => { expect(fn).toHaveBeenCalledWith('btn') }) }) - -describe('config()', () => { - test('can return the resolved config when passed no arguments', async () => { - using fn = vi.fn() - await compile( - css` - @plugin "my-plugin"; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ config }: PluginAPI) => { - fn(config()) - }, - } - }, - }, - ) - - expect(fn).toHaveBeenCalledWith( - expect.objectContaining({ - theme: expect.objectContaining({ - spacing: expect.any(Object), - }), - }), - ) - }) - - test('can return part of the config', async () => { - using fn = vi.fn() - await compile( - css` - @plugin "my-plugin"; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ config }: PluginAPI) => { - fn(config('theme')) - }, - } - }, - }, - ) - - expect(fn).toHaveBeenCalledWith( - expect.objectContaining({ - spacing: expect.any(Object), - }), - ) - }) - - test('falls back to default value if requested path does not exist', async () => { - using fn = vi.fn() - await compile( - css` - @plugin "my-plugin"; - `, - { - async loadModule(_id, base) { - return { - path: '', - base, - module: ({ config }: PluginAPI) => { - fn(config('somekey', 'defaultvalue')) - }, - } - }, - }, - ) - - expect(fn).toHaveBeenCalledWith('defaultvalue') - }) - - // https://github.com/tailwindlabs/tailwindcss/issues/19721 - test('matchUtilities does not match Object.prototype properties as values', async ({ - expect, - }) => { - // These should not crash or produce output - expect( - await run( - [ - 'test-constructor', - 'test-hasOwnProperty', - 'test-toString', - 'test-valueOf', - 'test-__proto__', - ], - css` - @tailwind utilities; - @plugin "my-plugin"; - `, - { - loadModule: async (_id, base) => { - return { - path: '', - base, - module: plugin(function ({ matchUtilities }) { - matchUtilities( - { test: (value) => ({ '--test': value }) }, - { values: { foo: 'bar' } }, - ) - }), - } - }, - }, - ), - ).toEqual('') - }) -}) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 11878fb30..a4552520d 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -1,33 +1,20 @@ -import type { Features } from '..' import { substituteAtApply } from '../apply' -import { atRule, cloneAstNode, context, decl, rule, type AstNode } from '../ast' +import { 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' +import { withAlpha, withNegative } from '../utilities' 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 { substituteAtSlot } from '../variants' import type { ResolvedConfig, UserConfig } from './config/types' import { createThemeFn } from './plugin-functions' export type Config = UserConfig export type PluginFn = (api: PluginAPI) => void -export type PluginWithConfig = { - handler: PluginFn - config?: UserConfig - - /** @internal */ - reference?: boolean - src?: SourceLocation | undefined -} +export type PluginWithConfig = { handler: PluginFn; config?: UserConfig } export type PluginWithOptions = { (options?: T): PluginWithConfig __isOptionsFunction: true @@ -84,78 +71,35 @@ export type PluginAPI = { ): void theme(path: string, defaultValue?: any): any - config(path?: string, defaultValue?: any): any + config(path: string, defaultValue?: any): any prefix(className: string): string } -const IS_VALID_UTILITY_NAME = /^[a-z@][a-zA-Z0-9/%._-]*$/ +const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ -export function buildPluginApi({ - designSystem, - ast, - resolvedConfig, - featuresRef, - referenceMode, - src, -}: { - designSystem: DesignSystem - ast: AstNode[] - resolvedConfig: ResolvedConfig - featuresRef: { current: Features } - referenceMode: boolean - src: SourceLocation | undefined -}): PluginAPI { +export function buildPluginApi( + designSystem: DesignSystem, + ast: AstNode[], + resolvedConfig: ResolvedConfig, +): PluginAPI { let api: PluginAPI = { addBase(css) { - if (referenceMode) return let baseNodes = objectToAst(css) - featuresRef.current |= substituteFunctions(baseNodes, designSystem) - let rule = atRule('@layer', 'base', baseNodes) - walk([rule], (node) => { - node.src = src - }) - ast.push(rule) + substituteFunctions(baseNodes, api.theme) + ast.push(rule('@layer base', baseNodes)) }, 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 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( - name, - (r) => { - r.nodes = parseVariantValue(variant, r.nodes) - }, - { - compounds: compoundsForSelectors(typeof variant === 'string' ? [variant] : variant), - }, - ) + designSystem.variants.static(name, (r) => { + r.nodes = parseVariantValue(variant, r.nodes) + }) } // CSS-in-JS object else if (typeof variant === 'object') { - designSystem.variants.fromAst(name, objectToAst(variant), designSystem) + designSystem.variants.fromAst(name, objectToAst(variant)) } }, matchVariant(name, fn, options) { @@ -168,17 +112,6 @@ 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( () => { @@ -202,17 +135,12 @@ export function buildPluginApi({ ruleNodes.nodes, ) } else if (variant.value.kind === 'named' && options?.values) { - if (!Object.hasOwn(options.values, variant.value.value)) { - return null - } let defaultValue = options.values[variant.value.value] if (typeof defaultValue !== 'string') { - return null + return } ruleNodes.nodes = resolveVariantValue(defaultValue, variant.modifier, ruleNodes.nodes) - } else { - return null } }) }, @@ -222,18 +150,12 @@ export function buildPluginApi({ if (a.kind !== 'functional' || z.kind !== 'functional') { return 0 } + if (!a.value || !z.value) { + return 0 + } - let aValueKey = a.value ? a.value.value : 'DEFAULT' - let zValueKey = z.value ? z.value.value : 'DEFAULT' - - let aValue = - (options?.values && Object.hasOwn(options.values, aValueKey) - ? options.values[aValueKey] - : undefined) ?? aValueKey - let zValue = - (options?.values && Object.hasOwn(options.values, zValueKey) - ? options.values[zValueKey] - : undefined) ?? zValueKey + let aValue = options?.values?.[a.value.value] ?? a.value.value + let zValue = options?.values?.[z.value.value] ?? z.value.value if (options && typeof options.sort === 'function') { return options.sort( @@ -242,25 +164,13 @@ export function buildPluginApi({ ) } - let aOrder = defaultOptionKeys.indexOf(aValueKey) - let zOrder = defaultOptionKeys.indexOf(zValueKey) + let aOrder = defaultOptionKeys.indexOf(a.value.value) + let zOrder = defaultOptionKeys.indexOf(z.value.value) - // Sort arbitrary values after configured values - aOrder = aOrder === -1 ? defaultOptionKeys.length : aOrder - zOrder = zOrder === -1 ? defaultOptionKeys.length : zOrder - - if (aOrder !== zOrder) return aOrder - zOrder - - // SAFETY: The values don't need to be checked for equality as they - // are guaranteed to be unique since we sort a list of de-duped - // variants and different (valid) variants cannot produce the same AST. - return aValue < zValue ? -1 : 1 + if (aOrder - zOrder === 0) return aValue < zValue ? -1 : 1 + return aOrder - zOrder }, ) - - designSystem.variants.suggest(name, () => - Object.keys(options?.values ?? {}).filter((v) => v !== 'DEFAULT'), - ) }, addUtilities(utilities) { @@ -274,82 +184,41 @@ export function buildPluginApi({ ) // Merge entries for the same class - let utils = new DefaultMap(() => []) + let utils: Record = {} for (let [name, css] of entries) { - if (name.startsWith('@keyframes ')) { - if (!referenceMode) { - let keyframes = rule(name, objectToAst(css)) - walk([keyframes], (node) => { - node.src = src - }) - ast.push(keyframes) + let [className, ...parts] = segment(name, ':') + + // Modify classes using pseudo-classes or pseudo-elements to use nested rules + if (parts.length > 0) { + let pseudos = parts.map((p) => `:${p.trim()}`).join('') + css = { + [`&${pseudos}`]: css, } + } + + utils[className] ??= [] + css = Array.isArray(css) ? css : [css] + utils[className].push(...css) + } + + for (let [name, css] of Object.entries(utils)) { + if (name.startsWith('@keyframes ')) { + ast.push(rule(name, objectToAst(css))) continue } - let selectorAst = SelectorParser.parse(name) - let foundValidUtility = false - - walk(selectorAst, (node) => { - if ( - node.kind === 'selector' && - node.value[0] === '.' && - IS_VALID_UTILITY_NAME.test(node.value.slice(1)) - ) { - let value = node.value - node.value = '&' - let selector = SelectorParser.toCss(selectorAst) - - let className = value.slice(1) - let contents = selector === '&' ? objectToAst(css) : [rule(selector, objectToAst(css))] - utils.get(className).push(...contents) - foundValidUtility = true - - node.value = value - return - } - - if ( - node.kind === 'function' && - (node.value === ':not' || - // A class inside `:nth-child(… of )` is part of the - // condition, not a utility being defined. - node.value === ':nth-child' || - node.value === ':nth-last-child') - ) { - return WalkAction.Skip - } - }) - - if (!foundValidUtility) { + if (name[0] !== '.' || !IS_VALID_UTILITY_NAME.test(name.slice(1))) { throw new Error( `\`addUtilities({ '${name}' : … })\` defines an invalid utility selector. Utilities must be a single class name and start with a lowercase letter, eg. \`.scrollbar-none\`.`, ) } - } - for (let [className, ast] of utils) { - // Prefix all class selector with the configured theme prefix - if (designSystem.theme.prefix) { - walk(ast, (node) => { - if (node.kind === 'rule') { - let selectorAst = SelectorParser.parse(node.selector) - walk(selectorAst, (node) => { - if (node.kind === 'selector' && node.value[0] === '.') { - node.value = `.${designSystem.theme.prefix}\\:${node.value.slice(1)}` - } - }) - node.selector = SelectorParser.toCss(selectorAst) - } - }) - } - - designSystem.utilities.static(className, (candidate) => { - let clonedAst = ast.map(cloneAstNode) - replaceNestedClassNameReferences(clonedAst, className, candidate.raw) - featuresRef.current |= substituteAtApply(clonedAst, designSystem) - return clonedAst + designSystem.utilities.static(name.slice(1), (candidate) => { + if (candidate.negative) return + let ast = objectToAst(css) + substituteAtApply(ast, designSystem) + return ast }) } }, @@ -368,121 +237,110 @@ export function buildPluginApi({ ) } - function compileFn({ negative }: { negative: boolean }) { - return (candidate: Extract) => { - // Throw out any candidate whose value is not a supported type + function compileFn(candidate: Extract) { + // A negative utility was provided but is unsupported + if (!options?.supportsNegativeValues && candidate.negative) return + + // Throw out any candidate whose value is not a supported type + if (candidate.value?.kind === 'arbitrary' && types.length > 0 && !types.includes('any')) { + // The candidate has an explicit data type but it's not in the list + // of supported types by this utility. For example, a `scrollbar` + // utility that is only used to change the scrollbar color but is + // used with a `length` value: `scrollbar-[length:var(--whatever)]` + if (candidate.value.dataType && !types.includes(candidate.value.dataType)) { + return + } + + // The candidate does not have an explicit data type and the value + // cannot be inferred as one of the supported types. For example, a + // `scrollbar` utility that is only used to change the scrollbar + // color but is used with a `length` value: `scrollbar-[33px]` if ( - candidate.value?.kind === 'arbitrary' && - types.length > 0 && - !types.includes('any') + !candidate.value.dataType && + !inferDataType(candidate.value.value, types as any[]) ) { - // The candidate has an explicit data type but it's not in the list - // of supported types by this utility. For example, a `scrollbar` - // utility that is only used to change the scrollbar color but is - // used with a `length` value: `scrollbar-[length:var(--whatever)]` - if (candidate.value.dataType && !types.includes(candidate.value.dataType)) { - return - } - - // The candidate does not have an explicit data type and the value - // cannot be inferred as one of the supported types. For example, a - // `scrollbar` utility that is only used to change the scrollbar - // color but is used with a `length` value: `scrollbar-[33px]` - if ( - !candidate.value.dataType && - !inferDataType(candidate.value.value, types as any[]) - ) { - return - } + return } - - let isColor = types.includes('color') - - // Resolve the candidate value - let value: string | null = null - let ignoreModifier = false - - { - let values = options?.values ?? {} - - if (isColor) { - // Color utilities implicitly support `inherit`, `transparent`, and `currentcolor` - // for backwards compatibility but still allow them to be overridden - values = Object.assign( - { - inherit: 'inherit', - transparent: 'transparent', - current: 'currentcolor', - }, - values, - ) - } - - if (!candidate.value) { - value = values.DEFAULT ?? null - } else if (candidate.value.kind === 'arbitrary') { - value = candidate.value.value - } else if ( - candidate.value.fraction && - Object.hasOwn(values, candidate.value.fraction) - ) { - value = values[candidate.value.fraction] - ignoreModifier = true - } 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 - ignoreModifier = - (candidate.value.fraction !== null && value?.includes('/')) ?? false - } - } - - if (value === null) return - - // Resolve the modifier value - let modifier: string | null - - { - let modifiers = options?.modifiers ?? null - - if (!candidate.modifier) { - modifier = null - } else if (modifiers === 'any' || candidate.modifier.kind === 'arbitrary') { - modifier = 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}%` - } else { - modifier = null - } - } - - // A modifier was provided but is invalid - if (candidate.modifier && modifier === null && !ignoreModifier) { - // For arbitrary values, return `null` to avoid falling through to the next utility - return candidate.value?.kind === 'arbitrary' ? null : undefined - } - - if (isColor && modifier !== null) { - value = withAlpha(value, modifier) - } - - if (negative) { - value = `calc(${value} * -1)` - } - - let ast = objectToAst(fn(value, { modifier })) - replaceNestedClassNameReferences(ast, name, candidate.raw) - featuresRef.current |= substituteAtApply(ast, designSystem) - return ast } + + let isColor = types.includes('color') + + // Resolve the candidate value + let value: string | null = null + let isFraction = false + + { + let values = options?.values ?? {} + + if (isColor) { + // Color utilities implicitly support `inherit`, `transparent`, and `currentColor` + // for backwards compatibility but still allow them to be overridden + values = Object.assign( + { + inherit: 'inherit', + transparent: 'transparent', + current: 'currentColor', + }, + values, + ) + } + + if (!candidate.value) { + value = values.DEFAULT ?? null + } else if (candidate.value.kind === 'arbitrary') { + value = candidate.value.value + } else if (values[candidate.value.value]) { + value = values[candidate.value.value] + } else if (values.__BARE_VALUE__) { + value = values.__BARE_VALUE__(candidate.value) ?? null + + isFraction = (candidate.value.fraction !== null && value?.includes('/')) ?? false + } + } + + if (value === null) return + + // Resolve the modifier value + let modifier: string | null + + { + let modifiers = options?.modifiers ?? null + + if (!candidate.modifier) { + modifier = null + } else if (modifiers === 'any' || candidate.modifier.kind === 'arbitrary') { + modifier = candidate.modifier.value + } else if (modifiers?.[candidate.modifier.value]) { + modifier = modifiers[candidate.modifier.value] + } else if (isColor && !Number.isNaN(Number(candidate.modifier.value))) { + modifier = `${candidate.modifier.value}%` + } else { + modifier = null + } + } + + // A modifier was provided but is invalid + if (candidate.modifier && modifier === null && !isFraction) { + // For arbitrary values, return `null` to avoid falling through to the next utility + return candidate.value?.kind === 'arbitrary' ? null : undefined + } + + if (isColor && modifier !== null) { + value = withAlpha(value, modifier) + } + + if (candidate.negative) { + value = withNegative(value, candidate) + } + + let ast = objectToAst(fn(value, { modifier })) + substituteAtApply(ast, designSystem) + return ast } - if (options?.supportsNegativeValues) { - designSystem.utilities.functional(`-${name}`, compileFn({ negative: true }), { types }) - } - designSystem.utilities.functional(name, compileFn({ negative: false }), { types }) + designSystem.utilities.functional(name, compileFn, { + types, + }) designSystem.utilities.suggest(name, () => { let values = options?.values ?? {} @@ -492,10 +350,6 @@ 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') @@ -537,8 +391,6 @@ export function buildPluginApi({ config(path, defaultValue) { let obj: Record = resolvedConfig - if (!path) return obj - let keypath = toKeyPath(path) for (let i = 0; i < keypath.length; ++i) { @@ -570,26 +422,16 @@ 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') { - ast.push(rule(name, [atRule('@slot')])) - continue - } - + if (!name.startsWith('--') && value === '@slot') { + ast.push(rule(name, [rule('@slot', [])])) + } else { // Convert camelCase to kebab-case: // https://github.com/postcss/postcss-js/blob/b3db658b932b42f6ac14ca0b1d50f50c4569805b/parser.js#L30-L35 name = name.replace(/([A-Z])/g, '-$1').toLowerCase() - } - ast.push(decl(name, String(value))) + ast.push(decl(name, String(value))) + } } else if (Array.isArray(value)) { for (let item of value) { if (typeof item === 'string') { @@ -598,7 +440,7 @@ export function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] { ast.push(rule(name, objectToAst(item))) } } - } else { + } else if (value !== null) { ast.push(rule(name, objectToAst(value))) } } @@ -608,50 +450,17 @@ export function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] { function parseVariantValue(resolved: string | string[], nodes: AstNode[]): AstNode[] { let resolvedArray = typeof resolved === 'string' ? [resolved] : resolved - return resolvedArray.flatMap((r): AstNode | AstNode[] => { + return resolvedArray.flatMap((r) => { if (r.trim().endsWith('}')) { - let usesAtScope = r.includes('@scope') let updatedCSS = r.replace('}', '{@slot}}') let ast = CSS.parse(updatedCSS) - - if (usesAtScope) { - substituteAtSlot(ast, [context({ source: 'user' }, nodes)]) - return context({ source: 'variant' }, ast) - } - substituteAtSlot(ast, nodes) return ast } else { - let node = rule(r, nodes) - - if (node.kind === 'at-rule' && node.name === '@scope') { - node.nodes = [context({ source: 'user' }, nodes)] - return context({ source: 'variant' }, [node]) - } - - return node + return rule(r, nodes) } }) } type Primitive = string | number | boolean | null export type CssPluginOptions = Record - -function replaceNestedClassNameReferences( - ast: AstNode[], - utilityName: string, - rawCandidate: string, -) { - // Replace nested rules using the utility name in the selector - walk(ast, (node) => { - if (node.kind === 'rule') { - let selectorAst = SelectorParser.parse(node.selector) - walk(selectorAst, (node) => { - if (node.kind === 'selector' && node.value === `.${utilityName}`) { - node.value = `.${escape(rawCandidate)}` - } - }) - node.selector = SelectorParser.toCss(selectorAst) - } - }) -} diff --git a/packages/tailwindcss/src/compat/plugin-functions.ts b/packages/tailwindcss/src/compat/plugin-functions.ts index bbd5fac46..b75815e44 100644 --- a/packages/tailwindcss/src/compat/plugin-functions.ts +++ b/packages/tailwindcss/src/compat/plugin-functions.ts @@ -2,9 +2,7 @@ import type { DesignSystem } from '../design-system' import { ThemeOptions, type Theme, type ThemeKey } from '../theme' import { withAlpha } from '../utilities' import { DefaultMap } from '../utils/default-map' -import { unescape } from '../utils/escape' import { toKeyPath } from '../utils/to-key-path' -import { keyPathToCssProperty } from './apply-config-to-theme' import { deepMerge } from './config/deep-merge' import type { UserConfig } from './config/types' @@ -30,10 +28,6 @@ export function createThemeFn( let configValue = resolveValue(get(configTheme() ?? {}, keypath) ?? null) - if (typeof configValue === 'string') { - configValue = configValue.replace('', '1') - } - // Resolved to a primitive value. if (typeof cssValue !== 'object') { if (typeof options !== 'object' && options & ThemeOptions.DEFAULT) { @@ -43,6 +37,7 @@ export function createThemeFn( return cssValue } + // if (configValue !== null && typeof configValue === 'object' && !Array.isArray(configValue)) { let configValueCopy: Record & { __CSS_VALUES__?: Record } = // We want to make sure that we don't mutate the original config @@ -75,7 +70,7 @@ export function createThemeFn( } // CSS values from `@theme` win over values from the config - configValueCopy[unescape(key)] = cssValue[key] + configValueCopy[key] = cssValue[key] } return configValueCopy @@ -100,28 +95,6 @@ export function createThemeFn( return [base, extra] } - // If `--color-foo` and `--color-foo-bar` are both defined, `colors.foo` - // produces a synthetic object: - // - // ```ts - // { DEFAULT: 'red', bar: 'blue', __CSS_VALUES__: { DEFAULT: 0, bar: 0 } } - // ``` - // - // Prefer `DEFAULT` instead of exposing the object to plugin code. - // - // This only applies to lookups of a specific key (e.g. `colors.foo`). A - // namespace root lookup (e.g. `theme('shadow')`) resolves to the whole - // namespace as an object, like in v3. - if ( - keypath.length > 1 && - cssValue !== null && - typeof cssValue === 'object' && - !Array.isArray(cssValue) && - 'DEFAULT' in cssValue - ) { - return cssValue.DEFAULT - } - // Values from CSS take precedence over values from the config return cssValue ?? configValue })() @@ -154,7 +127,21 @@ function readFromCss( // A nested tuple with additional data | [main: string, extra: Record] - let themeKey = keyPathToCssProperty(path) + let themeKey = path + // [1] should move into the nested object tuple. To create the CSS variable + // name for this, we replace it with an empty string that will result in two + // subsequent dashes when joined. + .map((path) => (path === '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()}`), + ) + + // Remove the `DEFAULT` key at the end of a path + // We're reading from CSS anyway so it'll be a string + .filter((part, index) => part !== 'DEFAULT' || index !== path.length - 1) + .join('-') let map = new Map() let nested = new DefaultMap>( @@ -230,12 +217,7 @@ function readFromCss( // The request looked like `theme('animation.spin')` and was turned into a // lookup for `--animation-spin-*` which had only one entry which means it // should be returned directly. - // - // This only applies to lookups of a specific key (`path.length > 1`). A - // namespace root lookup like `theme('shadow')` resolves to the whole - // namespace as an object, like in v3, so it must keep the object shape (`{ - // DEFAULT: … }`). - if (path.length > 1 && 'DEFAULT' in obj && Object.keys(obj).length === 1) { + if ('DEFAULT' in obj && Object.keys(obj).length === 1) { return [obj.DEFAULT as any, optionsObj.DEFAULT ?? ThemeOptions.NONE] as const } @@ -250,10 +232,8 @@ function get(obj: any, path: string[]) { for (let i = 0; i < path.length; ++i) { let key = path[i] - // The key does not exist so concatenate it with the next key. - // We use Object.hasOwn to avoid matching inherited prototype properties - // (e.g. "constructor", "toString") when traversing config objects. - if (obj === null || obj === undefined || typeof obj !== 'object' || !Object.hasOwn(obj, key)) { + // The key does not exist so concatenate it with the next key + if (obj[key] === undefined) { if (path[i + 1] === undefined) { return undefined } diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index d81cd8de6..5f75504ad 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -1,89 +1,87 @@ import { describe, expect, test } from 'vitest' -import { run } from '../test-utils/run' +import { compile } from '..' const css = String.raw test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { - expect( - await run( - [ - 'sm:flex', - 'md:flex', - 'lg:flex', - 'min-sm:max-md:underline', - 'min-md:max-lg:underline', - 'max-w-screen-sm', - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @theme { - --breakpoint-md: 50rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: '44rem', - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @theme { + --breakpoint-md: 50rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '44rem', }, }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + }), + }) + + expect( + compiler.build([ + 'sm:flex', + 'md:flex', + 'lg:flex', + 'min-sm:max-md:underline', + 'min-md:max-lg:underline', + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - .max-w-screen-sm { - max-width: 44rem; + ":root { + --breakpoint-md: 50rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; } - - @media (min-width: 44rem) { - .sm\\:flex { + .sm\\:flex { + @media (width >= 44rem) { display: flex; } - - @media not all and (min-width: 50rem) { - .min-sm\\:max-md\\:underline { + } + .min-sm\\:max-md\\:underline { + @media (width >= 44rem) { + @media (width < 50rem) { text-decoration-line: underline; } } } - - @media (min-width: 50rem) { - .md\\:flex { + .md\\:flex { + @media (width >= 50rem) { display: flex; } - - @media not all and (min-width: 64rem) { - .min-md\\:max-lg\\:underline { + } + .min-md\\:max-lg\\:underline { + @media (width >= 50rem) { + @media (width < 64rem) { text-decoration-line: underline; } } } - - @media (min-width: 64rem) { - .lg\\:flex { + .lg\\:flex { + @media (width >= 64rem) { display: flex; } } - - @media print { - .print\\:items-end { + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -92,98 +90,103 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { }) test('JS config `screens` extend CSS `--breakpoint-*`', async () => { - expect( - await run( - [ - // Order is messed up on purpose - 'md:flex', - 'sm:flex', - 'lg:flex', - 'xs:flex', - 'min-md:max-lg:underline', - 'min-sm:max-md:underline', - 'min-xs:flex', - 'min-xs:max-md:underline', + let input = css` + @theme default { + --breakpoint-xs: 39rem; + --breakpoint-md: 49rem; + } + @theme { + --breakpoint-md: 50rem; + } + @config "./config.js"; + @tailwind utilities; + ` - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @theme default { - --breakpoint-xs: 39rem; - --breakpoint-md: 49rem; - } - @theme { - --breakpoint-md: 50rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - xs: '30rem', - sm: '40rem', - md: '48rem', - lg: '60rem', - }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + xs: '30rem', + sm: '40rem', + md: '48rem', + lg: '60rem', }, }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + }), + }) + + expect( + compiler.build([ + // Order is messed up on purpose + 'md:flex', + 'sm:flex', + 'lg:flex', + 'xs:flex', + 'min-md:max-lg:underline', + 'min-sm:max-md:underline', + 'min-xs:flex', + 'min-xs:max-md:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 30rem) { - .min-xs\\:flex, .xs\\:flex { + ":root { + --breakpoint-md: 50rem; + } + .min-xs\\:flex { + @media (width >= 30rem) { display: flex; } - - @media not all and (min-width: 50rem) { - .min-xs\\:max-md\\:underline { + } + .xs\\:flex { + @media (width >= 30rem) { + display: flex; + } + } + .min-xs\\:max-md\\:underline { + @media (width >= 30rem) { + @media (width < 50rem) { text-decoration-line: underline; } } } - - @media (min-width: 40rem) { - .sm\\:flex { + .sm\\:flex { + @media (width >= 40rem) { display: flex; } - - @media not all and (min-width: 50rem) { - .min-sm\\:max-md\\:underline { + } + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 50rem) { text-decoration-line: underline; } } } - - @media (min-width: 50rem) { - .md\\:flex { + .md\\:flex { + @media (width >= 50rem) { display: flex; } - - @media not all and (min-width: 60rem) { - .min-md\\:max-lg\\:underline { + } + .min-md\\:max-lg\\:underline { + @media (width >= 50rem) { + @media (width < 60rem) { text-decoration-line: underline; } } } - - @media (min-width: 60rem) { - .lg\\:flex { + .lg\\:flex { + @media (width >= 60rem) { display: flex; } } - - @media print { - .print\\:items-end { + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -192,73 +195,70 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { }) test('JS config `screens` only setup, even if those match the default-theme export', async () => { - expect( - await run( - [ - // Order is messed up on purpose - 'md:flex', - 'sm:flex', - 'lg:flex', - 'min-md:max-lg:underline', - 'min-sm:max-md:underline', + let input = css` + @config "./config.js"; + @tailwind utilities; + ` - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - screens: { - sm: '40rem', - md: '48rem', - lg: '64rem', - }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + screens: { + sm: '40rem', + md: '48rem', + lg: '64rem', }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + }), + }) + + expect( + compiler.build([ + // Order is messed up on purpose + 'md:flex', + 'sm:flex', + 'lg:flex', + 'min-md:max-lg:underline', + 'min-sm:max-md:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 40rem) { - .sm\\:flex { + ".sm\\:flex { + @media (width >= 40rem) { display: flex; } - - @media not all and (min-width: 48rem) { - .min-sm\\:max-md\\:underline { + } + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { text-decoration-line: underline; } } } - - @media (min-width: 48rem) { - .md\\:flex { + .md\\:flex { + @media (width >= 48rem) { display: flex; } - - @media not all and (min-width: 64rem) { - .min-md\\:max-lg\\:underline { + } + .min-md\\:max-lg\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { text-decoration-line: underline; } } } - - @media (min-width: 64rem) { - .lg\\:flex { + .lg\\:flex { + @media (width >= 64rem) { display: flex; } } - - @media print { - .print\\:items-end { + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -267,85 +267,111 @@ test('JS config `screens` only setup, even if those match the default-theme expo }) test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => { - expect( - await run( - [ - 'sm:flex', - 'md:flex', - 'mini:flex', - 'midi:flex', - 'maxi:flex', - 'min-md:max-lg:underline', - 'min-sm:max-md:underline', - 'min-midi:max-maxi:underline', - 'min-mini:max-midi:underline', + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @config "./config.js"; + @tailwind utilities; + ` - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', - }, - }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + 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(` - " - @media (min-width: 40rem) { - .mini\\:flex, .sm\\:flex { + ":root { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + .mini\\:flex { + @media (width >= 40rem) { display: flex; } - - @media not all and (min-width: 48rem) { - .min-mini\\:max-midi\\:underline, .min-sm\\:max-md\\:underline { + } + .sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .min-mini\\:max-midi\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { text-decoration-line: underline; } } } - - @media (min-width: 48rem) { - .md\\:flex, .midi\\:flex { - display: flex; - } - - @media not all and (min-width: 64rem) { - .min-md\\:max-lg\\:underline, .min-midi\\:max-maxi\\:underline { + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { text-decoration-line: underline; } } } - - @media (min-width: 64rem) { - .maxi\\:flex { + .md\\:flex { + @media (width >= 48rem) { display: flex; } } - - @media print { - .print\\:items-end { + .midi\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .min-md\\:max-lg\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .min-midi\\:max-maxi\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .maxi\\:flex { + @media (width >= 64rem) { + display: flex; + } + } + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -359,91 +385,70 @@ test('JS config with `theme: { extends }` should not include the `default-config @tailwind utilities; ` - expect( - await run(['sm:flex', 'md:flex', 'min-md:max-lg:underline', 'min-sm:max-md:underline'], input, { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', }, }, }, - base: '/root', - path: '', - }), + }, + base: '/root', }), + }) + + expect( + compiler.build(['sm:flex', 'md:flex', 'min-md:max-lg:underline', 'min-sm:max-md:underline']), ).toBe('') expect( - await run( - [ - 'mini:flex', - 'midi:flex', - 'maxi:flex', - 'min-midi:max-maxi:underline', - 'min-mini:max-midi:underline', + compiler.build([ + 'mini:flex', + 'midi:flex', + 'maxi:flex', + 'min-midi:max-maxi:underline', + 'min-mini:max-midi:underline', - // Ensure other core variants appear at the end - 'print:items-end', - ], - input, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 40rem) { - .mini\\:flex { + ".mini\\:flex { + @media (width >= 40rem) { display: flex; } - - @media not all and (min-width: 48rem) { - .min-mini\\:max-midi\\:underline { + } + .min-mini\\:max-midi\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { text-decoration-line: underline; } } } - - @media (min-width: 48rem) { - .midi\\:flex { + .midi\\:flex { + @media (width >= 48rem) { display: flex; } - - @media not all and (min-width: 64rem) { - .min-midi\\:max-maxi\\:underline { + } + .min-midi\\:max-maxi\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { text-decoration-line: underline; } } } - - @media (min-width: 64rem) { - .maxi\\:flex { + .maxi\\:flex { + @media (width >= 64rem) { display: flex; } } - - @media print { - .print\\:items-end { + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -458,104 +463,76 @@ describe('complex screen configs', () => { @tailwind utilities; ` - expect( - await run(['min-sm:flex', 'min-md:flex', 'min-xl:flex', 'min-tall:flex'], input, { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: { max: '639px' }, - md: [ - // - { min: '668px', max: '767px' }, - '868px', - ], - lg: { min: '868px' }, - xl: { min: '1024px', max: '1279px' }, - tall: { raw: '(min-height: 800px)' }, - }, + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: { max: '639px' }, + md: [ + // + { min: '668px', max: '767px' }, + '868px', + ], + lg: { min: '868px' }, + xl: { min: '1024px', max: '1279px' }, + tall: { raw: '(min-height: 800px)' }, }, }, }, - base: '/root', - path: '', - }), + }, + base: '/root', }), - ).toBe('') + }) + + expect(compiler.build(['min-sm:flex', 'min-md:flex', 'min-xl:flex', 'min-tall:flex'])).toBe('') expect( - await run( - [ - 'sm:flex', - 'md:flex', - 'lg:flex', - 'min-lg:flex', - 'xl:flex', - 'tall:flex', + compiler.build([ + 'sm:flex', + 'md:flex', + 'lg:flex', + 'min-lg:flex', + 'xl:flex', + 'tall:flex', - // Ensure other core variants appear at the end - 'print:items-end', - ], - input, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: { max: '639px' }, - md: [ - // - { min: '668px', max: '767px' }, - '868px', - ], - lg: { min: '868px' }, - xl: { min: '1024px', max: '1279px' }, - tall: { raw: '(min-height: 800px)' }, - }, - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 868px) { - .lg\\:flex, .min-lg\\:flex { + ".lg\\:flex { + @media (width >= 868px) { display: flex; } } - - @media (max-width: 639px) { - .sm\\:flex { + .min-lg\\:flex { + @media (width >= 868px) { display: flex; } } - - @media (max-width: 767px) and (min-width: 668px), (min-width: 868px) { - .md\\:flex { + .sm\\:flex { + @media (639px >= width) { display: flex; } } - - @media (max-width: 1279px) and (min-width: 1024px) { - .xl\\:flex { + .md\\:flex { + @media (767px >= width >= 668px), (width >= 868px) { display: flex; } } - - @media (min-height: 800px) { - .tall\\:flex { + .xl\\:flex { + @media (1279px >= width >= 1024px) { display: flex; } } - - @media print { - .print\\:items-end { + .tall\\:flex { + @media (min-height: 800px) { + display: flex; + } + } + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -564,65 +541,73 @@ describe('complex screen configs', () => { }) test("don't interfere with `min-*` and `max-*` variants of non-complex screen configs", async () => { - expect( - await run( - [ - 'sm:flex', - 'md:flex', - 'portrait:flex', - 'min-sm:flex', - 'min-md:flex', - 'min-portrait:flex', - // Ensure other core variants appear at the end - 'print:items-end', - ], - css` - @theme default { - --breakpoint-sm: 39rem; - --breakpoint-md: 48rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - extend: { - screens: { - sm: '40rem', - portrait: { raw: 'screen and (orientation: portrait)' }, - }, - }, + let input = css` + @theme default { + --breakpoint-sm: 39rem; + --breakpoint-md: 48rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '40rem', + portrait: { raw: 'screen and (orientation: portrait)' }, }, }, - base: '/root', - path: '', - }), + }, }, - ), + base: '/root', + }), + }) + + expect( + compiler.build([ + 'sm:flex', + 'md:flex', + 'portrait:flex', + 'min-sm:flex', + 'min-md:flex', + 'min-portrait:flex', + // Ensure other core variants appear at the end + 'print:items-end', + ]), ).toMatchInlineSnapshot(` - " - @media (min-width: 40rem) { - .min-sm\\:flex, .sm\\:flex { + ":root { + --breakpoint-md: 48rem; + } + .min-sm\\:flex { + @media (width >= 40rem) { display: flex; } } - - @media (min-width: 48rem) { - .md\\:flex, .min-md\\:flex { + .sm\\:flex { + @media (width >= 40rem) { display: flex; } } - - @media screen and (orientation: portrait) { - .portrait\\:flex { + .md\\:flex { + @media (width >= 48rem) { display: flex; } } - - @media print { - .print\\:items-end { + .min-md\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .portrait\\:flex { + @media screen and (orientation: portrait) { + display: flex; + } + } + .print\\:items-end { + @media print { align-items: flex-end; } } @@ -630,40 +615,3 @@ describe('complex screen configs', () => { `) }) }) - -test('JS config `screens` can overwrite default CSS `--breakpoint-*`', async () => { - // Note: The `sm`, `md`, and other variants are still there because they are - // created before the compat layer can intercept. We do not remove them - // currently. - expect( - await run( - ['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-2xl:flex'], - css` - @theme default { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - @config "./config.js"; - @tailwind utilities; - `, - { - loadModule: async () => ({ - module: { - theme: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', - }, - }, - }, - base: '/root', - path: '', - }), - }, - ), - ).toEqual('') -}) diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index 8d5c65136..04fdaa7d1 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -1,4 +1,4 @@ -import { atRule } from '../ast' +import { rule } from '../ast' import type { DesignSystem } from '../design-system' import type { ResolvedConfig } from './config/types' @@ -45,7 +45,7 @@ export function registerScreensConfig(userConfig: ResolvedConfig, designSystem: designSystem.variants.static( name, (ruleNode) => { - ruleNode.nodes = [atRule('@media', query, ruleNode.nodes)] + ruleNode.nodes = [rule(`@media ${query}`, ruleNode.nodes)] }, { order }, ) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 46f562918..87b5133da 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -1,24 +1,21 @@ -import { atRule, context, decl, rule, type AstNode, type Rule, type StyleRule } from './ast' +import { decl, rule, walk, WalkAction, type AstNode, type Rule } from './ast' import { type Candidate, type Variant } from './candidate' -import { CompileAstFlags, type DesignSystem } from './design-system' +import { substituteFunctions } from './css-functions' +import { 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, - respectImportant, - }: { onInvalidCandidate?: (candidate: string) => void; respectImportant?: boolean } = {}, + { onInvalidCandidate }: { onInvalidCandidate?: (candidate: string) => void } = {}, ) { let nodeSorting = new Map< AstNode, - { properties: { order: number[]; count: number }; variants: bigint; candidate: string } + { properties: number[]; variants: bigint; candidate: string } >() let astNodes: AstNode[] = [] let matches = new Map() @@ -39,12 +36,6 @@ export function compileCandidates( matches.set(rawCandidate, candidates) } - let flags = CompileAstFlags.None - - if (respectImportant ?? true) { - flags |= CompileAstFlags.RespectImportant - } - let variantOrderMap = designSystem.getVariantOrder() // Create the AST @@ -52,9 +43,25 @@ export function compileCandidates( let found = false for (let candidate of candidates) { - let rules = designSystem.compileAstNodes(candidate, flags) + let rules = designSystem.compileAstNodes(candidate) if (rules.length === 0) continue + // Arbitrary values (`text-[theme(--color-red-500)]`) and arbitrary + // properties (`[--my-var:theme(--color-red-500)]`) can contain function + // calls so we need evaluate any functions we find there that weren't in + // the source CSS. + try { + substituteFunctions( + rules.map(({ node }) => node), + designSystem.resolveThemeValue, + ) + } catch (err) { + // If substitution fails then the candidate likely contains a call to + // `theme()` that is invalid which may be because of incorrect usage, + // invalid arguments, or a theme key that does not exist. + continue + } + found = true for (let { node, propertySort } of rules) { @@ -81,7 +88,7 @@ export function compileCandidates( } astNodes.sort((a, z) => { - // SAFETY: At this point it is safe to use TypeScript's non-null assertion + // Safety: At this point it is safe to use TypeScript's non-null assertion // operator because if the ast nodes didn't exist, we introduced a bug // above, but there is no need to re-check just to be sure. If this relied // on pure user input, then we would need to check for its existence. @@ -96,19 +103,18 @@ export function compileCandidates( // Find the first property that is different between the two rules let offset = 0 while ( - offset < aSorting.properties.order.length && - offset < zSorting.properties.order.length && - aSorting.properties.order[offset] === zSorting.properties.order[offset] + aSorting.properties.length < offset && + zSorting.properties.length < offset && + aSorting.properties[offset] === zSorting.properties[offset] ) { offset += 1 } return ( // Sort by lowest property index first - (aSorting.properties.order[offset] ?? Infinity) - - (zSorting.properties.order[offset] ?? Infinity) || + (aSorting.properties[offset] ?? Infinity) - (zSorting.properties[offset] ?? Infinity) || // Sort by most properties first, then by least properties - zSorting.properties.count - aSorting.properties.count || + zSorting.properties.length - aSorting.properties.length || // Sort alphabetically compare(aSorting.candidate, zSorting.candidate) ) @@ -120,22 +126,13 @@ export function compileCandidates( } } -export function compileAstNodes( - candidate: Candidate, - designSystem: DesignSystem, - flags: CompileAstFlags, -) { +export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem) { let asts = compileBaseUtility(candidate, designSystem) if (asts.length === 0) return [] - let respectImportant = designSystem.important && Boolean(flags & CompileAstFlags.RespectImportant) - let rules: { node: AstNode - propertySort: { - order: number[] - count: number - } + propertySort: number[] }[] = [] let selector = `.${escape(candidate.raw)}` @@ -143,14 +140,11 @@ export function compileAstNodes( for (let nodes of asts) { let propertySort = getPropertySort(nodes) - // 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) { + if (candidate.important || designSystem.important) { applyImportant(nodes) } - let node: StyleRule = { + let node: Rule = { kind: 'rule', selector, nodes, @@ -187,22 +181,11 @@ export function applyVariant( // E.g. `[>img]:flex` is not valid, but `has-[>img]:flex` is if (variant.relative && depth === 0) return null - let child: AstNode = rule(variant.selector, node.nodes) - - // Wrap variant-generated `@scope` rules in a `context` marker, so they are - // hoisted with the variant semantics instead of their native CSS nesting - // semantics. The slotted content is marked as `user` content, so `@scope` - // rules defined there keep their native CSS nesting semantics. - if (child.kind === 'at-rule' && child.name === '@scope') { - child.nodes = [context({ source: 'user' }, node.nodes)] - child = context({ source: 'variant' }, [child]) - } - - node.nodes = [child] + node.nodes = [rule(variant.selector, node.nodes)] return } - // SAFETY: At this point it is safe to use TypeScript's non-null assertion + // Safety: At this point it is safe to use TypeScript's non-null assertion // operator because if the `candidate.root` didn't exist, `parseCandidate` // would have returned `null` and we would have returned early resulting in // not hitting this code path. @@ -222,20 +205,11 @@ export function applyVariant( // To solve this, we provide an isolated placeholder node to the variant. // The variant can now apply its logic to the isolated node without // affecting the original node. - let isolatedNode = atRule('@slot') + let isolatedNode = rule('@slot', []) let result = applyVariant(isolatedNode, variant.variant, variants, depth + 1) if (result === null) return null - if (variant.root === 'not' && isolatedNode.nodes.length > 1) { - // The `not` variant cannot negate sibling rules / at-rules because these - // are an OR relationship. Doing so would require transforming sibling - // nodes into nesting while negating them. This isn't possible with the - // current implementation of the `not` variant or with how variants are - // applied in general (on a per-node basis). - return null - } - for (let child of isolatedNode.nodes) { // Only some variants wrap children in rules. For example, the `force` // variant is a noop on the AST. And the `has` variant modifies the @@ -244,16 +218,16 @@ export function applyVariant( // This means `child` may be a declaration and we don't want to apply the // variant to it. This also means the entire variant as a whole is not // applicable to the rule and should generate nothing. - if (child.kind !== 'rule' && child.kind !== 'at-rule') return null + if (child.kind !== 'rule') return null - let result = applyFn(child, variant) + let result = applyFn(child as Rule, variant) if (result === null) return null } // Replace the placeholder node with the actual node { walk(isolatedNode.nodes, (child) => { - if ((child.kind === 'rule' || child.kind === 'at-rule') && child.nodes.length <= 0) { + if (child.kind === 'rule' && child.nodes.length <= 0) { child.nodes = node.nodes return WalkAction.Skip } @@ -280,7 +254,7 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { // Assumption: If an arbitrary property has a modifier, then we assume it // is an opacity modifier. if (candidate.modifier) { - value = asColor(value, candidate.modifier, designSystem.theme) + value = asColor(value, candidate.modifier) } if (value === null) return [] @@ -298,19 +272,7 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) { - // `null` means that the result is invalid and that this plugin should not - // result in any CSS, but that doesn't mean that subsequent plugins are - // invalid as well. - // - // However, for backwards compatibility with `matchUtilities` this means - // that we do need to bail entirely: plugins that handle a specific - // arbitrary value type prevent falling through to other plugins if the - // result is invalid for that plugin - if (utility.options?.types?.length) return asts - - continue - } + if (compiledNodes === null) return asts asts.push(compiledNodes) } @@ -322,19 +284,7 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) { - // `null` means that the result is invalid and that this plugin should not - // result in any CSS, but that doesn't mean that subsequent plugins are - // invalid as well. - // - // However, for backwards compatibility with `matchUtilities` this means - // that we do need to bail entirely: plugins that handle a specific - // arbitrary value type prevent falling through to other plugins if the - // result is invalid for that plugin - if (utility.options?.types?.length) return asts - - continue - } + if (compiledNodes === null) return asts asts.push(compiledNodes) } @@ -351,7 +301,7 @@ function applyImportant(ast: AstNode[]): void { if (node.kind === 'declaration') { node.important = true - } else if (node.kind === 'rule' || node.kind === 'at-rule') { + } else if (node.kind === 'rule') { applyImportant(node.nodes) } } @@ -359,44 +309,30 @@ function applyImportant(ast: AstNode[]): void { function getPropertySort(nodes: AstNode[]) { // Determine sort order based on properties used - let order = new Set() - let count = 0 + let propertySort = new Set() let q: AstNode[] = nodes.slice() - let seenTwSort = false - while (q.length > 0) { - // SAFETY: At this point it is safe to use TypeScript's non-null assertion + // Safety: At this point it is safe to use TypeScript's non-null assertion // operator because we guarded against `q.length > 0` above. let node = q.shift()! if (node.kind === 'declaration') { - // Empty strings should still be counted, e.g.: `--tw-foo:;` is valid - if (node.value === undefined) continue - - count++ - - if (seenTwSort) continue - if (node.property === '--tw-sort') { let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.value ?? '') if (idx !== -1) { - order.add(idx) - seenTwSort = true - continue + propertySort.add(idx) + break } } let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.property) - if (idx !== -1) order.add(idx) - } else if (node.kind === 'rule' || node.kind === 'at-rule') { + if (idx !== -1) propertySort.add(idx) + } else if (node.kind === 'rule') { for (let child of node.nodes) { q.push(child) } } } - return { - order: Array.from(order).sort((a, z) => a - z), - count, - } + return Array.from(propertySort).sort((a, z) => a - z) } diff --git a/packages/tailwindcss/src/constant-fold-declaration.test.ts b/packages/tailwindcss/src/constant-fold-declaration.test.ts deleted file mode 100644 index 5a0c867b6..000000000 --- a/packages/tailwindcss/src/constant-fold-declaration.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -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 deleted file mode 100644 index c15420150..000000000 --- a/packages/tailwindcss/src/constant-fold-declaration.ts +++ /dev/null @@ -1,357 +0,0 @@ -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 - - //