main 3997fbd5ddf6 cached
142 files
202.0 KB
61.4k tokens
117 symbols
1 requests
Download .txt
Showing preview only (232K chars total). Download the full file or copy to clipboard to get everything.
Repository: tailwindlabs/prettier-plugin-tailwindcss
Branch: main
Commit: 3997fbd5ddf6
Files: 142
Total size: 202.0 KB

Directory structure:
gitextract_s76hpf38/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.md
│   │   └── config.yml
│   └── workflows/
│       ├── ci.yml
│       ├── prepare-release.yml
│       ├── release-insiders.yml
│       └── release.yml
├── .gitignore
├── .oxlintrc.json
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── knip.json
├── package.json
├── prettier.config.js
├── scripts/
│   ├── copy-licenses.js
│   ├── install-fixture-deps.js
│   ├── release-channel.js
│   └── release-notes.js
├── src/
│   ├── config.ts
│   ├── console.ts
│   ├── create-plugin.ts
│   ├── expiring-map.ts
│   ├── index.ts
│   ├── internal.d.ts
│   ├── options.ts
│   ├── resolve.ts
│   ├── sorter.ts
│   ├── sorting.ts
│   ├── transform.ts
│   ├── types.ts
│   ├── utils.bench.ts
│   ├── utils.test.ts
│   ├── utils.ts
│   └── versions/
│       ├── assets.ts
│       ├── v3.ts
│       └── v4.ts
├── tests/
│   ├── fixtures/
│   │   ├── basic/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── cjs/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.cjs
│   │   ├── custom-jsx/
│   │   │   ├── index.jsx
│   │   │   ├── output.jsx
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── custom-pkg-name-v3/
│   │   │   ├── config.js
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── package.json
│   │   ├── custom-pkg-name-v4/
│   │   │   ├── app.css
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── package.json
│   │   ├── custom-vue/
│   │   │   ├── index.vue
│   │   │   ├── output.vue
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── esm/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.mjs
│   │   ├── esm-explicit/
│   │   │   ├── config.mjs
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── prettier.config.js
│   │   ├── monorepo/
│   │   │   ├── .prettierrc
│   │   │   ├── package-1/
│   │   │   │   ├── app.css
│   │   │   │   ├── index.jsx
│   │   │   │   ├── output.jsx
│   │   │   │   └── package.json
│   │   │   ├── package-2/
│   │   │   │   ├── index.jsx
│   │   │   │   ├── output.jsx
│   │   │   │   ├── package.json
│   │   │   │   └── tailwind.config.js
│   │   │   └── package.json
│   │   ├── no-local-version/
│   │   │   ├── app.css
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── package.json
│   │   ├── no-prettier-config/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── tailwind.config.js
│   │   ├── no-stylesheet-given/
│   │   │   ├── index.html
│   │   │   └── output.html
│   │   ├── package.json
│   │   ├── plugins/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── ts/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.ts
│   │   ├── ts-explicit/
│   │   │   ├── config.ts
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── prettier.config.js
│   │   ├── v3-2/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── package.json
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── v3-jiti-reexport/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── package.json
│   │   │   ├── packages/
│   │   │   │   └── tailwind-config/
│   │   │   │       ├── dist/
│   │   │   │       │   └── index.mjs
│   │   │   │       ├── package.json
│   │   │   │       └── src/
│   │   │   │           └── index.ts
│   │   │   └── tailwind.config.mjs
│   │   └── v4/
│   │       ├── basic/
│   │       │   ├── app.css
│   │       │   ├── index.html
│   │       │   ├── output.html
│   │       │   └── package.json
│   │       ├── css-loading-js/
│   │       │   ├── app.css
│   │       │   ├── cjs/
│   │       │   │   ├── my-config.cjs
│   │       │   │   └── my-plugin.cjs
│   │       │   ├── esm/
│   │       │   │   ├── my-config.mjs
│   │       │   │   └── my-plugin.mjs
│   │       │   ├── index.html
│   │       │   ├── output.html
│   │       │   ├── package.json
│   │       │   └── ts/
│   │       │       ├── my-config.ts
│   │       │       └── my-plugin.ts
│   │       └── subpath-imports/
│   │           ├── app.css
│   │           ├── index.html
│   │           ├── output.html
│   │           ├── package.json
│   │           └── theme.css
│   ├── fixtures.test.ts
│   ├── format.test.ts
│   ├── plugins.test.ts
│   ├── sorter.test.ts
│   ├── tests.ts
│   └── utils.ts
├── tsconfig.json
├── tsdown.config.ts
└── vitest.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
custom: ['https://tailwindcss.com/sponsor']


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug report
about: If you've already asked for help with a problem and confirmed something is broken with prettier-plugin-tailwindcss itself, create a bug report.
title: ''
labels: ''
assignees: ''
---

<!-- Please provide all of the information requested below. We're a small team and without all of this information it's not possible for us to help and your bug report will be closed. -->

**What version of `prettier-plugin-tailwindcss` are you using?**

For example: v0.1.7

**What version of Tailwind CSS are you using?**

For example: v3.0.22

**What version of Node.js are you using?**

For example: v12.0.0

**What package manager are you using?**

For example: npm, Yarn

**What operating system are you using?**

For example: macOS, Windows

**Reproduction URL**

A 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. Unfortunately we can't provide support without a reproduction, and your issue will be closed with no comment if this is not provided.

**Describe your issue**

Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: Get Help
    url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=help&title=%5BPrettier%20Plugin%5D%20
    about: If you can't get something to work the way you expect, open a question in our discussion forums.
  - name: Feature Request
    url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas&title=%5BPrettier%20Plugin%5D%20
    about: 'Suggest any ideas you have using our discussion forums.'


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

env:
  CI: true
  NODE_VERSION: 22

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Use Node.js ${{ env.NODE_VERSION }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: pnpm

      - name: Install dependencies
        run: pnpm install

      - name: Check formatting
        run: pnpm run format --check

      - name: Lint
        run: pnpm run lint

      - name: Build Prettier Plugin
        run: pnpm run build

      - name: Test against Prettier v3
        run: pnpm run test


================================================
FILE: .github/workflows/prepare-release.yml
================================================
name: Prepare Release

on:
  workflow_dispatch:
  push:
    tags:
      - 'v*'

env:
  CI: true
  NODE_VERSION: 22

permissions:
  contents: read

jobs:
  build:
    permissions:
      contents: write # for softprops/action-gh-release to create GitHub release

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - run: git fetch --tags -f

      - name: Resolve version
        id: vars
        run: |
          echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV

      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Use Node.js ${{ env.NODE_VERSION }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: pnpm
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        run: pnpm install

      - name: Get release notes
        run: |
          RELEASE_NOTES=$(pnpm run release-notes --silent)
          echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
          echo "$RELEASE_NOTES" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV

      - name: Release
        uses: softprops/action-gh-release@v2
        with:
          draft: true
          tag_name: ${{ env.TAG_NAME }}
          body: |
            ${{ env.RELEASE_NOTES }}


================================================
FILE: .github/workflows/release-insiders.yml
================================================
name: Release Insiders

on:
  workflow_dispatch:

permissions:
  contents: read
  id-token: write

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

env:
  CI: true
  NODE_VERSION: 22
  RELEASE_CHANNEL: insiders

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Use Node.js ${{ env.NODE_VERSION }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: pnpm
          registry-url: "https://registry.npmjs.org"

      - name: Install dependencies
        run: pnpm install

      - name: Build Prettier Plugin
        run: pnpm run build

      - name: Test
        run: pnpm run test

      - name: Resolve version
        id: vars
        run: |
          echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV

      - name: "Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}"
        run: pnpm version 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }} --force --no-git-tag-version

      - name: Publish
        run: pnpm publish --provenance --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  release:
    types: [published]

permissions:
  contents: read
  id-token: write

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

env:
  CI: true
  NODE_VERSION: 22

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Use Node.js ${{ env.NODE_VERSION }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: pnpm
          registry-url: "https://registry.npmjs.org"

      - name: Install dependencies
        run: pnpm install

      - name: Build Prettier Plugin
        run: pnpm run build

      - name: Test
        run: pnpm run test

      - name: Calculate environment variables
        run: |
          echo "RELEASE_CHANNEL=$(pnpm run release-channel --silent)" >> $GITHUB_ENV

      - name: Publish
        run: pnpm publish --provenance --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}


================================================
FILE: .gitignore
================================================
node_modules
/dist


================================================
FILE: .oxlintrc.json
================================================
{
  "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
  "rules": {
    "typescript/unbound-method": "off"
  }
}


================================================
FILE: .prettierignore
================================================
tests/fixtures
.github/
dist/
README.md


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- Remove top-level await ([#420](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/420))
- Improve load-time performance ([#420](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/420))

### Fixed

- Collapse whitespace in template literals with adjacent quasis ([#427](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/427))

## [0.7.2] - 2025-12-01

### Fixed

- Load compatible plugins sequentially to work around race conditions in Node.js ([#412](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/412))
- Fix compatibility with `prettier-plugin-svelte` when using Prettier v3.7+ ([#418](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/418))

## [0.7.1] - 2025-10-17

### Fixed

- Match against correct name of dynamic attributes when using regexes ([#410](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/410))

## [0.7.0] - 2025-10-14

### Added

- Format quotes in `@source`, `@plugin`, and `@config` ([#387](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/387))
- Sort in function calls in Twig ([#358](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/358))
- Sort in callable template literals ([#367](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/367))
- Sort in function calls mixed with property accesses ([#367](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/367))
- Support regular expression patterns for attributes ([#405](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/405))
- Support regular expression patterns for function names ([#405](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/405))

### Changed
- Improved monorepo support by loading Tailwind CSS relative to the input file instead of prettier config file ([#386](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/386))
- Improved monorepo support by loading v3 configs relative to the input file instead of prettier config file ([#386](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/386))
- Fallback to Tailwind CSS v4 instead of v3 by default ([#390](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/390))
- Don't augment global Prettier `ParserOptions` and `RequiredOptions` types ([#354](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/354))
- Drop support for `prettier-plugin-import-sort` ([#385](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/385))

### Fixed
- Handle quote escapes in LESS when sorting `@apply` ([#392](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/392))
- Fix whitespace removal inside nested concat and template expressions ([#396](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/396))

## [0.6.14] - 2025-07-09

- Add support for OXC + Hermes Prettier plugins ([#376](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/376), [#380](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/380))
- Sort template literals in Angular expressions ([#377](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/377))
- Don't repeatedly add backslashes to escape sequences when formatting ([#381](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/381))

## [0.6.13] - 2025-06-19

- Prevent Svelte files from breaking when there are duplicate classes ([#359](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/359))
- Ensure `prettier-plugin-multiline-arrays` and `prettier-plugin-jsdoc` work when used together with this plugin ([#372](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/372))

## [0.6.12] - 2025-05-30

- Add internal (unsupported) option to load Tailwind CSS using a different package name ([#366](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/366))

## [0.6.11] - 2025-01-23

- Support TypeScript configs and plugins when using v4 ([#342](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/342))

## [0.6.10] - 2025-01-15

- Add support for `@zackad/prettier-plugin-twig` ([#327](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/327))
- Drop support for `@zackad/prettier-plugin-twig-melody` ([#327](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/327))
- Update Prettier options types ([#325](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/325))
- Don't remove whitespace inside template literals in Svelte ([#332](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/332))

## [0.6.9] - 2024-11-19

- Introduce `tailwindStylesheet` option to replace `tailwindEntryPoint` ([#330](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/330))

## [0.6.8] - 2024-09-24

- Fix crash ([#320](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/320))

## [0.6.7] - 2024-09-24

- Improved performance with large Svelte, Liquid, and Angular files ([#312](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/312))
- Add support for `@plugin` and `@config` in v4 ([#316](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/316))
- Add support for Tailwind CSS v4.0.0-alpha.25 ([#317](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/317))

## [0.6.6] - 2024-08-09

- Add support for `prettier-plugin-multiline-arrays` ([#299](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/299))
- Add resolution cache for known plugins ([#301](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/301))
- Support Tailwind CSS `v4.0.0-alpha.19` ([#310](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/310))

## [0.6.5] - 2024-06-17

- Only re-apply string escaping when necessary ([#295](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/295))

## [0.6.4] - 2024-06-12

- Export `PluginOptions` type ([#292](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/292))

## [0.6.3] - 2024-06-11

- Improve detection of string concatenation ([#288](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/288))

## [0.6.2] - 2024-06-07

### Changed

- Only remove duplicate Tailwind classes ([#277](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/277))
- Make sure escapes in classes are preserved in string literals ([#286](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/286))

## [0.6.1] - 2024-05-31

### Added

- Add new `tailwindPreserveDuplicates` option to disable removal of duplicate classes ([#276](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/276))

### Fixed

- Improve handling of whitespace removal when concatenating strings ([#276](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/276))
- Fix a bug where Angular expressions may produce invalid code after sorting ([#276](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/276))
- Disabled whitespace and duplicate class removal for Liquid and Svelte ([#276](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/276))

## [0.6.0] - 2024-05-30

### Changed

- Remove duplicate classes ([#272](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/272))
- Remove extra whitespace around classes ([#272](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/272))

## [0.5.14] - 2024-04-15

### Fixed

- Fix detection of v4 projects on Windows ([#265](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/265))

## [0.5.13] - 2024-03-27

### Added

- Add support for `@zackad/prettier-plugin-twig-melody` ([#255](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/255))

## [0.5.12] - 2024-03-06

### Added

- Add support for `prettier-plugin-sort-imports` ([#241](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/241))
- Add support for Tailwind CSS v4.0 ([#249](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/249))

## [0.5.11] - 2024-01-05

### Changed

- Bumped bundled version of Tailwind CSS to v3.4.1 ([#240](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/240))

## [0.5.10] - 2023-12-28

### Changed

- Bumped bundled version of Tailwind CSS to v3.4 ([#235](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/235))

## [0.5.9] - 2023-12-05

### Fixed

- Fixed location of embedded preflight CSS file ([#231](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/231))

## [0.5.8] - 2023-12-05

### Added

- Re-enable support for `prettier-plugin-marko` ([#229](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/229))

## [0.5.7] - 2023-11-08

### Fixed

- Fix sorting inside dynamic custom attributes ([#225](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/225))

## [0.5.6] - 2023-10-12

### Fixed

- Fix sorting inside `{{ … }}` expressions when using `@shopify/prettier-plugin-liquid` v1.3+ ([#222](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/222))

## [0.5.5] - 2023-10-03

### Fixed

- Sort classes inside `className` in Astro ([#215](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/215))
- Support member access on function calls ([#218](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/218))

## [0.5.4] - 2023-08-31

### Fixed

- Type `tailwindFunctions` and `tailwindAttributes` as optional ([#206](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/206))
- Don’t break `@apply … #{'!important'}` sorting in SCSS ([#212](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/212))

## [0.5.3] - 2023-08-15

### Fixed

- Fix CJS `__dirname` interop on Windows ([#204](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/204))

## [0.5.2] - 2023-08-11

### Fixed

- Fix intertop with bundled CJS dependencies ([#199](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/199))

## [0.5.1] - 2023-08-10

### Fixed

- Updated Prettier peer dependency ([#197](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/197))

## [0.5.0] - 2023-08-10

### Added

- Sort expressions in Astro's `class:list` attribute ([#192](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/192))
- Re-enabled support for plugins when using Prettier v3+ ([#195](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/195))

## [0.4.1] - 2023-07-14

### Fixed

- Don't move partial classes inside Twig attributes ([#184](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/184))

## [0.4.0] - 2023-07-11

### Added

- Export types for Prettier config ([#162](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/162))
- Add Prettier v3 support ([#179](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/179))

### Fixed

- Don't move partial classes inside Liquid script attributes ([#164](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/164))
- Do not split classes by non-ASCII whitespace ([#166](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/166))
- Match tagged template literals with tag expressions ([#169](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/169))

## [0.3.0] - 2023-05-15

### Added

- Added support for `prettier-plugin-marko` ([#151](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/151))
- Allow sorting of custom attributes, functions, and tagged template literals ([#155](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/155))

### Fixed

- Speed up formatting ([#153](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/153))
- Fix plugin compatibility when loaded with require ([#159](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/159))

## [0.2.8] - 2023-04-28

### Changed

- Remove support for `@prettier/plugin-php` ([#152](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/152))

## [0.2.7] - 2023-04-05

### Fixed

- Don't break liquid tags inside attributes when sorting classes ([#143](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/143))

## [0.2.6] - 2023-03-29

### Added

- Support ESM and TS config files ([#137](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/137))

### Fixed

- Load `tailwindcss` modules from nearest instance only ([#139](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/139))

## [0.2.5] - 2023-03-17

### Fixed

- Fix class sorting in `capture` liquid tag ([#131](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/131))

## [0.2.4] - 2023-03-02

### Fixed

- Sort `class` attribute on components and custom elements in Astro ([#129](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/129))

## [0.2.3] - 2023-02-15

### Fixed

- Don't sort classes in Glimmer `concat` helper ([#119](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/119))
- Add support for `@ianvs/prettier-plugin-sort-imports` ([#122](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/122))

## [0.2.2] - 2023-01-24

### Fixed

- Add prettier plugins to peer dependencies ([#114](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/114))
- Traverse await expression blocks in Svelte ([#118](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/118))

## [0.2.1] - 2022-12-08

### Fixed

- Fix support for latest Shopify Liquid plugin ([#109](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/109))

## [0.2.0] - 2022-11-25

### Changed

- Don't bundle `prettier-plugin-svelte` ([#101](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/101))

### Added

- Improve compatibility with other Prettier plugins ([#101](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/101), [#102](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/102))

## [0.1.13] - 2022-07-25

### Fixed

- Fix error when using Angular pipes ([#86](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/86))

## [0.1.12] - 2022-07-07

### Added

- Add support for Glimmer / Handlebars ([#83](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/83))

## [0.1.11] - 2022-05-16

### Changed

- Update `prettier-plugin-svelte` to `v2.7.0` ([#77](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/77))

### Fixed

- Fix sorting in Svelte `:else` blocks ([#79](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/79))

## [0.1.10] - 2022-04-20

### Removed

- Remove whitespace tidying and duplicate class removal due to [issues](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/71) with whitespace removal ([#72](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/72))

## [0.1.9] - 2022-04-19

### Added

- Add license file ([#64](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/64))
- Add whitespace tidying and duplicate class removal ([#70](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/70))

## [0.1.8] - 2022-02-24

### Changed

- Use Tailwind's `getClassOrder` API when available ([#57](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/57))

### Fixed

- Fix Tailwind config file resolution when Prettier config file is not present ([#62](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/62))

## [0.1.7] - 2022-02-09

### Fixed

- Fix single quotes being converted to double quotes ([#51](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/51))

## [0.1.6] - 2022-02-08

### Fixed

- Fix error when no Prettier options provided ([#46](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/46))

## [0.1.5] - 2022-02-04

### Added

- Add support for MDX ([#30](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/30))

### Fixed

- Fix error when formatting Svelte files that contain `let:class` attributes ([#24](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/24))

## [0.1.4] - 2022-01-25

### Fixed

- Handle empty class attributes ([#17](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/17))
- Handle TypeScript syntax in Vue/Angular class attributes ([#18](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/18))

## [0.1.3] - 2022-01-24

### Fixed

- Ignore `!important` when sorting `@apply` classes ([#4](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/4))

## [0.1.2] - 2022-01-24

### Fixed

- Fix error when using nullish coalescing operator in Vue/Angular ([#2](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/2))

[unreleased]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.7.2...HEAD
[0.7.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.14...v0.7.0
[0.6.14]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.13...v0.6.14
[0.6.13]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.12...v0.6.13
[0.6.12]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.11...v0.6.12
[0.6.11]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.10...v0.6.11
[0.6.10]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.9...v0.6.10
[0.6.9]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.8...v0.6.9
[0.6.8]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.7...v0.6.8
[0.6.7]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.6...v0.6.7
[0.6.6]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.5...v0.6.6
[0.6.5]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.4...v0.6.5
[0.6.4]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.3...v0.6.4
[0.6.3]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.2...v0.6.3
[0.6.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.1...v0.6.2
[0.6.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.14...v0.6.0
[0.5.14]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.13...v0.5.14
[0.5.13]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.12...v0.5.13
[0.5.12]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.11...v0.5.12
[0.5.11]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.10...v0.5.11
[0.5.10]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.9...v0.5.10
[0.5.9]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.8...v0.5.9
[0.5.8]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.7...v0.5.8
[0.5.7]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.6...v0.5.7
[0.5.6]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.5...v0.5.6
[0.5.5]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.4...v0.5.5
[0.5.4]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.3...v0.5.4
[0.5.3]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.2...v0.5.3
[0.5.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.4.1...v0.5.0
[0.4.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.8...v0.3.0
[0.2.8]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.7...v0.2.8
[0.2.7]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.6...v0.2.7
[0.2.6]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.5...v0.2.6
[0.2.5]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.4...v0.2.5
[0.2.4]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.3...v0.2.4
[0.2.3]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.2...v0.2.3
[0.2.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.1...v0.2.2
[0.2.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.13...v0.2.0
[0.1.13]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.12...v0.1.13
[0.1.12]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.11...v0.1.12
[0.1.11]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.10...v0.1.11
[0.1.10]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.9...v0.1.10
[0.1.9]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.8...v0.1.9
[0.1.8]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.7...v0.1.8
[0.1.7]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.6...v0.1.7
[0.1.6]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.5...v0.1.6
[0.1.5]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.4...v0.1.5
[0.1.4]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/d9c27f07a69bf9feec7f9d889426ad2ba76e1b09...v0.1.2


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) Tailwind Labs Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<img src="https://raw.githubusercontent.com/tailwindlabs/prettier-plugin-tailwindcss/main/.github/banner.jpg" alt="prettier-plugin-tailwindcss" />

A [Prettier v3+](https://prettier.io/) plugin for Tailwind CSS v3.0+ that automatically sorts classes based on [our recommended class order](https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted).

## Installation

To get started, install `prettier-plugin-tailwindcss` as a dev-dependency:

```sh
npm install -D prettier prettier-plugin-tailwindcss
```

Then add the plugin to your [Prettier configuration](https://prettier.io/docs/en/configuration.html):

```json5
// .prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"]
}
```

When using a JavaScript config, you can import the types for IntelliSense:

```js
// prettier.config.js

/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
export default {
  plugins: ["prettier-plugin-tailwindcss"],
}
```

## Upgrading to v0.5.x

As of v0.5.x, this plugin now requires Prettier v3 and is ESM-only. This means it cannot be loaded via `require()`. For more information see our [upgrade guide](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/207#issuecomment-1698071122).

## Options

### Specifying your Tailwind stylesheet path (Tailwind CSS v4+)

When using Tailwind CSS v4 you must specify your CSS file entry point, which includes your theme, custom utilities, and other Tailwind configuration options. To do this, use the `tailwindStylesheet` option in your Prettier configuration.

Note that paths are resolved relative to the Prettier configuration file.

```json5
// .prettierrc
{
  "tailwindStylesheet": "./resources/css/app.css"
}
```

### Specifying your Tailwind JavaScript config path (Tailwind CSS v3)

To ensure that the class sorting takes into consideration any of your project's Tailwind customizations, it needs access to your [Tailwind configuration file](https://tailwindcss.com/docs/configuration) (`tailwind.config.js`).

By default the plugin will look for this file in the same directory as your Prettier configuration file. However, if your Tailwind configuration is somewhere else, you can specify this using the `tailwindConfig` option in your Prettier configuration.

Note that paths are resolved relative to the Prettier configuration file.

```json5
// .prettierrc
{
  "tailwindConfig": "./styles/tailwind.config.js"
}
```

If a local configuration file cannot be found the plugin will fallback to the default Tailwind configuration.

## Sorting non-standard attributes

By default this plugin sorts classes in the `class` attribute, any framework-specific equivalents like `className`, `:class`, `[ngClass]`, and any Tailwind `@apply` directives.

You can sort additional attributes using the `tailwindAttributes` option, which takes an array of attribute names:

```json5
// .prettierrc
{
  "tailwindAttributes": ["myClassList"]
}
```

With this configuration, any classes found in the `myClassList` attribute will be sorted:

```jsx
function MyButton({ children }) {
  return (
    <button myClassList="rounded bg-blue-500 px-4 py-2 text-base text-white">
      {children}
    </button>
  );
}
```

### Using regex patterns

You can also use regular expressions to match multiple attributes. Patterns should be enclosed in forward slashes. Note that JS regex literals are not supported with Prettier:

```json5
// .prettierrc
{
  "tailwindAttributes": ["myClassList", "/data-.*/"]
}
```

This example will sort classes in the `myClassList` attribute as well as any attribute starting with `data-`:

```jsx
function MyButton({ children }) {
  return (
    <button
      myClassList="rounded bg-blue-500 px-4 py-2 text-base text-white"
      data-theme="dark:bg-gray-800 bg-white"
      data-classes="flex items-center"
    >
      {children}
    </button>
  );
}
```

## Sorting classes in function calls

In addition to sorting classes in attributes, you can also sort classes in strings provided to function calls. This is useful when working with libraries like [clsx](https://github.com/lukeed/clsx) or [cva](https://cva.style/).

You can sort classes in function calls using the `tailwindFunctions` option, which takes a list of function names:

```json5
// .prettierrc
{
  "tailwindFunctions": ["clsx"]
}
```

With this configuration, any classes in `clsx()` function calls will be sorted:

```jsx
import clsx from 'clsx'

function MyButton({ isHovering, children }) {
  let classes = clsx(
    'rounded bg-blue-500 px-4 py-2 text-base text-white',
    {
      'bg-blue-700 text-gray-100': isHovering,
    },
  )

  return (
    <button className={classes}>
      {children}
    </button>
  )
}
```

## Sorting classes in template literals

This plugin also enables sorting of classes in tagged template literals.

You can sort classes in template literals using the `tailwindFunctions` option, which takes a list of function names:

```json5
// .prettierrc
{
  "tailwindFunctions": ["tw"],
}
```

With this configuration, any classes in template literals tagged with `tw` will automatically be sorted:

```jsx
import { View, Text } from 'react-native'
import tw from 'twrnc'

function MyScreen() {
  return (
    <View style={tw`bg-white p-4 dark:bg-black`}>
      <Text style={tw`text-md text-black dark:text-white`}>Hello World</Text>
    </View>
  )
}
```

This feature can be used with third-party libraries like `twrnc` or you can create your own tagged template by defining this "identity" function:

```js
const tw = (strings, ...values) => String.raw({ raw: strings }, ...values)
```

Once added, tag your strings with the function and the plugin will sort them:

```js
const mySortedClasses = tw`bg-white p-4 dark:bg-black`
```

## Public API

If you want to use the Tailwind class sorting logic outside of Prettier, import from the
`sorter` entrypoint:

```js
import { createSorter } from 'prettier-plugin-tailwindcss/sorter'

let sorter = await createSorter({
  base: '/path/to/project',
  stylesheetPath: './app.css',
})

// Sort HTML class attributes (space-separated strings)
let sorted = sorter.sortClassAttributes([
  'sm:bg-tomato bg-red-500',
  'p-4 m-2'
])
// Returns: ['bg-red-500 sm:bg-tomato', 'm-2 p-4']

// Sort class lists (arrays of class names)
let sortedLists = sorter.sortClassLists([
  ['sm:bg-tomato', 'bg-red-500'],
  ['p-4', 'm-2']
])
// Returns: [['bg-red-500', 'sm:bg-tomato'], ['m-2', 'p-4']]
```

### API Options

The `createSorter` function accepts the following options:

- **`base`** (optional): The directory used to resolve relative file paths. Defaults to the current working directory.
- **`filepath`** (optional): The path to the file being formatted. When provided, Tailwind CSS is resolved relative to this path.
- **`configPath`** (optional): Path to the Tailwind CSS config file (v3). Paths are resolved relative to `base`.
- **`stylesheetPath`** (optional): Path to the CSS stylesheet used by Tailwind CSS (v4+). Paths are resolved relative to `base`.
- **`preserveWhitespace`** (optional): Whether to preserve whitespace around classes. Default: `false`.
- **`preserveDuplicates`** (optional): Whether to preserve duplicate classes. Default: `false`.

### Sorter Methods

The sorter object returned by `createSorter` has two methods:

- **`sortClassAttributes(classes: string[]): string[]`**
  Sorts one or more HTML class attributes. Each element should be a space-separated string of class names (like the value of an HTML `class` attribute).

- **`sortClassLists(classes: string[][]): (string | null)[][]`**
  Sorts one or more class lists. Each element should be an array of individual class names. When removing duplicates (default behavior), duplicate classes are replaced with `null` in the output.

### Using regex patterns

Like the `tailwindAttributes` option, the `tailwindFunctions` option also supports regular expressions to match multiple function names. Patterns should be enclosed in forward slashes. Note that JS regex literals are not supported with Prettier.

## Preserving whitespace

This plugin automatically removes unnecessary whitespace between classes to ensure consistent formatting. If you prefer to preserve whitespace, you can use the `tailwindPreserveWhitespace` option:

```json5
// .prettierrc
{
  "tailwindPreserveWhitespace": true,
}
```

With this configuration, any whitespace surrounding classes will be preserved:

```jsx
import clsx from 'clsx'

function MyButton({ isHovering, children }) {
  return (
    <button className=" rounded  bg-blue-500 px-4  py-2     text-base text-white ">
      {children}
    </button>
  )
}
```

## Preserving duplicate classes

This plugin automatically removes duplicate classes from your class lists. However, this can cause issues in some templating languages, like Fluid or Blade, where we can't distinguish between classes and the templating syntax.

If removing duplicate classes is causing issues in your project, you can use the `tailwindPreserveDuplicates` option to disable this behavior:

```json5
// .prettierrc
{
  "tailwindPreserveDuplicates": true,
}
```

With this configuration, anything we perceive as duplicate classes will be preserved:

```html
<div
  class="
    {f:if(condition: isCompact, then: 'grid-cols-3', else: 'grid-cols-5')}
    {f:if(condition: isDark, then: 'bg-black/50', else: 'bg-white/50')}
    grid gap-4 p-4
  "
>
</div>
```

## Compatibility with other Prettier plugins

This plugin uses Prettier APIs that can only be used by one plugin at a time, making it incompatible with other Prettier plugins implemented the same way. To solve this we've added explicit per-plugin workarounds that enable compatibility with the following Prettier plugins:

- `@ianvs/prettier-plugin-sort-imports`
- `@prettier/plugin-pug`
- `@shopify/prettier-plugin-liquid`
- `@trivago/prettier-plugin-sort-imports`
- `prettier-plugin-astro`
- `prettier-plugin-css-order`
- `prettier-plugin-jsdoc`
- `prettier-plugin-multiline-arrays`
- `prettier-plugin-organize-attributes`
- `prettier-plugin-organize-imports`
- `prettier-plugin-svelte`
- `prettier-plugin-sort-imports`

One limitation with this approach is that `prettier-plugin-tailwindcss` *must* be loaded last.

```json5
// .prettierrc
{
  // ..
  "plugins": [
    "prettier-plugin-svelte",
    "prettier-plugin-organize-imports",
    "prettier-plugin-tailwindcss" // MUST come last
  ]
}
```


================================================
FILE: knip.json
================================================
{
  "$schema": "https://unpkg.com/knip@5/schema.json",
  "entry": ["scripts/*.js"],
  "project": ["src/**/*.ts", "scripts/**/*.js", "tests/**/*.ts"],
  "ignore": ["src/**/*.d.ts", "tests/fixtures/**"],
  "ignoreDependencies": [
    "@ianvs/prettier-plugin-sort-imports",
    "@prettier/plugin-oxc",
    "@prettier/plugin-hermes",
    "@prettier/plugin-pug",
    "@shopify/prettier-plugin-liquid",
    "@trivago/prettier-plugin-sort-imports",
    "@zackad/prettier-plugin-twig",
    "prettier-plugin-astro",
    "prettier-plugin-css-order",
    "prettier-plugin-jsdoc",
    "prettier-plugin-marko",
    "prettier-plugin-multiline-arrays",
    "prettier-plugin-organize-attributes",
    "prettier-plugin-organize-imports",
    "prettier-plugin-sort-imports",
    "prettier-plugin-svelte",
    "import-sort-style-module",
    "marko",
    "postcss",
    "postcss-import"
  ],
  "rules": {
    "exports": "off",
    "types": "off"
  }
}


================================================
FILE: package.json
================================================
{
  "type": "module",
  "name": "prettier-plugin-tailwindcss",
  "version": "0.7.2",
  "description": "A Prettier plugin for sorting Tailwind CSS classes.",
  "license": "MIT",
  "main": "dist/index.mjs",
  "module": "dist/index.mjs",
  "types": "dist/index.d.mts",
  "files": [
    "dist"
  ],
  "exports": {
    ".": {
      "types": "./dist/index.d.mts",
      "import": "./dist/index.mjs",
      "default": "./dist/index.mjs"
    },
    "./sorter": {
      "types": "./dist/sorter.d.mts",
      "import": "./dist/sorter.mjs",
      "default": "./dist/sorter.mjs"
    }
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/tailwindlabs/prettier-plugin-tailwindcss"
  },
  "bugs": {
    "url": "https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues"
  },
  "scripts": {
    "build": "tsdown",
    "dev": "tsdown --watch",
    "pretest": "node scripts/install-fixture-deps.js",
    "test": "vitest",
    "prepublishOnly": "npm run build && node scripts/copy-licenses.js",
    "format": "prettier \"src/**/*.ts\" \"scripts/**/*.js\" \"tests/*.ts\" --write --print-width 100 --single-quote --no-semi",
    "lint": "knip && oxlint --type-aware src tests/*.ts",
    "release-channel": "node ./scripts/release-channel.js",
    "release-notes": "node ./scripts/release-notes.js"
  },
  "devDependencies": {
    "@babel/types": "^7.28.2",
    "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
    "@marko/compiler": "^5.39.49",
    "@prettier/plugin-hermes": "^0.1.2",
    "@prettier/plugin-oxc": "^0.1.2",
    "@prettier/plugin-pug": "^3.4.2",
    "@shopify/prettier-plugin-liquid": "^1.10.0",
    "@trivago/prettier-plugin-sort-imports": "^6.0.0",
    "@types/node": "^24.3.0",
    "@zackad/prettier-plugin-twig": "^0.16.0",
    "clear-module": "^4.1.2",
    "dedent": "^1.6.0",
    "enhanced-resolve": "^5.18.3",
    "escalade": "^3.2.0",
    "fast-glob": "^3.3.3",
    "import-sort-style-module": "^6.0.0",
    "jiti": "^2.6.0",
    "knip": "^5.83.0",
    "license-checker": "^25.0.1",
    "line-column": "^1.0.2",
    "marko": "^5.37.46",
    "oxlint": "^1.43.0",
    "oxlint-tsgolint": "^0.11.4",
    "postcss": "^8.5.6",
    "postcss-import": "^16.1.1",
    "prettier": "^3.7.3",
    "prettier-plugin-astro": "^0.14.1",
    "prettier-plugin-css-order": "^2.1.2",
    "prettier-plugin-jsdoc": "^1.3.3",
    "prettier-plugin-marko": "^3.3.2",
    "prettier-plugin-multiline-arrays": "^4.0.3",
    "prettier-plugin-organize-attributes": "^1.0.0",
    "prettier-plugin-organize-imports": "^4.2.0",
    "prettier-plugin-sort-imports": "^1.8.8",
    "prettier-plugin-svelte": "^3.4.0",
    "pug-lexer": "^5.0.1",
    "svelte": "^5.38.2",
    "tailwindcss-v3": "npm:tailwindcss@^3.4.18",
    "tailwindcss-v4": "npm:tailwindcss@^4.1.14",
    "tsdown": "^0.20.1",
    "vitest": "^3.2.4"
  },
  "peerDependencies": {
    "@ianvs/prettier-plugin-sort-imports": "*",
    "@prettier/plugin-hermes": "*",
    "@prettier/plugin-oxc": "*",
    "@prettier/plugin-pug": "*",
    "@shopify/prettier-plugin-liquid": "*",
    "@trivago/prettier-plugin-sort-imports": "*",
    "@zackad/prettier-plugin-twig": "*",
    "prettier": "^3.0",
    "prettier-plugin-astro": "*",
    "prettier-plugin-css-order": "*",
    "prettier-plugin-jsdoc": "*",
    "prettier-plugin-marko": "*",
    "prettier-plugin-multiline-arrays": "*",
    "prettier-plugin-organize-attributes": "*",
    "prettier-plugin-organize-imports": "*",
    "prettier-plugin-sort-imports": "*",
    "prettier-plugin-svelte": "*"
  },
  "peerDependenciesMeta": {
    "@ianvs/prettier-plugin-sort-imports": {
      "optional": true
    },
    "@prettier/plugin-hermes": {
      "optional": true
    },
    "@prettier/plugin-oxc": {
      "optional": true
    },
    "@prettier/plugin-pug": {
      "optional": true
    },
    "@shopify/prettier-plugin-liquid": {
      "optional": true
    },
    "@trivago/prettier-plugin-sort-imports": {
      "optional": true
    },
    "@zackad/prettier-plugin-twig": {
      "optional": true
    },
    "prettier-plugin-astro": {
      "optional": true
    },
    "prettier-plugin-css-order": {
      "optional": true
    },
    "prettier-plugin-jsdoc": {
      "optional": true
    },
    "prettier-plugin-marko": {
      "optional": true
    },
    "prettier-plugin-multiline-arrays": {
      "optional": true
    },
    "prettier-plugin-organize-attributes": {
      "optional": true
    },
    "prettier-plugin-organize-imports": {
      "optional": true
    },
    "prettier-plugin-sort-imports": {
      "optional": true
    },
    "prettier-plugin-svelte": {
      "optional": true
    }
  },
  "engines": {
    "node": ">=20.19"
  },
  "importSort": {
    ".js, .jsx, .ts, .tsx": {
      "style": "module"
    }
  },
  "jest": {
    "testTimeout": 15000
  },
  "overrides": {
    "@ianvs/prettier-plugin-sort-imports": {
      "@prettier/plugin-oxc": "^0.1.2"
    }
  },
  "packageManager": "pnpm@10.28.2"
}


================================================
FILE: prettier.config.js
================================================
/** @type {import('prettier').Config} */
export default {
  semi: false,
  singleQuote: true,
  trailingComma: 'all',
  plugins: ['@ianvs/prettier-plugin-sort-imports'],
  importOrder: ['^@', '^[a-zA-Z0-9-]+', '^[./]'],
  printWidth: 120,
}


================================================
FILE: scripts/copy-licenses.js
================================================
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
import checker from 'license-checker'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const pkg = JSON.parse(await fs.readFile(path.resolve(__dirname, '../package.json'), 'utf8'))

let exclude = ['vitest', 'license-checker', 'prettier', 'svelte', 'knip']

/** @type {checker.ModuleInfo} */
let packages = await new Promise((resolve, reject) => {
  checker.init({ start: path.resolve(__dirname, '..') }, (_err, packages) => {
    if (_err) {
      reject(_err)
    } else {
      resolve(packages)
    }
  })
})

for (let key in packages) {
  let dep = packages[key]
  let name = key.split(/(?<=.)@/)[0]

  if (exclude.includes(name)) continue
  if (!dep.licenseFile) continue
  if (!(name in pkg.devDependencies)) continue

  let dir = path.resolve(__dirname, '../dist/licenses', name)
  await fs.mkdir(dir, { recursive: true })
  await fs.copyFile(dep.licenseFile, path.resolve(dir, path.basename(dep.licenseFile)))
}


================================================
FILE: scripts/install-fixture-deps.js
================================================
import { exec } from 'node:child_process'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import glob from 'fast-glob'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const fixtures = glob.sync(
  [
    'tests/fixtures/*/package.json',
    'tests/fixtures/v4/*/package.json',
    'tests/fixtures/monorepo/*/package.json',
  ],
  {
    cwd: path.resolve(__dirname, '..'),
  },
)

const execAsync = promisify(exec)

await Promise.all(
  fixtures.map(async (fixture) => {
    console.log(`Installing dependencies for ${fixture}`)

    await execAsync('npm install', { cwd: path.dirname(fixture) })
  }),
)


================================================
FILE: scripts/release-channel.js
================================================
// Given a version, figure out what the release channel is so that we can publish to the correct
// channel on npm.
//
// E.g.:
//
//   1.2.3                  -> latest (default)
//   0.0.0-insiders.ffaa88  -> insiders
//   4.1.0-alpha.4          -> alpha
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const pkg = JSON.parse(await fs.readFile(path.resolve(__dirname, '../package.json'), 'utf8'))

let version = process.argv[2] || process.env.npm_package_version || pkg.version

let match = /\d+\.\d+\.\d+-(.*)\.\d+/g.exec(version)
if (match) {
  console.log(match[1])
} else {
  console.log('latest')
}


================================================
FILE: scripts/release-notes.js
================================================
// Given a version, figure out what the release notes are so that we can use this to pre-fill the
// relase notes on a GitHub release for the current version.

import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const pkg = JSON.parse(await fs.readFile(path.resolve(__dirname, '../package.json'), 'utf8'))

let version = process.argv[2] || process.env.npm_package_version || pkg.version

let changelog = await fs.readFile(path.resolve(__dirname, '..', 'CHANGELOG.md'), 'utf8')
let match = new RegExp(
  `## \\[${version}\\] - (.*)\\n\\n([\\s\\S]*?)\\n(?:(?:##\\s)|(?:\\[))`,
  'g',
).exec(changelog)

if (match) {
  let [, , notes] = match
  console.log(notes.trim())
} else {
  console.log(`Placeholder release notes for version: v${version}`)
}


================================================
FILE: src/config.ts
================================================
// @ts-check
import * as path from 'node:path'
import prettier from 'prettier'
import type { ParserOptions } from 'prettier'
import * as console from './console'
import { expiringMap } from './expiring-map.js'
import { getTailwindConfig as getTailwindConfigFromLib } from './sorter.js'
import type { UnifiedApi } from './types'
import { cacheForDirs } from './utils.js'

let prettierConfigCache = expiringMap<string, string | null>(10_000)

async function resolvePrettierConfigDir(
  filePath: string,
  inputDir: string,
): Promise<string> {
  // Check cache for this directory
  let cached = prettierConfigCache.get(inputDir)
  if (cached !== undefined) {
    return cached ?? process.cwd()
  }

  const resolve = async () => {
    try {
      return await prettier.resolveConfigFile(filePath)
    } catch (err) {
      console.error('prettier-config-not-found', 'Failed to resolve Prettier Config')
      console.error('prettier-config-not-found-err', err)
      return null
    }
  }

  let prettierConfig = await resolve()

  // Cache all directories from inputDir up to config location
  if (prettierConfig) {
    let configDir = path.dirname(prettierConfig)
    cacheForDirs(prettierConfigCache, inputDir, configDir, configDir)
    return configDir
  } else {
    prettierConfigCache.set(inputDir, null)
    return process.cwd()
  }
}

export async function getTailwindConfig(options: ParserOptions): Promise<UnifiedApi> {
  let cwd = process.cwd()
  let inputDir = options.filepath ? path.dirname(options.filepath) : cwd

  // Only resolve prettier config dir if we need it for relative path resolution
  let needsPrettierConfig =
    (options.tailwindConfig && !path.isAbsolute(options.tailwindConfig)) ||
    (options.tailwindStylesheet && !path.isAbsolute(options.tailwindStylesheet)) ||
    (options.tailwindEntryPoint && !path.isAbsolute(options.tailwindEntryPoint))

  let configDir: string
  if (needsPrettierConfig) {
    configDir = await resolvePrettierConfigDir(options.filepath, inputDir)
  } else {
    configDir = cwd
  }

  let configPath =
    options.tailwindConfig && !options.tailwindConfig.endsWith('.css')
      ? options.tailwindConfig
      : undefined

  let stylesheetPath = options.tailwindStylesheet
  if (!stylesheetPath && options.tailwindEntryPoint) {
    console.warn(
      'entrypoint-is-deprecated',
      configDir,
      'Deprecated: Use the `tailwindStylesheet` option for v4 projects instead of `tailwindEntryPoint`.',
    )
    stylesheetPath = options.tailwindEntryPoint
  }

  if (!stylesheetPath && options.tailwindConfig && options.tailwindConfig.endsWith('.css')) {
    console.warn(
      'config-as-css-is-deprecated',
      configDir,
      'Deprecated: Use the `tailwindStylesheet` option for v4 projects instead of `tailwindConfig`.',
    )
    stylesheetPath = options.tailwindConfig
  }

  return getTailwindConfigFromLib({
    base: configDir,
    filepath: options.filepath,
    configPath,
    stylesheetPath,
    packageName: options.tailwindPackageName,
  })
}


================================================
FILE: src/console.ts
================================================
let seen = new Set<string>()

export function log(key: string, arg: unknown, ...args: unknown[]) {
  if (seen.has(key)) return
  seen.add(key)
  console.log(arg, ...args)
}

export function warn(key: string, arg: unknown, ...args: unknown[]) {
  if (seen.has(key)) return
  seen.add(key)
  console.warn(arg, ...args)
}

export function error(key: string, arg: unknown, ...args: unknown[]) {
  if (seen.has(key)) return
  seen.add(key)
  console.error(arg, ...args)
}


================================================
FILE: src/create-plugin.ts
================================================
import { isAbsolute } from 'path'
import type { Parser, ParserOptions, Plugin, Printer } from 'prettier'
import { getTailwindConfig } from './config'
import { createMatcher } from './options'
import { loadIfExists, maybeResolve } from './resolve'
import type { PluginLoad, TransformOptions } from './transform'
import type { TransformerEnv } from './types'

export function createPlugin(transforms: TransformOptions<any>[]) {
  // Prettier parsers and printers may be async functions at definition time.
  // They'll be awaited when the plugin is loaded but must also be swapped out
  // with the resolved value before returning as later Prettier internals
  // assume that parsers and printers are objects and not functions.
  type Init<T> = (() => Promise<T | undefined>) | T | undefined

  let parsers: Record<string, Init<Parser<any>>> = Object.create(null)
  let printers: Record<string, Init<Printer<any>>> = Object.create(null)

  for (let opts of transforms) {
    for (let [name, meta] of Object.entries(opts.parsers)) {
      parsers[name] = async () => {
        let plugin = await loadPlugins(meta.load ?? opts.load ?? [])
        let original = plugin.parsers?.[name]
        if (!original) return

        parsers[name] = await createParser({
          name,
          original,
          opts,
        })

        return parsers[name]
      }
    }

    for (let [name, _meta] of Object.entries(opts.printers ?? {})) {
      printers[name] = async () => {
        let plugin = await loadPlugins(opts.load ?? [])
        let original = plugin.printers?.[name]
        if (!original) return

        printers[name] = createPrinter({
          original,
          opts,
        })

        return printers[name]
      }
    }
  }

  return { parsers, printers }
}

async function createParser({
  name,
  original,
  opts,
}: {
  name: string
  original: Parser<any>
  opts: TransformOptions<any>
}) {
  let parser: Parser<any> = { ...original }

  async function load(options: ParserOptions<any>) {
    let parser: Parser<any> = { ...original }

    for (const pluginName of opts.compatible || []) {
      let plugin = await findEnabledPlugin(options, pluginName)
      if (plugin?.parsers?.[name]) Object.assign(parser, plugin.parsers[name])
    }

    return parser
  }

  parser.preprocess = async (code: string, options: ParserOptions) => {
    let parser = await load(options)
    return parser.preprocess ? parser.preprocess(code, options) : code
  }

  parser.parse = async (code, options) => {
    let original = await load(options)

    // @ts-expect-error: `options` is passed twice for compat with older plugins that were written
    // for Prettier v2 but still work with v3.
    //
    // Currently only the Twig plugin requires this.
    let ast = await original.parse(code, options, options)

    let env = await loadTailwindCSS({ opts, options })

    transformAst({
      ast,
      env,
      opts,
      options,
    })

    options.__tailwindcss__ = env

    return ast
  }

  return parser
}

function createPrinter({
  original,
  opts,
}: {
  original: Printer<any>
  opts: TransformOptions<any>
}) {
  let printer: Printer<any> = { ...original }

  let reprint = opts.reprint

  // Hook into the preprocessing phase to load the config
  if (reprint) {
    printer.print = new Proxy(original.print, {
      apply(target, thisArg, args) {
        let [path, options] = args as Parameters<typeof original.print>
        let env = options.__tailwindcss__ as TransformerEnv
        reprint(path, { ...env, options: options })
        return Reflect.apply(target, thisArg, args)
      },
    })

    if (original.embed) {
      printer.embed = new Proxy(original.embed, {
        apply(target, thisArg, args) {
          let [path, options] = args as Parameters<typeof original.embed>
          let env = options.__tailwindcss__ as TransformerEnv
          reprint(path, { ...env, options: options as any })
          return Reflect.apply(target, thisArg, args)
        },
      })
    }
  }

  return printer
}

async function loadPlugins<T>(fns: PluginLoad[]) {
  let plugin: Plugin<T> = {
    parsers: Object.create(null),
    printers: Object.create(null),
    options: Object.create(null),
    defaultOptions: Object.create(null),
    languages: [],
  }

  for (let source of fns) {
    let loaded = await loadPlugin(source)
    Object.assign(plugin.parsers!, loaded.parsers ?? {})
    Object.assign(plugin.printers!, loaded.printers ?? {})
    Object.assign(plugin.options!, loaded.options ?? {})
    Object.assign(plugin.defaultOptions!, loaded.defaultOptions ?? {})

    plugin.languages = [...(plugin.languages ?? []), ...(loaded.languages ?? [])]
  }

  return plugin
}

const EMPTY_PLUGIN: Plugin<any> = {
  parsers: {},
  printers: {},
  languages: [],
  options: {},
  defaultOptions: {},
}

async function loadPlugin(source: PluginLoad): Promise<Plugin<any>> {
  if ('importer' in source && typeof source.importer === 'function') {
    return normalizePlugin(await source.importer())
  }

  return source
}

function normalizePlugin(source: unknown): Plugin<any> {
  if (source === null || typeof source !== 'object') return EMPTY_PLUGIN
  let maybeModule = source as { default?: unknown }
  let plugin = maybeModule.default
  return (plugin && typeof plugin === 'object' ? plugin : source) as Plugin<any>
}

function findEnabledPlugin(options: ParserOptions<any>, name: string) {
  for (let plugin of options.plugins) {
    if (plugin instanceof URL) {
      if (plugin.protocol !== 'file:') continue
      if (plugin.hostname !== '') continue

      plugin = plugin.pathname
    }

    if (typeof plugin !== 'string') {
      if (!plugin.name) {
        continue
      }
      plugin = plugin.name
    }

    if (
      plugin === name ||
      (isAbsolute(plugin) && plugin.includes(name) && maybeResolve(name) === plugin)
    ) {
      return loadIfExists<Plugin<any>>(name)
    }
  }
}

async function loadTailwindCSS<T = any>({
  options,
  opts,
}: {
  options: ParserOptions<T>
  opts: TransformOptions<T>
}): Promise<TransformerEnv> {
  let parsers = opts.parsers
  let parser = options.parser as string

  let context = await getTailwindConfig(options)

  let matcher = createMatcher(options, parser, {
    staticAttrs: new Set(parsers[parser]?.staticAttrs ?? opts.staticAttrs ?? []),
    dynamicAttrs: new Set(parsers[parser]?.dynamicAttrs ?? opts.dynamicAttrs ?? []),
    functions: new Set(),
    staticAttrsRegex: [],
    dynamicAttrsRegex: [],
    functionsRegex: [],
  })

  return {
    context,
    matcher,
    options,
    changes: [],
  }
}

function transformAst<T = any>({
  ast,
  env,
  opts,
}: {
  ast: T
  env: TransformerEnv
  options: ParserOptions<T>
  opts: TransformOptions<T>
}) {
  let transform = opts.transform
  if (transform) transform(ast, env)
}


================================================
FILE: src/expiring-map.ts
================================================
interface ExpiringMap<K, V> {
  get(key: K): V | undefined
  remember(key: K, factory: () => V): V
  set(key: K, value: V): void
}

export function expiringMap<K, V>(duration: number): ExpiringMap<K, V> {
  let map = new Map<K, { value: V; expiration: Date }>()

  return {
    get(key: K) {
      let result = map.get(key)

      if (result && result.expiration > new Date()) {
        return result.value
      }

      map.delete(key)

      return undefined
    },

    remember(key: K, factory: () => V) {
      let result = map.get(key)

      if (result && result.expiration > new Date()) {
        return result.value
      }

      map.delete(key)

      let value = factory()
      this.set(key, value)
      return value
    },

    set(key: K, value: V) {
      let expiration = new Date()
      expiration.setMilliseconds(expiration.getMilliseconds() + duration)

      map.set(key, {
        value,
        expiration,
      })
    },
  }
}


================================================
FILE: src/index.ts
================================================
// @ts-ignore
import type * as Liquid from '@shopify/prettier-plugin-liquid/dist/types.js'
// @ts-ignore
import lineColumn from 'line-column'
import * as prettierParserAngular from 'prettier/plugins/angular'
import * as prettierParserBabel from 'prettier/plugins/babel'
import * as prettierParserCss from 'prettier/plugins/postcss'
// @ts-ignore
import { createPlugin } from './create-plugin.js'
import type { Matcher } from './options.js'
import { sortClasses, sortClassList } from './sorting.js'
import { defineTransform } from './transform.js'
import type { StringChange, TransformerEnv } from './types'
import { spliceChangesIntoString, visit, type Path } from './utils.js'

const ESCAPE_SEQUENCE_PATTERN = /\\(['"\\nrtbfv0-7xuU])/g
function tryParseAngularAttribute(value: string, env: TransformerEnv) {
  try {
    return prettierParserAngular.parsers.__ng_directive.parse(value, env.options)
  } catch (err) {
    console.warn('prettier-plugin-tailwindcss: Unable to parse angular directive')
    console.warn(err)
    return null
  }
}

function transformDynamicAngularAttribute(attr: any, env: TransformerEnv) {
  let directiveAst = tryParseAngularAttribute(attr.value, env)

  // If we've reached this point we couldn't parse the expression we we should bail
  // `tryParseAngularAttribute` will display some warnings/errors
  // But we shouldn't fail outright — just miss parsing some attributes
  if (!directiveAst) {
    return
  }

  let changes: StringChange[] = []

  visit(directiveAst, {
    StringLiteral(node, path) {
      if (!node.value) return

      let collapseWhitespace = canCollapseWhitespaceIn(path, env)

      changes.push({
        start: node.start + 1,
        end: node.end - 1,
        before: node.value,
        after: sortClasses(node.value, {
          env,
          collapseWhitespace,
        }),
      })
    },

    TemplateLiteral(node, path) {
      if (!node.quasis.length) return

      let collapseWhitespace = canCollapseWhitespaceIn(path, env)

      for (let i = 0; i < node.quasis.length; i++) {
        let quasi = node.quasis[i]

        changes.push({
          start: quasi.start,
          end: quasi.end,
          before: quasi.value.raw,
          after: sortClasses(quasi.value.raw, {
            env,

            // Is not the first "item" and does not start with a space
            ignoreFirst: i > 0 && !/^\s/.test(quasi.value.raw),

            // Is between two expressions
            // And does not end with a space
            ignoreLast: i < node.expressions.length && !/\s$/.test(quasi.value.raw),

            collapseWhitespace: collapseWhitespace
              ? {
                  start: collapseWhitespace.start && i === 0,
                  end: collapseWhitespace.end && i >= node.expressions.length,
                }
              : false,
          }),
        })
      }
    },
  })

  attr.value = spliceChangesIntoString(attr.value, changes)
}

function transformDynamicJsAttribute(attr: any, env: TransformerEnv) {
  let { matcher } = env

  let expressionPrefix = 'let __prettier_temp__ = '
  let source = `${expressionPrefix}${attr.value}`
  let ast = prettierParserBabel.parsers['babel-ts'].parse(source, env.options)

  let didChange = false
  let changes: StringChange[] = []

  function findConcatEntry(path: Path<any, any>) {
    return path.find(
      (entry) => entry.parent?.type === 'BinaryExpression' && entry.parent.operator === '+',
    )
  }

  function addChange(
    start: number | null | undefined,
    end: number | null | undefined,
    after: string,
  ) {
    if (start == null || end == null) return

    let offsetStart = start - expressionPrefix.length
    let offsetEnd = end - expressionPrefix.length

    if (offsetStart < 0 || offsetEnd < 0) return

    didChange = true
    changes.push({
      start: offsetStart,
      end: offsetEnd,
      before: attr.value.slice(offsetStart, offsetEnd),
      after,
    })
  }

  visit(ast, {
    StringLiteral(node, path) {
      let concat = findConcatEntry(path)
      let sorted = sortStringLiteral(node, {
        env,
        collapseWhitespace: {
          start: concat?.key !== 'right',
          end: concat?.key !== 'left',
        },
      })

      if (sorted) {
        // @ts-ignore
        let raw = node.extra?.raw ?? node.raw
        if (typeof raw === 'string') {
          addChange(node.start, node.end, raw)
        }
      }
    },

    Literal(node: any, path) {
      if (!isStringLiteral(node)) return

      let concat = findConcatEntry(path)
      let sorted = sortStringLiteral(node, {
        env,
        collapseWhitespace: {
          start: concat?.key !== 'right',
          end: concat?.key !== 'left',
        },
      })

      if (sorted) {
        // @ts-ignore
        let raw = node.extra?.raw ?? node.raw
        if (typeof raw === 'string') {
          addChange(node.start, node.end, raw)
        }
      }
    },

    TemplateLiteral(node, path) {
      let concat = findConcatEntry(path)
      let originalQuasis = node.quasis.map((quasi) => quasi.value.raw)
      let sorted = sortTemplateLiteral(node, {
        env,
        collapseWhitespace: {
          start: concat?.key !== 'right',
          end: concat?.key !== 'left',
        },
      })

      if (sorted) {
        for (let i = 0; i < node.quasis.length; i++) {
          let quasi = node.quasis[i]
          if (quasi.value.raw !== originalQuasis[i]) {
            addChange(quasi.start, quasi.end, quasi.value.raw)
          }
        }
      }
    },

    TaggedTemplateExpression(node, path) {
      if (!isSortableTemplateExpression(node, matcher)) {
        return
      }

      let concat = findConcatEntry(path)
      let originalQuasis = node.quasi.quasis.map((quasi) => quasi.value.raw)
      let sorted = sortTemplateLiteral(node.quasi, {
        env,
        collapseWhitespace: {
          start: concat?.key !== 'right',
          end: concat?.key !== 'left',
        },
      })

      if (sorted) {
        for (let i = 0; i < node.quasi.quasis.length; i++) {
          let quasi = node.quasi.quasis[i]
          if (quasi.value.raw !== originalQuasis[i]) {
            addChange(quasi.start, quasi.end, quasi.value.raw)
          }
        }
      }
    },
  })

  if (didChange) {
    attr.value = spliceChangesIntoString(attr.value, changes)
  }
}

function transformHtml(ast: any, env: TransformerEnv) {
  let { matcher } = env
  let { parser } = env.options

  for (let attr of ast.attrs ?? []) {
    if (matcher.hasStaticAttr(attr.name)) {
      attr.value = sortClasses(attr.value, { env })
    } else if (matcher.hasDynamicAttr(attr.name)) {
      if (!/[`'"]/.test(attr.value)) {
        continue
      }

      if (parser === 'angular') {
        transformDynamicAngularAttribute(attr, env)
      } else {
        transformDynamicJsAttribute(attr, env)
      }
    }
  }

  for (let child of ast.children ?? []) {
    transformHtml(child, env)
  }
}

function transformGlimmer(ast: any, env: TransformerEnv) {
  let { matcher } = env

  visit(ast, {
    AttrNode(attr, _path, meta) {
      if (matcher.hasStaticAttr(attr.name) && attr.value) {
        meta.sortTextNodes = true
      }
    },

    TextNode(node, path, meta) {
      if (!meta.sortTextNodes) {
        return
      }

      let concat = path.find((entry) => {
        return entry.parent && entry.parent.type === 'ConcatStatement'
      })

      let siblings = {
        prev: concat?.parent.parts[concat.index! - 1],
        next: concat?.parent.parts[concat.index! + 1],
      }

      node.chars = sortClasses(node.chars, {
        env,
        ignoreFirst: siblings.prev && !/^\s/.test(node.chars),
        ignoreLast: siblings.next && !/\s$/.test(node.chars),
        collapseWhitespace: {
          start: !siblings.prev,
          end: !siblings.next,
        },
      })
    },

    StringLiteral(node, path, meta) {
      if (!meta.sortTextNodes) {
        return
      }

      let concat = path.find((entry) => {
        return (
          entry.parent &&
          entry.parent.type === 'SubExpression' &&
          entry.parent.path.original === 'concat'
        )
      })

      node.value = sortClasses(node.value, {
        env,
        ignoreLast: Boolean(concat) && !/[^\S\r\n]$/.test(node.value),
        collapseWhitespace: {
          start: false,
          end: !concat,
        },
      })
    },
  })
}

function transformLiquid(ast: any, env: TransformerEnv) {
  let { matcher } = env

  function isClassAttr(node: { name: string | { type: string; value: string }[] }) {
    return Array.isArray(node.name)
      ? node.name.every((n) => n.type === 'TextNode' && matcher.hasStaticAttr(n.value))
      : matcher.hasStaticAttr(node.name)
  }

  function hasSurroundingQuotes(str: string) {
    let start = str[0]
    let end = str[str.length - 1]

    return start === end && (start === '"' || start === "'" || start === '`')
  }

  let sources: { type: string; source: string }[] = []

  let changes: StringChange[] = []

  function sortAttribute(attr: Liquid.AttrSingleQuoted | Liquid.AttrDoubleQuoted) {
    for (let i = 0; i < attr.value.length; i++) {
      let node = attr.value[i]
      if (node.type === 'TextNode') {
        let after = sortClasses(node.value, {
          env,
          ignoreFirst: i > 0 && !/^\s/.test(node.value),
          ignoreLast: i < attr.value.length - 1 && !/\s$/.test(node.value),
          removeDuplicates: false,
          collapseWhitespace: false,
        })

        changes.push({
          start: node.position.start,
          end: node.position.end,
          before: node.value,
          after,
        })
      } else if (
        // @ts-ignore: `LiquidDrop` is for older versions of the liquid plugin (1.2.x)
        (node.type === 'LiquidDrop' || node.type === 'LiquidVariableOutput') &&
        typeof node.markup === 'object' &&
        node.markup.type === 'LiquidVariable'
      ) {
        visit(node.markup.expression, {
          String(node: any) {
            let pos = { ...node.position }

            // We have to offset the position ONLY when quotes are part of the String node
            // This is because `value` does NOT include quotes
            if (hasSurroundingQuotes(node.source.slice(pos.start, pos.end))) {
              pos.start += 1
              pos.end -= 1
            }

            let after = sortClasses(node.value, { env })

            changes.push({
              start: pos.start,
              end: pos.end,
              before: node.value,
              after,
            })
          },
        })
      }
    }
  }

  visit(ast, {
    LiquidTag(node: any) {
      sources.push(node)
    },

    HtmlElement(node: any) {
      sources.push(node)
    },

    AttrSingleQuoted(node: any) {
      if (isClassAttr(node)) {
        sources.push(node)
        sortAttribute(node)
      }
    },

    AttrDoubleQuoted(node: any) {
      if (isClassAttr(node)) {
        sources.push(node)
        sortAttribute(node)
      }
    },
  })

  for (let node of sources) {
    node.source = spliceChangesIntoString(node.source, changes)
  }
}

function sortStringLiteral(
  node: any,
  {
    env,
    removeDuplicates,
    collapseWhitespace = { start: true, end: true },
  }: {
    env: TransformerEnv
    removeDuplicates?: false
    collapseWhitespace?: false | { start: boolean; end: boolean }
  },
) {
  let result = sortClasses(node.value, {
    env,
    removeDuplicates,
    collapseWhitespace,
  })

  let didChange = result !== node.value

  if (!didChange) return false

  node.value = result

  // Preserve the original escaping level for the new content
  let raw = node.extra?.raw ?? node.raw
  let quote = raw[0]
  let originalRawContent = raw.slice(1, -1)
  let originalValue = node.extra?.rawValue ?? node.value

  if (node.extra) {
    // The original list has ecapes so we ensure that the sorted list also
    // maintains those by replacing backslashes from escape sequences.
    //
    // It seems that TypeScript-based ASTs don't need this special handling
    // which is why this is guarded inside the `node.extra` check
    if (originalRawContent !== originalValue && originalValue.includes('\\')) {
      result = result.replace(ESCAPE_SEQUENCE_PATTERN, '\\\\$1')
    }

    // JavaScript (StringLiteral)
    node.extra = {
      ...node.extra,
      rawValue: result,
      raw: quote + result + quote,
    }
  } else {
    // TypeScript (Literal)
    node.raw = quote + result + quote
  }

  return true
}

function isStringLiteral(node: any) {
  return (
    node.type === 'StringLiteral' || (node.type === 'Literal' && typeof node.value === 'string')
  )
}

function sortTemplateLiteral(
  node: any,
  {
    env,
    removeDuplicates,
    collapseWhitespace = { start: true, end: true },
  }: {
    env: TransformerEnv
    removeDuplicates?: false
    collapseWhitespace?: false | { start: boolean; end: boolean }
  },
) {
  let didChange = false

  for (let i = 0; i < node.quasis.length; i++) {
    let quasi = node.quasis[i]
    let same = quasi.value.raw === quasi.value.cooked
    let originalRaw = quasi.value.raw
    let originalCooked = quasi.value.cooked

    quasi.value.raw = sortClasses(quasi.value.raw, {
      env,
      removeDuplicates,
      // Is not the first "item" and does not start with a space
      ignoreFirst: i > 0 && !/^\s/.test(quasi.value.raw),

      // Is between two expressions
      // And does not end with a space
      ignoreLast: i < node.expressions.length && !/\s$/.test(quasi.value.raw),

      collapseWhitespace: collapseWhitespace && {
        start: collapseWhitespace && collapseWhitespace.start && i === 0,
        end: collapseWhitespace && collapseWhitespace.end && i >= node.expressions.length,
      },
    })

    quasi.value.cooked = same
      ? quasi.value.raw
      : sortClasses(quasi.value.cooked, {
          env,
          ignoreFirst: i > 0 && !/^\s/.test(quasi.value.cooked),
          ignoreLast: i < node.expressions.length && !/\s$/.test(quasi.value.cooked),
          removeDuplicates,
          collapseWhitespace: collapseWhitespace && {
            start: collapseWhitespace && collapseWhitespace.start && i === 0,
            end: collapseWhitespace && collapseWhitespace.end && i >= node.expressions.length,
          },
        })

    if (quasi.value.raw !== originalRaw || quasi.value.cooked !== originalCooked) {
      didChange = true
    }
  }

  return didChange
}

function isSortableTemplateExpression(
  node: import('@babel/types').TaggedTemplateExpression,
  matcher: Matcher,
): boolean {
  return isSortableExpression(node.tag, matcher)
}

function isSortableCallExpression(
  node: import('@babel/types').CallExpression,
  matcher: Matcher,
): boolean {
  if (!node.arguments?.length) return false

  return isSortableExpression(node.callee, matcher)
}

function isSortableExpression(
  node: import('@babel/types').Expression | import('@babel/types').V8IntrinsicIdentifier,
  matcher: Matcher,
): boolean {
  // Traverse property accesses and function calls to find the leading ident
  while (node.type === 'CallExpression' || node.type === 'MemberExpression') {
    if (node.type === 'CallExpression') {
      node = node.callee
    } else if (node.type === 'MemberExpression') {
      node = node.object
    }
  }

  if (node.type === 'Identifier') {
    return matcher.hasFunction(node.name)
  }

  return false
}

function canCollapseWhitespaceIn(
  path: Path<import('@babel/types').Node, any>,
  env: TransformerEnv,
): false | { start: boolean; end: boolean } {
  if (env.options.tailwindPreserveWhitespace) {
    return false
  }

  let start = true
  let end = true

  for (let entry of path) {
    if (!entry.parent) continue

    // Nodes inside concat expressions shouldn't collapse whitespace
    // depending on which side they're part of.
    if (entry.parent.type === 'BinaryExpression' && entry.parent.operator === '+') {
      start &&= entry.key !== 'right'
      end &&= entry.key !== 'left'
    }

    // This is probably expression *inside* of a template literal. To collapse whitespace
    // `Expression`s adjacent-before a quasi must start with whitespace
    // `Expression`s adjacent-after a quasi must end with whitespace
    //
    // Note this check will bail out on more than it really should as it
    // could be reset somewhere along the way by having whitespace around a
    // string further up but not at the "root" but that complicates things
    if (entry.parent.type === 'TemplateLiteral') {
      let nodeStart = entry.node.start ?? null
      let nodeEnd = entry.node.end ?? null

      for (let quasi of entry.parent.quasis) {
        let quasiStart = quasi.start ?? null
        let quasiEnd = quasi.end ?? null

        if (nodeStart !== null && quasiEnd !== null && nodeStart - quasiEnd <= 2) {
          start &&= /^\s/.test(quasi.value.raw)
        }

        if (nodeEnd !== null && quasiStart !== null && nodeEnd - quasiStart <= 2) {
          end &&= /\s$/.test(quasi.value.raw)
        }
      }
    }
  }

  return { start, end }
}

// TODO: The `ast` types here aren't strictly correct.
//
// We cross several parsers that share roughly the same shape so things are
// good enough. The actual AST we should be using is probably estree + ts.
function transformJavaScript(ast: import('@babel/types').Node, env: TransformerEnv) {
  let { matcher } = env

  function sortInside(ast: import('@babel/types').Node) {
    visit(ast, (node, path) => {
      let collapseWhitespace = canCollapseWhitespaceIn(path, env)

      if (isStringLiteral(node)) {
        sortStringLiteral(node, { env, collapseWhitespace })
      } else if (node.type === 'TemplateLiteral') {
        sortTemplateLiteral(node, { env, collapseWhitespace })
      } else if (node.type === 'TaggedTemplateExpression') {
        if (isSortableTemplateExpression(node, matcher)) {
          sortTemplateLiteral(node.quasi, { env, collapseWhitespace })
        }
      }
    })
  }

  visit(ast, {
    JSXAttribute(node) {
      node = node as import('@babel/types').JSXAttribute

      if (!node.value) {
        return
      }

      // We don't want to support namespaced attributes (e.g. `somens:class`)
      // React doesn't support them and most tools don't either
      if (typeof node.name.name !== 'string') {
        return
      }

      if (!matcher.hasStaticAttr(node.name.name)) {
        return
      }

      if (isStringLiteral(node.value)) {
        sortStringLiteral(node.value, { env })
      } else if (node.value.type === 'JSXExpressionContainer') {
        sortInside(node.value)
      }
    },

    CallExpression(node) {
      node = node as import('@babel/types').CallExpression

      if (!isSortableCallExpression(node, matcher)) {
        return
      }

      node.arguments.forEach((arg) => sortInside(arg))
    },

    TaggedTemplateExpression(node, path) {
      node = node as import('@babel/types').TaggedTemplateExpression

      if (!isSortableTemplateExpression(node, matcher)) {
        return
      }

      let collapseWhitespace = canCollapseWhitespaceIn(path, env)

      sortTemplateLiteral(node.quasi, {
        env,
        collapseWhitespace,
      })
    },
  })
}

function transformCss(ast: any, env: TransformerEnv) {
  // `parseValue` inside Prettier's CSS parser is private API so we have to
  // produce the same result by parsing an import statement with the same params
  function tryParseAtRuleParams(name: string, params: any) {
    // It might already be an object or array. Could happen in the future if
    // Prettier decides to start parsing these.
    if (typeof params !== 'string') return params

    // Otherwise we let prettier re-parse the params into its custom value AST
    // based on postcss-value parser.
    try {
      let parser = prettierParserCss.parsers.css

      let root = parser.parse(`@import ${params};`, {
        // We can't pass env.options directly because css.parse overwrites
        // options.originalText which is used during the printing phase
        ...env.options,
      })

      return root.nodes[0].params
    } catch (err) {
      console.warn(`[prettier-plugin-tailwindcss] Unable to parse at rule`)
      console.warn({ name, params })
      console.warn(err)
    }

    return params
  }

  ast.walk((node: any) => {
    if (node.name === 'plugin' || node.name === 'config' || node.name === 'source') {
      node.params = tryParseAtRuleParams(node.name, node.params)
    }

    if (node.type === 'css-atrule' && node.name === 'apply') {
      let isImportant = /\s+(?:!important|#{(['"]*)!important\1})\s*$/.test(node.params)

      let classList = node.params

      let prefix = ''
      let suffix = ''

      if (classList.startsWith('~"') && classList.endsWith('"')) {
        prefix = '~"'
        suffix = '"'
        classList = classList.slice(2, -1)
        isImportant = false
      } else if (classList.startsWith("~'") && classList.endsWith("'")) {
        prefix = "~'"
        suffix = "'"
        classList = classList.slice(2, -1)
        isImportant = false
      }

      classList = sortClasses(classList, {
        env,
        ignoreLast: isImportant,
        collapseWhitespace: {
          start: false,
          end: !isImportant,
        },
      })

      node.params = `${prefix}${classList}${suffix}`
    }
  })
}

function transformAstro(ast: any, env: TransformerEnv) {
  let { matcher } = env

  if (ast.type === 'element' || ast.type === 'custom-element' || ast.type === 'component') {
    for (let attr of ast.attributes ?? []) {
      if (matcher.hasStaticAttr(attr.name) && attr.type === 'attribute' && attr.kind === 'quoted') {
        attr.value = sortClasses(attr.value, {
          env,
        })
      } else if (
        matcher.hasDynamicAttr(attr.name) &&
        attr.type === 'attribute' &&
        attr.kind === 'expression' &&
        typeof attr.value === 'string'
      ) {
        transformDynamicJsAttribute(attr, env)
      }
    }
  }

  for (let child of ast.children ?? []) {
    transformAstro(child, env)
  }
}

function transformMarko(ast: any, env: TransformerEnv) {
  let { matcher } = env

  const nodesToVisit = [ast]
  while (nodesToVisit.length > 0) {
    const currentNode = nodesToVisit.pop()
    switch (currentNode.type) {
      case 'File':
        nodesToVisit.push(currentNode.program)
        break
      case 'Program':
        nodesToVisit.push(...currentNode.body)
        break
      case 'MarkoTag':
        nodesToVisit.push(...currentNode.attributes)
        nodesToVisit.push(currentNode.body)
        break
      case 'MarkoTagBody':
        nodesToVisit.push(...currentNode.body)
        break
      case 'MarkoAttribute':
        if (!matcher.hasStaticAttr(currentNode.name)) break
        switch (currentNode.value.type) {
          case 'ArrayExpression':
            const classList = currentNode.value.elements
            for (const node of classList) {
              if (node.type === 'StringLiteral') {
                node.value = sortClasses(node.value, { env })
              }
            }
            break
          case 'StringLiteral':
            currentNode.value.value = sortClasses(currentNode.value.value, {
              env,
            })
            break
        }
        break
    }
  }
}

function transformTwig(ast: any, env: TransformerEnv) {
  let { matcher } = env

  for (let child of ast.expressions ?? []) {
    transformTwig(child, env)
  }

  visit(ast, {
    Attribute(node, _path, meta) {
      if (!matcher.hasStaticAttr(node.name.name)) return

      meta.sortTextNodes = true
    },

    CallExpression(node, _path, meta) {
      // Traverse property accesses and function calls to find the *trailing* ident
      while (node.type === 'CallExpression' || node.type === 'MemberExpression') {
        if (node.type === 'CallExpression') {
          node = node.callee
        } else if (node.type === 'MemberExpression') {
          // TODO: This is *different* than `isSortableExpression` and that doesn't feel right
          // but they're mutually exclusive implementations
          //
          // This is to handle foo.fnNameHere(…) where `isSortableExpression` is intentionally
          // handling `fnNameHere.foo(…)`.
          node = node.property
        }
      }

      if (node.type === 'Identifier') {
        if (!matcher.hasFunction(node.name)) return
      }

      meta.sortTextNodes = true
    },

    StringLiteral(node, path, meta) {
      if (!meta.sortTextNodes) {
        return
      }

      const concat = path.find((entry) => {
        return (
          entry.parent &&
          (entry.parent.type === 'BinaryConcatExpression' ||
            entry.parent.type === 'BinaryAddExpression')
        )
      })

      node.value = sortClasses(node.value, {
        env,
        ignoreFirst: concat?.key === 'right' && !/^[^\S\r\n]/.test(node.value),
        ignoreLast: concat?.key === 'left' && !/[^\S\r\n]$/.test(node.value),
        collapseWhitespace: {
          start: concat?.key !== 'right',
          end: concat?.key !== 'left',
        },
      })
    },
  })
}

function transformPug(ast: any, env: TransformerEnv) {
  let { matcher } = env

  // This isn't optimal
  // We should merge the classes together across class attributes and class tokens
  // And then we sort them
  // But this is good enough for now

  // First sort the classes in attributes
  for (const token of ast.tokens) {
    if (token.type === 'attribute' && matcher.hasStaticAttr(token.name)) {
      token.val = [
        token.val.slice(0, 1),
        sortClasses(token.val.slice(1, -1), { env }),
        token.val.slice(-1),
      ].join('')
    }
  }

  // Collect lists of consecutive class tokens
  let startIdx = -1
  let endIdx = -1
  let ranges: [number, number][] = []

  for (let i = 0; i < ast.tokens.length; i++) {
    const token = ast.tokens[i]

    if (token.type === 'class') {
      startIdx = startIdx === -1 ? i : startIdx
      endIdx = i
    } else if (startIdx !== -1) {
      ranges.push([startIdx, endIdx])
      startIdx = -1
      endIdx = -1
    }
  }

  if (startIdx !== -1) {
    ranges.push([startIdx, endIdx])
    startIdx = -1
    endIdx = -1
  }

  // Sort the lists of class tokens
  for (const [startIdx, endIdx] of ranges) {
    const classes = ast.tokens.slice(startIdx, endIdx + 1).map((token: any) => token.val)

    const { classList } = sortClassList({
      classList: classes,
      api: env.context,
      removeDuplicates: false,
    })

    for (let i = startIdx; i <= endIdx; i++) {
      ast.tokens[i].val = classList[i - startIdx]
    }
  }
}

function transformSvelte(ast: any, env: TransformerEnv) {
  let { matcher, changes } = env

  for (let attr of ast.attributes ?? []) {
    if (!matcher.hasStaticAttr(attr.name) || attr.type !== 'Attribute') {
      continue
    }

    for (let i = 0; i < attr.value.length; i++) {
      let value = attr.value[i]
      if (value.type === 'Text') {
        let same = value.raw === value.data
        value.raw = sortClasses(value.raw, {
          env,
          ignoreFirst: i > 0 && !/^\s/.test(value.raw),
          ignoreLast: i < attr.value.length - 1 && !/\s$/.test(value.raw),
          removeDuplicates: true,
          collapseWhitespace: false,
        })
        value.data = same
          ? value.raw
          : sortClasses(value.data, {
              env,
              ignoreFirst: i > 0 && !/^\s/.test(value.data),
              ignoreLast: i < attr.value.length - 1 && !/\s$/.test(value.data),
              removeDuplicates: true,
              collapseWhitespace: false,
            })
      } else if (value.type === 'MustacheTag') {
        visit(value.expression, {
          Literal(node) {
            if (isStringLiteral(node)) {
              let before = node.raw
              let sorted = sortStringLiteral(node, {
                env,
                removeDuplicates: false,
                collapseWhitespace: false,
              })

              if (sorted) {
                changes.push({
                  before,
                  after: node.raw,
                  start: node.loc.start,
                  end: node.loc.end,
                })
              }
            }
          },
          TemplateLiteral(node) {
            let before = node.quasis.map((quasi: any) => quasi.value.raw)
            let sorted = sortTemplateLiteral(node, {
              env,
              removeDuplicates: false,
              collapseWhitespace: false,
            })

            if (sorted) {
              for (let [idx, quasi] of node.quasis.entries()) {
                changes.push({
                  before: before[idx],
                  after: quasi.value.raw,
                  start: quasi.loc.start,
                  end: quasi.loc.end,
                })
              }
            }
          },
        })
      }
    }
  }

  for (let child of ast.children ?? []) {
    transformSvelte(child, env)
  }

  if (ast.type === 'IfBlock') {
    for (let child of ast.else?.children ?? []) {
      transformSvelte(child, env)
    }
  }

  if (ast.type === 'AwaitBlock') {
    let nodes = [ast.pending, ast.then, ast.catch]

    for (let child of nodes) {
      transformSvelte(child, env)
    }
  }

  if (ast.html) {
    transformSvelte(ast.html, env)
  }
}

export { options } from './options.js'

type HtmlNode =
  | { type: 'attribute'; name: string; value: string }
  | { kind: 'attribute'; name: string; value: string }

let html = defineTransform<HtmlNode>({
  staticAttrs: ['class'],

  load: [{ name: 'prettier/plugins/html', importer: () => import('prettier/plugins/html') }],
  compatible: ['prettier-plugin-organize-attributes'],

  parsers: {
    html: {},
    lwc: {},
    angular: { dynamicAttrs: ['[ngClass]'] },
    vue: { dynamicAttrs: [':class', 'v-bind:class'] },
  },

  transform: transformHtml,
})

type GlimmerNode =
  | { type: 'TextNode'; chars: string }
  | { type: 'StringLiteral'; value: string }
  | { type: 'ConcatStatement'; parts: GlimmerNode[] }
  | { type: 'SubExpression'; path: { original: string } }
  | { type: 'AttrNode'; name: string; value: GlimmerNode }

let glimmer = defineTransform<GlimmerNode>({
  staticAttrs: ['class'],
  load: [{ name: 'prettier/plugins/glimmer', importer: () => import('prettier/plugins/glimmer') }],

  parsers: {
    glimmer: {},
  },

  transform: transformGlimmer,
})

type CssValueNode = { type: 'value-*'; name: string; params: string }
type CssNode = {
  type: 'css-atrule'
  name: string
  params: string | CssValueNode
}

let css = defineTransform<CssNode>({
  load: [prettierParserCss],
  compatible: ['prettier-plugin-css-order'],

  parsers: {
    css: {},
    scss: {},
    less: {},
  },

  transform: transformCss,
})

let js = defineTransform<import('@babel/types').Node>({
  staticAttrs: ['class', 'className'],
  compatible: [
    // The following plugins must come *before* the jsdoc plugin for it to
    // function correctly. Additionally `multiline-arrays` usually needs to be
    // placed before import sorting plugins.
    //
    // https://github.com/electrovir/prettier-plugin-multiline-arrays#compatibility
    'prettier-plugin-multiline-arrays',
    '@ianvs/prettier-plugin-sort-imports',
    '@trivago/prettier-plugin-sort-imports',
    'prettier-plugin-organize-imports',
    'prettier-plugin-sort-imports',
    'prettier-plugin-jsdoc',
  ],

  parsers: {
    babel: { load: [prettierParserBabel] },
    'babel-flow': { load: [prettierParserBabel] },
    'babel-ts': { load: [prettierParserBabel] },
    __js_expression: { load: [prettierParserBabel] },
    typescript: {
      load: [
        {
          name: 'prettier/plugins/typescript',
          importer: () => import('prettier/plugins/typescript'),
        },
      ],
    },
    meriyah: {
      load: [
        { name: 'prettier/plugins/meriyah', importer: () => import('prettier/plugins/meriyah') },
      ],
    },
    acorn: {
      load: [{ name: 'prettier/plugins/acorn', importer: () => import('prettier/plugins/acorn') }],
    },
    flow: {
      load: [{ name: 'prettier/plugins/flow', importer: () => import('prettier/plugins/flow') }],
    },
    oxc: {
      load: [{ name: '@prettier/plugin-oxc', importer: () => import('@prettier/plugin-oxc') }],
    },
    'oxc-ts': {
      load: [{ name: '@prettier/plugin-oxc', importer: () => import('@prettier/plugin-oxc') }],
    },
    hermes: {
      load: [
        { name: '@prettier/plugin-hermes', importer: () => import('@prettier/plugin-hermes') },
      ],
    },
    astroExpressionParser: {
      load: [
        {
          name: 'prettier-plugin-astro',
          importer: () => {
            // @ts-expect-error - This plugin doesn't have types
            return import('prettier-plugin-astro')
          },
        },
      ],
      staticAttrs: ['class'],
      dynamicAttrs: ['class:list'],
    },
  },

  transform: transformJavaScript,
})

type SvelteNode = import('svelte/compiler').AST.SvelteNode & {
  changes: StringChange[]
}

let svelte = defineTransform<SvelteNode>({
  staticAttrs: ['class'],
  load: [{ name: 'prettier-plugin-svelte', importer: () => import('prettier-plugin-svelte') }],

  parsers: {
    svelte: {},
  },

  printers: {
    'svelte-ast': {},
  },

  transform: transformSvelte,

  reprint(path, { options, changes }) {
    if (options.__mutatedOriginalText) return
    options.__mutatedOriginalText = true

    if (!changes?.length) return

    let finder = lineColumn(options.originalText)

    let stringChanges: StringChange[] = changes.map((change) => ({
      ...change,
      start: finder.toIndex(change.start.line, change.start.column + 1),
      end: finder.toIndex(change.end.line, change.end.column + 1),
    }))

    options.originalText = spliceChangesIntoString(options.originalText, stringChanges)
  },
})

type AstroNode =
  | { type: 'element'; attributes: Extract<AstroNode, { type: 'attribute' }>[] }
  | {
      type: 'custom-element'
      attributes: Extract<AstroNode, { type: 'attribute' }>[]
    }
  | {
      type: 'component'
      attributes: Extract<AstroNode, { type: 'attribute' }>[]
    }
  | { type: 'attribute'; kind: 'quoted'; name: string; value: string }
  | { type: 'attribute'; kind: 'expression'; name: string; value: unknown }

let astro = defineTransform<AstroNode>({
  staticAttrs: ['class', 'className'],
  dynamicAttrs: ['class:list', 'className'],
  load: [
    {
      name: 'prettier-plugin-astro',
      importer: () => {
        // @ts-expect-error - This plugin doesn't have types
        return import('prettier-plugin-astro')
      },
    },
  ],

  parsers: {
    astro: {},
  },

  transform: transformAstro,
})

type MarkoNode = import('@marko/compiler').types.Node

let marko = defineTransform<MarkoNode>({
  staticAttrs: ['class'],
  load: [{ name: 'prettier-plugin-marko', importer: () => import('prettier-plugin-marko') }],

  parsers: {
    marko: {},
  },

  transform: transformMarko,
})

type TwigIdentifier = { type: 'Identifier'; name: string }

type TwigMemberExpression = {
  type: 'MemberExpression'
  property: TwigIdentifier | TwigCallExpression | TwigMemberExpression
}

type TwigCallExpression = {
  type: 'CallExpression'
  callee: TwigIdentifier | TwigCallExpression | TwigMemberExpression
}

type TwigNode =
  | { type: 'Attribute'; name: TwigIdentifier }
  | { type: 'StringLiteral'; value: string }
  | { type: 'BinaryConcatExpression' }
  | { type: 'BinaryAddExpression' }
  | TwigIdentifier
  | TwigMemberExpression
  | TwigCallExpression

let twig = defineTransform<TwigNode>({
  staticAttrs: ['class'],
  load: [
    {
      name: '@zackad/prettier-plugin-twig',
      importer: () => {
        // @ts-expect-error - This plugin doesn't have types
        return import('@zackad/prettier-plugin-twig')
      },
    },
  ],

  parsers: {
    twig: {},
  },

  transform: transformTwig,
})

interface PugNode {
  content: string
  tokens: import('pug-lexer').Token[]
}

let pug = defineTransform<PugNode>({
  staticAttrs: ['class'],
  load: [{ name: '@prettier/plugin-pug', importer: () => import('@prettier/plugin-pug') }],

  parsers: {
    pug: {},
  },

  transform: transformPug,
})

type LiquidNode =
  | Liquid.TextNode
  | Liquid.AttributeNode
  | Liquid.LiquidTag
  | Liquid.HtmlElement
  | Liquid.DocumentNode
  | Liquid.LiquidExpression

let liquid = defineTransform<LiquidNode>({
  staticAttrs: ['class'],
  load: [
    {
      name: '@shopify/prettier-plugin-liquid',
      importer: () => import('@shopify/prettier-plugin-liquid'),
    },
  ],

  parsers: { 'liquid-html': {} },

  transform: transformLiquid,
})

export const { parsers, printers } = createPlugin([
  //
  html,
  glimmer,
  css,
  js,
  svelte,
  astro,
  marko,
  twig,
  pug,
  liquid,
])

export interface PluginOptions {
  /**
   * Path to the Tailwind config file.
   */
  tailwindConfig?: string

  /**
   * Path to the CSS stylesheet used by Tailwind CSS (v4+)
   */
  tailwindStylesheet?: string

  /**
   * Path to the CSS stylesheet used by Tailwind CSS (v4+)
   *
   * @deprecated Use `tailwindStylesheet` instead
   */
  tailwindEntryPoint?: string

  /**
   * List of custom function and tag names that contain classes.
   *
   * Default: []
   */
  tailwindFunctions?: string[]

  /**
   * List of custom attributes that contain classes.
   *
   * Default: []
   */
  tailwindAttributes?: string[]

  /**
   * Preserve whitespace around Tailwind classes when sorting.
   *
   * Default: false
   */
  tailwindPreserveWhitespace?: boolean

  /**
   * Preserve duplicate classes inside a class list when sorting.
   *
   * Default: false
   */
  tailwindPreserveDuplicates?: boolean
}


================================================
FILE: src/internal.d.ts
================================================
import type { PluginOptions } from '.'

export interface InternalOptions extends PluginOptions {
  printer: Printer<any>

  /**
   * The package name to use when loading Tailwind CSS
   */
  tailwindPackageName?: string
}

export interface InternalPlugin {
  name?: string
}

declare module 'prettier' {
  interface RequiredOptions extends InternalOptions {}
  interface ParserOptions extends InternalOptions {}
  interface Plugin<T = any> extends InternalPlugin {}
}


================================================
FILE: src/options.ts
================================================
import type { RequiredOptions, SupportOption } from 'prettier'
import type { Customizations } from './types'

export const options: Record<string, SupportOption> = {
  tailwindConfig: {
    type: 'string',
    category: 'Tailwind CSS',
    description: 'Path to Tailwind configuration file',
  },

  tailwindEntryPoint: {
    type: 'string',
    category: 'Tailwind CSS',
    description: 'Path to the CSS entrypoint in your Tailwind project (v4+)',

    // Can't include this otherwise the option is not passed to parsers
    // deprecated: "This option is deprecated. Use 'tailwindStylesheet' instead.",
  },

  tailwindStylesheet: {
    type: 'string',
    category: 'Tailwind CSS',
    description: 'Path to the CSS stylesheet in your Tailwind project (v4+)',
  },

  tailwindAttributes: {
    type: 'string',
    array: true,
    default: [{ value: [] }],
    category: 'Tailwind CSS',
    description: 'List of attributes/props that contain sortable Tailwind classes',
  },

  tailwindFunctions: {
    type: 'string',
    array: true,
    default: [{ value: [] }],
    category: 'Tailwind CSS',
    description: 'List of functions and tagged templates that contain sortable Tailwind classes',
  },

  tailwindPreserveWhitespace: {
    type: 'boolean',
    default: false,
    category: 'Tailwind CSS',
    description: 'Preserve whitespace around Tailwind classes when sorting',
  },

  tailwindPreserveDuplicates: {
    type: 'boolean',
    default: false,
    category: 'Tailwind CSS',
    description: 'Preserve duplicate classes inside a class list when sorting',
  },

  tailwindPackageName: {
    type: 'string',
    default: 'tailwindcss',
    category: 'Tailwind CSS',
    description: 'The package name to use when loading Tailwind CSS',
  },
}

export interface Matcher {
  hasStaticAttr(name: string): boolean
  hasDynamicAttr(name: string): boolean
  hasFunction(name: string): boolean
}

export function createMatcher(
  options: RequiredOptions,
  parser: string,
  defaults: Customizations,
): Matcher {
  let staticAttrs = new Set<string>(defaults.staticAttrs)
  let dynamicAttrs = new Set<string>(defaults.dynamicAttrs)
  let functions = new Set<string>(defaults.functions)
  let staticAttrsRegex: RegExp[] = [...defaults.staticAttrsRegex]
  let functionsRegex: RegExp[] = [...defaults.functionsRegex]

  // Create a list of "static" attributes
  for (let attr of options.tailwindAttributes ?? []) {
    let regex = parseRegex(attr)

    if (regex) {
      staticAttrsRegex.push(regex)
    } else if (parser === 'vue' && attr.startsWith(':')) {
      staticAttrs.add(attr.slice(1))
    } else if (parser === 'vue' && attr.startsWith('v-bind:')) {
      staticAttrs.add(attr.slice(7))
    } else if (parser === 'vue' && attr.startsWith('v-')) {
      dynamicAttrs.add(attr)
    } else if (parser === 'angular' && attr.startsWith('[') && attr.endsWith(']')) {
      staticAttrs.add(attr.slice(1, -1))
    } else {
      staticAttrs.add(attr)
    }
  }

  // Generate a list of dynamic attributes
  for (let attr of staticAttrs) {
    if (parser === 'vue') {
      dynamicAttrs.add(`:${attr}`)
      dynamicAttrs.add(`v-bind:${attr}`)
    } else if (parser === 'angular') {
      dynamicAttrs.add(`[${attr}]`)
    }
  }

  // Generate a list of supported functions
  for (let fn of options.tailwindFunctions ?? []) {
    let regex = parseRegex(fn)

    if (regex) {
      functionsRegex.push(regex)
    } else {
      functions.add(fn)
    }
  }

  return {
    hasStaticAttr: (name: string) => {
      // If the name looks like a dynamic attribute we're not a static attr
      // Only applies to Vue and Angular
      let newName = nameFromDynamicAttr(name, parser)
      if (newName) return false

      return hasMatch(name, staticAttrs, staticAttrsRegex)
    },

    hasDynamicAttr: (name: string) => {
      // This is definitely a dynamic attribute
      if (hasMatch(name, dynamicAttrs, [])) return true

      // If the name looks like a dynamic attribute compare the actual name
      // Only applies to Vue and Angular
      let newName = nameFromDynamicAttr(name, parser)
      if (!newName) return false

      return hasMatch(newName, staticAttrs, staticAttrsRegex)
    },

    hasFunction: (name: string) => hasMatch(name, functions, functionsRegex),
  }
}

function nameFromDynamicAttr(name: string, parser: string) {
  if (parser === 'vue') {
    if (name.startsWith(':')) return name.slice(1)
    if (name.startsWith('v-bind:')) return name.slice(7)
    if (name.startsWith('v-')) return name
    return null
  }

  if (parser === 'angular') {
    if (name.startsWith('[') && name.endsWith(']')) return name.slice(1, -1)
    return null
  }

  return null
}

/**
 * Check for matches against a static list or possible regex patterns
 */
function hasMatch(name: string, list: Set<string>, patterns: RegExp[]): boolean {
  if (list.has(name)) return true

  for (let regex of patterns) {
    if (regex.test(name)) return true
  }

  return false
}

function parseRegex(str: string): RegExp | null {
  if (!str.startsWith('/')) return null

  let lastSlash = str.lastIndexOf('/')
  if (lastSlash <= 0) return null

  try {
    let pattern = str.slice(1, lastSlash)
    let flags = str.slice(lastSlash + 1)
    return new RegExp(pattern, flags)
  } catch {
    return null
  }
}


================================================
FILE: src/resolve.ts
================================================
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve'
import { expiringMap } from './expiring-map'

const fileSystem = new CachedInputFileSystem(fs, 30_000)

const esmResolver = ResolverFactory.createResolver({
  fileSystem,
  useSyncFileSystemCalls: true,
  extensions: ['.mjs', '.js'],
  mainFields: ['module'],
  conditionNames: ['node', 'import'],
})

const cjsResolver = ResolverFactory.createResolver({
  fileSystem,
  useSyncFileSystemCalls: true,
  extensions: ['.js', '.cjs'],
  mainFields: ['main'],
  conditionNames: ['node', 'require'],
})

const cssResolver = ResolverFactory.createResolver({
  fileSystem,
  useSyncFileSystemCalls: true,
  extensions: ['.css'],
  mainFields: ['style'],
  conditionNames: ['style'],
})

// This is a long-lived cache for resolved modules whether they exist or not
// Because we're compatible with a large number of plugins, we need to check
// for the existence of a module before attempting to import it. This cache
// is used to mitigate the cost of that check because Node.js does not cache
// failed module resolutions making repeated checks very expensive.
const resolveCache = expiringMap<string, string | null>(30_000)

export function maybeResolve(name: string) {
  let modpath = resolveCache.get(name)

  if (modpath === undefined) {
    try {
      modpath = resolveJsFrom(fileURLToPath(import.meta.url), name)
      resolveCache.set(name, modpath)
    } catch {
      resolveCache.set(name, null)
      return null
    }
  }

  return modpath
}

export async function loadIfExists<T>(name: string): Promise<T | null> {
  let modpath = maybeResolve(name)

  if (modpath) {
    let mod = await import(name)
    return mod.default ?? mod
  }

  return null
}

export function resolveJsFrom(base: string, id: string): string {
  try {
    return esmResolver.resolveSync({}, base, id) || id
  } catch {
    return cjsResolver.resolveSync({}, base, id) || id
  }
}

export function resolveCssFrom(base: string, id: string) {
  return cssResolver.resolveSync({}, base, id) || id
}


================================================
FILE: src/sorter.ts
================================================
import * as path from 'node:path'
import { pathToFileURL } from 'node:url'
import escalade from 'escalade/sync'
import * as console from './console'
import { expiringMap } from './expiring-map.js'
import { resolveJsFrom } from './resolve'
import { sortClasses, sortClassList } from './sorting.js'
import type { TransformerEnv, UnifiedApi } from './types'
import { cacheForDirs } from './utils.js'

export interface SorterOptions {
  /**
   * The directory used to resolve relative file paths.
   *
   * When not provided this will be:
   * - The current working directory
   */
  base?: string

  /**
   * The path to the file being formatted.
   *
   * When provided, Tailwind CSS is resolved relative to this path; otherwise,
   * it is resolved relative to `base`.
   */
  filepath?: string

  /**
   * Path to the Tailwind CSS config file (v3).
   *
   * Paths are resolved relative to `base`.
   */
  configPath?: string

  /**
   * Path to the CSS stylesheet used by Tailwind CSS (v4+).
   *
   * Paths are resolved relative to `base`.
   */
  stylesheetPath?: string

  /**
   * Whether or not to preserve whitespace around classes.
   *
   * Default: false
   */
  preserveWhitespace?: boolean

  /**
   * Whether or not to preserve duplicate classes.
   *
   * Default: false
   */
  preserveDuplicates?: boolean

  /**
   * The package name to use when loading Tailwind CSS.
   *
   * Useful when multiple versions are installed in the same project.
   *
   * Default: `tailwindcss`
   *
   * @internal
   */
  packageName?: string
}

export interface Sorter {
  /**
   * Sort one or more class attributes.
   *
   * Each element is the value of an HTML `class` attribute (or similar). i.e. a
   * space separated list of class names as a string.
   */
  sortClassAttributes(classes: string[]): string[]

  /**
   * Sort one or more class lists.
   *
   * Each element is an array of class names. Passing a space separated class
   * list in each element is not supported.
   *
   * Duplicates are removed by default unless `preserveDuplicates` is enabled.
   */
  sortClassLists(classes: string[][]): string[][]
}

type TailwindConfigOptions = {
  base?: string
  filepath?: string
  configPath?: string
  stylesheetPath?: string
  packageName?: string
}

function resolveIfRelative(base: string, filePath?: string) {
  if (!filePath) return null
  return path.isAbsolute(filePath) ? filePath : path.resolve(base, filePath)
}

let pathToApiMap = expiringMap<string | null, Promise<UnifiedApi>>(10_000)

/**
 * Get a Tailwind CSS API instance based on the provided options.
 * @internal
 */
export async function getTailwindConfig(options: TailwindConfigOptions): Promise<UnifiedApi> {
  let base = options.base ?? process.cwd()
  let inputDir = options.filepath ? path.dirname(options.filepath) : base

  let configPath = resolveIfRelative(base, options.configPath)
  let stylesheetPath = resolveIfRelative(base, options.stylesheetPath)

  // Locate Tailwind CSS itself
  //
  // We resolve this like we're in `inputDir` for better monorepo support as
  // Prettier may be configured at the workspace root but Tailwind CSS is
  // installed for a workspace package rather than the entire monorepo
  let [mod, pkgDir] = await resolveTailwindPath({ packageName: options.packageName }, inputDir)

  // Locate project stylesheet relative to the formatter config file
  //
  // We resolve this relative to the config file because it is *required*
  // to work with a project's custom config. Given that, resolving it
  // relative to where the path is defined makes the most sense.
  let stylesheet = resolveStylesheet(stylesheetPath, base)

  // Locate *explicit* v3 configs relative to the formatter config file
  //
  // We use this as a signal that we should always use v3 to format files even
  // when the local install is v4 — which means we'll use the bundled v3.
  let jsConfig = resolveJsConfigPath(configPath)

  // Locate the closest v3 config file
  //
  // Note:
  // We only need to do this when a stylesheet has not been provided otherwise
  // we'd know for sure this was a v4 project regardless of what local Tailwind
  // CSS installation is present. Additionally, if the local version is v4 we
  // skip this as we assume that the user intends to use that version.
  //
  // The config path is resolved in one of two ways:
  //
  // 1. When automatic, relative to the input file
  //
  // This ensures monorepos can load the "closest" JS config for a given file
  // which is important when a workspace package includes Tailwind CSS *and*
  // Prettier is configured globally instead of per-package.
  //
  // 2. When explicit via `configPath`, relative to `base`
  if (!stylesheet && !mod?.__unstable__loadDesignSystem) {
    jsConfig = jsConfig ?? findClosestJsConfig(inputDir)
  }

  // We've found a JS config either because it was specified by the user
  // or because it was automatically located. This means we should use v3.
  if (jsConfig) {
    if (!stylesheet) {
      return pathToApiMap.remember(`${pkgDir}:${jsConfig}`, async () => {
        const { loadV3 } = await import('./versions/v3')
        return loadV3(pkgDir, jsConfig)
      })
    }

    // In this case the user explicitly gave us a stylesheet and a config.
    // Warn them about this and use the bundled v4.
    console.error(
      'explicit-stylesheet-and-config-together',
      base,
      `You have specified a Tailwind CSS stylesheet and a Tailwind CSS config at the same time. Use stylesheetPath unless you are using v3. Preferring the stylesheet.`,
    )
  }

  if (mod && !mod.__unstable__loadDesignSystem) {
    if (!stylesheet) {
      return pathToApiMap.remember(`${pkgDir}:${jsConfig}`, async () => {
        const { loadV3 } = await import('./versions/v3')
        return loadV3(pkgDir, jsConfig)
      })
    }

    // In this case the user explicitly gave us a stylesheet but their local
    // installation is not v4. We'll fallback to the bundled v4 in this case.
    mod = null
    console.error(
      'stylesheet-unsupported',
      base,
      'You have specified a Tailwind CSS stylesheet but your installed version of Tailwind CSS does not support this feature.',
    )
  }

  // If we've detected a local version of v4 then we should fallback to using
  // its included theme as the stylesheet if the user didn't give us one.
  if (mod && mod.__unstable__loadDesignSystem && pkgDir) {
    stylesheet ??= `${pkgDir}/theme.css`
  }

  return pathToApiMap.remember(`${pkgDir}:${stylesheet}`, async () => {
    const { loadV4 } = await import('./versions/v4')
    return loadV4(mod, stylesheet)
  })
}

let resolvedModCache = expiringMap<string, [any, string | null]>(10_000)

async function resolveTailwindPath(
  options: { packageName?: string },
  baseDir: string,
): Promise<[any, string | null]> {
  let pkgName = options.packageName ?? 'tailwindcss'
  let makeKey = (dir: string) => `${pkgName}:${dir}`

  // Check cache for this directory
  let cached = resolvedModCache.get(makeKey(baseDir))
  if (cached !== undefined) {
    return cached
  }

  let resolve = async () => {
    let pkgDir: string | null = null
    let mod: any = null

    try {
      let pkgPath = resolveJsFrom(baseDir, pkgName)
      mod = await import(pathToFileURL(pkgPath).toString())

      let pkgFile = resolveJsFrom(baseDir, `${pkgName}/package.json`)
      pkgDir = path.dirname(pkgFile)
    } catch {}

    return [mod, pkgDir] as [any, string | null]
  }

  let result = await resolve()

  // Cache all directories from baseDir up to package location
  let [, pkgDir] = result
  if (pkgDir) {
    cacheForDirs(resolvedModCache, baseDir, result, pkgDir, makeKey)
  } else {
    resolvedModCache.set(makeKey(baseDir), result)
  }

  return result
}

function resolveJsConfigPath(configPath: string | null): string | null {
  if (!configPath) return null
  if (configPath.endsWith('.css')) return null
  return configPath
}

let configPathCache = new Map<string, string | null>()

function findClosestJsConfig(inputDir: string): string | null {
  // Check cache for this directory
  let cached = configPathCache.get(inputDir)
  if (cached !== undefined) {
    return cached
  }

  // Resolve
  let configPath: string | null = null
  try {
    let foundPath = escalade(inputDir, (_, names) => {
      if (names.includes('tailwind.config.js')) return 'tailwind.config.js'
      if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'
      if (names.includes('tailwind.config.mjs')) return 'tailwind.config.mjs'
      if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'
    })
    configPath = foundPath ?? null
  } catch {}

  // Cache all directories from inputDir up to config location
  if (configPath) {
    cacheForDirs(configPathCache, inputDir, configPath, path.dirname(configPath))
  } else {
    configPathCache.set(inputDir, null)
  }

  return configPath
}

function resolveStylesheet(stylesheetPath: string | null, base: string): string | null {
  if (!stylesheetPath) return null

  if (
    stylesheetPath.endsWith('.js') ||
    stylesheetPath.endsWith('.mjs') ||
    stylesheetPath.endsWith('.cjs') ||
    stylesheetPath.endsWith('.ts') ||
    stylesheetPath.endsWith('.mts') ||
    stylesheetPath.endsWith('.cts')
  ) {
    console.error(
      'stylesheet-is-js-file',
      base,
      "Your `stylesheetPath` option points to a JS/TS config file. You must point to your project's `.css` file for v4 projects.",
    )
  } else if (
    stylesheetPath.endsWith('.sass') ||
    stylesheetPath.endsWith('.scss') ||
    stylesheetPath.endsWith('.less') ||
    stylesheetPath.endsWith('.styl')
  ) {
    console.error(
      'stylesheet-is-preprocessor-file',
      base,
      'Your `stylesheetPath` option points to a preprocessor file. This is unsupported and you may get unexpected results.',
    )
  } else if (!stylesheetPath.endsWith('.css')) {
    console.error(
      'stylesheet-is-not-css-file',
      base,
      'Your `stylesheetPath` option does not point to a CSS file. This is unsupported and you may get unexpected results.',
    )
  }

  return stylesheetPath
}

/**
 * Creates a sorter instance for sorting Tailwind CSS classes.
 *
 * This function initializes a sorter with the specified Tailwind CSS configuration.
 * The sorter can be used to sort class attributes (space-separated strings) or
 * class lists (arrays of class names).

 * @example
 * ```ts
 * const sorter = await createSorter({})
 *
 * // Sort class lists
 * const sorted = sorter.sortClassLists([['p-4', 'm-2']])
 * // Returns: [['m-2', 'p-4']]
 * ```
 */
export async function createSorter(opts: SorterOptions): Promise<Sorter> {
  let preserveDuplicates = opts.preserveDuplicates ?? false
  let preserveWhitespace = opts.preserveWhitespace ?? false

  let api = await getTailwindConfig({
    base: opts.base,
    filepath: opts.filepath,
    configPath: opts.configPath,
    stylesheetPath: opts.stylesheetPath,
    packageName: opts.packageName,
  })

  let env: TransformerEnv = {
    context: api,
    changes: [],
    options: {
      tailwindPreserveWhitespace: preserveWhitespace,
      tailwindPreserveDuplicates: preserveDuplicates,
      tailwindPackageName: opts.packageName,
    } as any,
    matcher: undefined as any,
  }

  return {
    sortClassLists(classes) {
      return classes.map((list) => {
        let result = sortClassList({
          api,
          classList: list,
          removeDuplicates: !preserveDuplicates,
        })

        return result.classList
      })
    },

    sortClassAttributes(classes) {
      return classes.map((list) => sortClasses(list, { env }))
    },
  }
}


================================================
FILE: src/sorting.ts
================================================
import type { TransformerEnv, UnifiedApi } from './types'
import { bigSign } from './utils'

export interface SortOptions {
  ignoreFirst?: boolean
  ignoreLast?: boolean
  removeDuplicates?: boolean
  collapseWhitespace?: false | { start: boolean; end: boolean }
}

export function sortClasses(
  classStr: string,
  {
    env,
    ignoreFirst = false,
    ignoreLast = false,
    removeDuplicates = true,
    collapseWhitespace = { start: true, end: true },
  }: SortOptions & {
    env: TransformerEnv
  },
): string {
  if (typeof classStr !== 'string' || classStr === '') {
    return classStr
  }

  // Ignore class attributes containing `{{`, to match Prettier behaviour:
  // https://github.com/prettier/prettier/blob/8a88cdce6d4605f206305ebb9204a0cabf96a070/src/language-html/embed/class-names.js#L9
  if (classStr.includes('{{')) {
    return classStr
  }

  if (env.options.tailwindPreserveWhitespace) {
    collapseWhitespace = false
  }

  if (env.options.tailwindPreserveDuplicates) {
    removeDuplicates = false
  }

  // This class list is purely whitespace
  // Collapse it to a single space if the option is enabled
  if (collapseWhitespace && /^[\t\r\f\n ]+$/.test(classStr)) {
    return ' '
  }

  let result = ''
  let parts = classStr.split(/([\t\r\f\n ]+)/)
  let classes = parts.filter((_, i) => i % 2 === 0)
  let whitespace = parts.filter((_, i) => i % 2 !== 0)

  if (classes[classes.length - 1] === '') {
    classes.pop()
  }

  if (collapseWhitespace) {
    whitespace = whitespace.map(() => ' ')
  }

  let prefix = ''
  if (ignoreFirst) {
    prefix = `${classes.shift() ?? ''}${whitespace.shift() ?? ''}`
  }

  let suffix = ''
  if (ignoreLast) {
    suffix = `${whitespace.pop() ?? ''}${classes.pop() ?? ''}`
  }

  let { classList, removedIndices } = sortClassList({
    classList: classes,
    api: env.context,
    removeDuplicates,
  })

  // Remove whitespace that appeared before a removed classes
  whitespace = whitespace.filter((_, index) => !removedIndices.has(index + 1))

  for (let i = 0; i < classList.length; i++) {
    result += `${classList[i]}${whitespace[i] ?? ''}`
  }

  if (collapseWhitespace) {
    prefix = prefix.replace(/\s+$/g, ' ')
    suffix = suffix.replace(/^\s+/g, ' ')

    result = result
      .replace(/^\s+/, collapseWhitespace.start ? '' : ' ')
      .replace(/\s+$/, collapseWhitespace.end ? '' : ' ')
  }

  return prefix + result + suffix
}

export function sortClassList({
  classList,
  api,
  removeDuplicates,
}: {
  classList: string[]
  api: UnifiedApi
  removeDuplicates: boolean
}) {
  // Re-order classes based on the Tailwind CSS configuration
  let orderedClasses = api.getClassOrder(classList)

  orderedClasses.sort(([nameA, a], [nameZ, z]) => {
    // Move `...` to the end of the list
    if (nameA === '...' || nameA === '…') return 1
    if (nameZ === '...' || nameZ === '…') return -1

    if (a === z) return 0
    if (a === null) return -1
    if (z === null) return 1
    return bigSign(a - z)
  })

  // Remove duplicate Tailwind classes
  let removedIndices = new Set<number>()

  if (removeDuplicates) {
    let seenClasses = new Set<string>()

    orderedClasses = orderedClasses.filter(([cls, order], index) => {
      if (seenClasses.has(cls)) {
        removedIndices.add(index)
        return false
      }

      // Only consider known classes when removing duplicates
      if (order !== null) {
        seenClasses.add(cls)
      }

      return true
    })
  }

  return {
    classList: orderedClasses.map(([className]) => className),
    removedIndices,
  }
}


================================================
FILE: src/transform.ts
================================================
import type { AstPath, Plugin } from 'prettier'
import type { TransformerEnv } from './types'

export function defineTransform<T>(opts: TransformOptions<T>) {
  return opts
}

export interface LazyPluginLoad {
  name: string
  importer: () => Promise<unknown>
}

export type PluginLoad = LazyPluginLoad | Plugin<any>

export interface TransformOptions<T> {
  /**
   * Static attributes that are supported by default
   */
  staticAttrs?: string[]

  /**
   * Dynamic / expression attributes that are supported by default
   */
  dynamicAttrs?: string[]

  /**
   * Load the given plugins for the parsers and printers
   */
  load?: PluginLoad[]

  /**
   * A list of compatible, third-party plugins for this transformation step
   *
   * The loading of these is delayed until the actual parse call as
   * using the parse() function from these plugins may cause errors
   * if they haven't already been loaded by Prettier.
   */
  compatible?: string[]

  /**
   * A list of supported parser names
   */
  parsers: Record<
    string,
    {
      /**
       * Load the given plugins for the parsers and printers
       */
      load?: PluginLoad[]

      /**
       * Static attributes that are supported by default
       */
      staticAttrs?: string[]

      /**
       * Dynamic / expression attributes that are supported by default
       */
      dynamicAttrs?: string[]
    }
  >

  /**
   * A list of supported parser names
   */
  printers?: Record<string, {}>

  /**
   * Transform entire ASTs
   *
   * @param ast  The AST to transform
   * @param env  Provides options and mechanisms to sort classes
   */
  transform?(ast: T, env: TransformerEnv): void

  /**
   * Transform entire ASTs
   *
   * @param ast  The AST to transform
   * @param env  Provides options and mechanisms to sort classes
   */
  reprint?(path: AstPath<T>, options: TransformerEnv): void
}


================================================
FILE: src/types.ts
================================================
import type { ParserOptions } from 'prettier'
import type { Matcher } from './options'

export interface TransformerMetadata {
  // Default customizations for a given transformer
  functions?: string[]
  staticAttrs?: string[]
  dynamicAttrs?: string[]
}

export interface Customizations {
  functions: Set<string>
  staticAttrs: Set<string>
  dynamicAttrs: Set<string>
  staticAttrsRegex: RegExp[]
  dynamicAttrsRegex: RegExp[]
  functionsRegex: RegExp[]
}

export interface UnifiedApi {
  getClassOrder(classList: string[]): [string, bigint | null][]
}

export interface TransformerEnv {
  context: UnifiedApi
  matcher: Matcher
  options: ParserOptions
  changes: StringChangePositional[]
}

export interface StringChangePositional {
  start: { line: number; column: number }
  end: { line: number; column: number }
  before: string
  after: string
}

export interface StringChange {
  start: number
  end: number
  before: string
  after: string
}


================================================
FILE: src/utils.bench.ts
================================================
import { bench, describe } from 'vitest'
import type { StringChange } from './types'
import { spliceChangesIntoString } from './utils'

describe('spliceChangesIntoString', () => {
  // 44 bytes
  let strTemplate = 'the quick brown fox jumps over the lazy dog '
  let changesTemplate: StringChange[] = [
    { start: 10, end: 15, before: 'brown', after: 'purple' },
    { start: 4, end: 9, before: 'quick', after: 'slow' },
  ]

  function buildFixture(repeatCount: number, changeCount: number) {
    // A large set of changes across random places in the string
    let indxes = new Set(
      Array.from({ length: changeCount }, () => Math.ceil(Math.random() * repeatCount)),
    )

    let changes: StringChange[] = Array.from(indxes).flatMap((idx) => {
      return changesTemplate.map((change) => ({
        start: change.start + strTemplate.length * idx,
        end: change.end + strTemplate.length * idx,
        before: change.before,
        after: change.after,
      }))
    })

    return [strTemplate.repeat(repeatCount), changes] as const
  }

  let [strS, changesS] = buildFixture(5, 2)
  bench('small string', () => {
    spliceChangesIntoString(strS, changesS)
  })

  let [strM, changesM] = buildFixture(100, 5)
  bench('medium string', () => {
    spliceChangesIntoString(strM, changesM)
  })

  let [strL, changesL] = buildFixture(1_000, 50)
  bench('large string', () => {
    spliceChangesIntoString(strL, changesL)
  })

  let [strXL, changesXL] = buildFixture(100_000, 500)
  bench('extra large string', () => {
    spliceChangesIntoString(strXL, changesXL)
  })

  let [strXL2, changesXL2] = buildFixture(100_000, 5_000)
  bench('extra large string (5k changes)', () => {
    spliceChangesIntoString(strXL2, changesXL2)
  })
})


================================================
FILE: src/utils.test.ts
================================================
import { describe, test } from 'vitest'
import type { StringChange } from './types'
import { spliceChangesIntoString } from './utils'

describe('spliceChangesIntoString', () => {
  test('can apply changes to a string', ({ expect }) => {
    let str = 'the quick brown fox jumps over the lazy dog'
    let changes: StringChange[] = [
      //
      { start: 10, end: 15, before: 'brown', after: 'purple' },
    ]

    expect(spliceChangesIntoString(str, changes)).toBe(
      'the quick purple fox jumps over the lazy dog',
    )
  })

  test('changes are applied in order', ({ expect }) => {
    let str = 'the quick brown fox jumps over the lazy dog'
    let changes: StringChange[] = [
      //
      { start: 10, end: 15, before: 'brown', after: 'purple' },
      { start: 4, end: 9, before: 'quick', after: 'slow' },
    ]

    expect(spliceChangesIntoString(str, changes)).toBe(
      'the slow purple fox jumps over the lazy dog',
    )
  })
})


================================================
FILE: src/utils.ts
================================================
import * as path from 'node:path'
import type { StringChange } from './types'

// For loading prettier plugins only if they exist
export function loadIfExists(name: string): any {
  try {
    if (require.resolve(name)) {
      return require(name)
    }
  } catch {
    return null
  }
}

interface PathEntry<T, Meta> {
  node: T
  parent: T | null
  key: string | null
  index: number | null
  meta: Meta
}

export type Path<T, Meta> = PathEntry<T, Meta>[]

type Visitor<T, Meta extends Record<string, unknown>> = (
  node: T,
  path: Path<T, Meta>,
  meta: Partial<Meta>,
) => void | false

type Visitors<T, Meta extends Record<string, unknown>> = Record<string, Visitor<T, Meta>>

function isNodeLike(value: any): value is { type: string } {
  return typeof value?.type === 'string'
}

// https://lihautan.com/manipulating-ast-with-javascript/
export function visit<T extends {}, Meta extends Record<string, unknown>>(
  ast: T,
  callbackMap: Visitors<T, Meta> | Visitor<T, Meta>,
) {
  function _visit(node: any, path: Path<T, Meta>, meta: Meta) {
    if (typeof callbackMap === 'function') {
      if (callbackMap(node, path, meta) === false) {
        return
      }
    } else if (node.type in callbackMap) {
      if (callbackMap[node.type](node, path, meta) === false) {
        return
      }
    }

    const keys = Object.keys(node)
    for (let i = 0; i < keys.length; i++) {
      const child = node[keys[i]]
      if (Array.isArray(child)) {
        for (let j = 0; j < child.length; j++) {
          if (isNodeLike(child[j])) {
            let newMeta = { ...meta }
            let newPath = [
              {
                node: child[j],
                parent: node,
                key: keys[i],
                index: j,
                meta: newMeta,
              },
              ...path,
            ]

            _visit(child[j], newPath, newMeta)
          }
        }
      } else if (isNodeLike(child)) {
        let newMeta = { ...meta }
        let newPath = [
          {
            node: child,
            parent: node,
            key: keys[i],
            index: i,
            meta: newMeta,
          },
          ...path,
        ]

        _visit(child, newPath, newMeta)
      }
    }
  }

  let newMeta: Meta = {} as any
  let newPath: Path<T, Meta> = [
    {
      node: ast,
      parent: null,
      key: null,
      index: null,
      meta: newMeta,
    },
  ]

  _visit(ast, newPath, newMeta)
}

/**
 * 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.after

  for (let i = 1; i < changes.length; ++i) {
    let change = changes[i]

    result += str.slice(previous.end, change.start)
    result += change.after

    previous = change
  }

  // Add leftover string from last chunk to end
  result += str.slice(previous.end)

  return result
}

export function bigSign(bigIntValue: bigint) {
  return Number(bigIntValue > 0n) - Number(bigIntValue < 0n)
}

/**
 * Cache a value for all directories from `inputDir` up to `targetDir` (inclusive).
 * Stops early if an existing cache entry is found.
 *
 * How it works:
 *
 * For a file at '/repo/packages/ui/src/Button.tsx' with config at '/repo/package.json'
 *
 * `cacheForDirs(cache, '/repo/packages/ui/src', '/repo/package.json', '/repo')`
 *
 * Caches:
 * - '/repo/packages/ui/src' -> '/repo/package.json'
 * - '/repo/packages/ui'     -> '/repo/package.json'
 * - '/repo/packages'        -> '/repo/package.json'
 * - '/repo'                 -> '/repo/package.json'
 */
export function cacheForDirs<V>(
  cache: { set(key: string, value: V): void; get(key: string): V | undefined },
  inputDir: string,
  value: V,
  targetDir: string,
  makeKey: (dir: string) => string = (dir) => dir,
): void {
  let dir = inputDir
  while (dir !== path.dirname(dir) && dir.length >= targetDir.length) {
    const key = makeKey(dir)
    // Stop caching if we hit an existing entry
    if (cache.get(key) !== undefined) break

    cache.set(key, value)
    if (dir === targetDir) break
    dir = path.dirname(dir)
  }
}


================================================
FILE: src/versions/assets.ts
================================================
// @ts-ignore
import index from 'tailwindcss-v4/index.css'
// @ts-ignore
import preflight from 'tailwindcss-v4/preflight.css'
// @ts-ignore
import theme from 'tailwindcss-v4/theme.css'
// @ts-ignore
import utilities from 'tailwindcss-v4/utilities.css'

export const assets: Record<string, string> = {
  tailwindcss: index,
  'tailwindcss/index': index,
  'tailwindcss/index.css': index,

  'tailwindcss/preflight': preflight,
  'tailwindcss/preflight.css': preflight,

  'tailwindcss/theme': theme,
  'tailwindcss/theme.css': theme,

  'tailwindcss/utilities': utilities,
  'tailwindcss/utilities.css': utilities,
}


================================================
FILE: src/versions/v3.ts
================================================
// @ts-check
import * as path from 'node:path'
import { pathToFileURL } from 'node:url'
import clearModule from 'clear-module'
import { createJiti } from 'jiti'
// @ts-ignore
import { generateRules as generateRulesFallback } from 'tailwindcss-v3/lib/lib/generateRules'
// @ts-ignore
import { createContext as createContextFallback } from 'tailwindcss-v3/lib/lib/setupContextUtils'
import resolveConfigFallback from 'tailwindcss-v3/resolveConfig'
import type { RequiredConfig } from 'tailwindcss-v3/types/config.js'
import type { UnifiedApi } from '../types'
import { bigSign } from '../utils'

interface LegacyTailwindContext {
  tailwindConfig: {
    prefix: string | ((selector: string) => string)
  }

  getClassOrder?: (classList: string[]) => [string, bigint | null][]

  layerOrder: {
    components: bigint
  }
}

interface GenerateRules {
  (classes: Iterable<string>, context: LegacyTailwindContext): [bigint][]
}

function prefixCandidate(context: LegacyTailwindContext, selector: string): string {
  let prefix = context.tailwindConfig.prefix
  return typeof prefix === 'function' ? prefix(selector) : prefix + selector
}

export async function loadV3(pkgDir: string | null, jsConfig: string | null): Promise<UnifiedApi> {
  let createContext = createContextFallback
  let generateRules: GenerateRules = generateRulesFallback
  let resolveConfig = resolveConfigFallback
  let tailwindConfig: RequiredConfig = { content: [] }

  try {
    if (pkgDir) {
      resolveConfig = require(path.join(pkgDir, 'resolveConfig'))
      createContext = require(path.join(pkgDir, 'lib/lib/setupContextUtils')).createContext
      generateRules = require(path.join(pkgDir, 'lib/lib/generateRules')).generateRules
    }
  } catch {}

  try {
    if (jsConfig) {
      clearModule(jsConfig)
      let jiti = createJiti(import.meta.url, {
        moduleCache: false,
        fsCache: false,
        interopDefault: true,
      })
      let url = pathToFileURL(jsConfig)
      tailwindConfig = await jiti.import<RequiredConfig>(url.href, { default: true })
    }
  } catch (err) {
    console.error(`Unable to load your Tailwind CSS v3 config: ${jsConfig}`)
    throw err
  }

  // suppress "empty content" warning
  tailwindConfig.content = ['no-op']

  // Create the context
  let context: LegacyTailwindContext = createContext(resolveConfig(tailwindConfig))

  // Polyfill for older Tailwind CSS versions
  function getClassOrderPolyfill(classes: string[]): [string, bigint | null][] {
    // A list of utilities that are used by certain Tailwind CSS utilities but
    // that don't exist on their own. This will result in them "not existing" and
    // sorting could be weird since you still require them in order to make the
    // host utitlies work properly. (Thanks Biology)
    let parasiteUtilities = new Set([
      prefixCandidate(context, 'group'),
      prefixCandidate(context, 'peer'),
    ])

    let classNamesWithOrder: [string, bigint | null][] = []

    for (let className of classes) {
      let order: bigint | null =
        generateRules(new Set([className]), context).sort(([a], [z]) => bigSign(z - a))[0]?.[0] ??
        null

      if (order === null && parasiteUtilities.has(className)) {
        // This will make sure that it is at the very beginning of the
        // `components` layer which technically means 'before any
        // components'.
        order = context.layerOrder.components
      }

      classNamesWithOrder.push([className, order])
    }

    return classNamesWithOrder
  }

  context.getClassOrder ??= getClassOrderPolyfill

  return {
    getClassOrder: (classList: string[]) => {
      return context.getClassOrder
        ? context.getClassOrder(classList)
        : getClassOrderPolyfill(classList)
    },
  }
}


================================================
FILE: src/versions/v4.ts
================================================
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { pathToFileURL } from 'node:url'
import { createJiti, type Jiti } from 'jiti'
import * as v4 from 'tailwindcss-v4'
import { resolveCssFrom, resolveJsFrom } from '../resolve'
import type { UnifiedApi } from '../types'
import { assets } from './assets'

interface DesignSystem {
  getClassOrder(classList: string[]): [string, bigint | null][]
}

interface LoadOptions {
  base: string

  loadModule?(
    id: string,
    base: string,
    resourceType: string,
  ): Promise<{
    base: string
    module: unknown
  }>

  loadPlugin?(id: string, base: string, resourceType: string): Promise<unknown>
  loadConfig?(id: string, base: string, resourceType: string): Promise<unknown>

  loadStylesheet?(
    id: string,
    base: string,
  ): Promise<{
    base: string
    content: string
  }>
}

interface ApiV4 {
  __unstable__loadDesignSystem(css: string, options: LoadOptions): Promise<DesignSystem>
}

export async function loadV4(mod: ApiV4 | null, stylesheet: string | null): Promise<UnifiedApi> {
  // This is not Tailwind v4
  let isFallback = false
  if (!mod || !mod.__unstable__loadDesignSystem) {
    mod = v4 as ApiV4
    isFallback = true
  }

  // Create a Jiti instance that can be used to load plugins and config files
  let jiti = createJiti(import.meta.url, {
    moduleCache: false,
    fsCache: false,
  })

  let css: string
  let importBasePath: string

  if (stylesheet) {
    // Resolve imports in the entrypoint to a flat CSS tree
    css = await fs.readFile(stylesheet, 'utf-8')
    importBasePath = path.dirname(stylesheet)
  } else {
    importBasePath = process.cwd()
    stylesheet = path.join(importBasePath, 'fake.css')
    css = assets['tailwindcss/theme.css']
  }

  // Load the design system and set up a compatible context object that is
  // usable by the rest of the plugin
  let design = await mod.__unstable__loadDesignSystem(css, {
    base: importBasePath,

    // v4.0.0-alpha.25+
    loadModule: createLoader({
      legacy: false,
      jiti,
      filepath: stylesheet,
      onError: (id, err, resourceType) => {
        console.error(`Unable to load ${resourceType}: ${id}`, err)

        if (resourceType === 'config') {
          return {}
        } else if (resourceType === 'plugin') {
          return () => {}
        }
      },
    }),

    loadStylesheet: async (id: string, base: string) => {
      try {
        let resolved = resolveCssFrom(base, id)

        return {
          base: path.dirname(resolved),
          content: await fs.readFile(resolved, 'utf-8'),
        }
      } catch (err) {
        if (isFallback && id in assets) {
          return { base, content: assets[id] }
        }

        throw err
      }
    },

    // v4.0.0-alpha.24 and below
    loadPlugin: createLoader({
      legacy: true,
      jiti,
      filepath: stylesheet,
      onError(id, err) {
        console.error(`Unable to load plugin: ${id}`, err)

        return () => {}
      },
    }),

    loadConfig: createLoader({
      legacy: true,
      jiti,
      filepath: stylesheet,
      onError(id, err) {
        console.error(`Unable to load config: ${id}`, err)

        return {}
      },
    }),
  })

  return {
    getClassOrder: (classList: string[]) => {
      return design.getClassOrder(classList)
    },
  }
}

function createLoader<T>({
  legacy,
  jiti,
  filepath,
  onError,
}: {
  legacy: true
  jiti: Jiti
  filepath: string
  onError: (id: string, error: unknown, resourceType: string) => T
}): (id: string) => Promise<unknown>

function createLoader<T>({
  legacy,
  jiti,
  filepath,
  onError,
}: {
  legacy: false
  jiti: Jiti
  filepath: string
  onError: (id: string, error: unknown, resourceType: string) => T
}): (
  id: string,
  base: string,
  resourceType: string,
) => Promise<{
  base: string
  module: unknown
}>

/**
 * Create a loader function that can load plugins and config files relative to
 * the CSS file that uses them. However, we don't want missing files to prevent
 * everything from working so we'll let the error handler decide how to proceed.
 */
function createLoader<T>({
  legacy,
  jiti,
  filepath,
  onError,
}: {
  legacy: boolean
  jiti: Jiti
  filepath: string
  onError: (id: string, error: unknown, resourceType: string) => T
}) {
  let cacheKey = `${+Date.now()}`

  async function loadFile(id: string, base: string, resourceType: string) {
    try {
      let resolved = resolveJsFrom(base, id)

      let url = pathToFileURL(resolved)
      url.searchParams.append('t', cacheKey)

      return await jiti.import(url.href, { default: true })
    } catch (err) {
      return onError(id, err, resourceType)
    }
  }

  if (legacy) {
    let baseDir = path.dirname(filepath)
    return (id: string) => loadFile(id, baseDir, 'module')
  }

  return async (id: string, base: string, resourceType: string) => {
    return {
      base,
      module: await loadFile(id, base, resourceType),
    }
  }
}


================================================
FILE: tests/fixtures/basic/index.html
================================================
<div class="sm:bg-tomato bg-red-500"></div>


================================================
FILE: tests/fixtures/basic/output.html
================================================
<div class="bg-red-500 sm:bg-tomato"></div>


================================================
FILE: tests/fixtures/basic/prettier.config.js
================================================
module.exports = {};


================================================
FILE: tests/fixtures/basic/tailwind.config.js
================================================
module.exports = {
  theme: {
    extend: {
      colors: {
        tomato: 'tomato',
      },
    },
  },
}


================================================
FILE: tests/fixtures/cjs/index.html
================================================
<div class="sm:bg-hotpink bg-red-500"></div>


================================================
FILE: tests/fixtures/cjs/output.html
================================================
<div class="bg-red-500 sm:bg-hotpink"></div>


================================================
FILE: tests/fixtures/cjs/prettier.config.js
================================================
module.exports = {};


================================================
FILE: tests/fixtures/cjs/tailwind.config.cjs
================================================
module.exports = {
  theme: {
    extend: {
      colors: {
        hotpink: 'hotpink',
      },
    },
  },
}


================================================
FILE: tests/fixtures/custom-jsx/index.jsx
================================================
const a = sortMeFn("sm:p-1 p-2");
const b = sortMeFn({
  foo: "sm:p-1 p-2",
});

const c = dontSortFn("sm:p-1 p-2");
const d = sortMeTemplate`sm:p-1 p-2`;
const e = dontSortMeTemplate`sm:p-1 p-2`;
const f = tw.foo`sm:p-1 p-2`;
const g = tw.foo.bar`sm:p-1 p-2`;
const h = no.foo`sm:p-1 p-2`;
const i = no.tw`sm:p-1 p-2`;
const k = tw.foo('sm:p-1 p-2');
const l = tw.foo.bar('sm:p-1 p-2');
const m = no.foo('sm:p-1 p-2');
const n = no.tw('sm:p-1 p-2');
const o = tw(Foo)`sm:p-1 p-2`;
const p = tw(Foo)(Bar)`sm:p-1 p-2`;
const q = no(Foo)`sm:p-1 p-2`;
const r = no.tw(Foo)`sm:p-1 p-2`;
const s = tw(Foo)('sm:p-1 p-2');
const t = tw(Foo)(Bar)('sm:p-1 p-2');
const u = no(Foo)('sm:p-1 p-2');
const v = no.tw(Foo)('sm:p-1 p-2');
const w = tw.div(Foo)`sm:p-1 p-2`;
const x = tw(Foo).div`sm:p-1 p-2`;
const y = no.tw(Foo)`sm:p-1 p-2`;
const z = no(Foo).tw`sm:p-1 p-2`;

const A = (props) => <div className={props.sortMe} />;
const B = () => <A sortMe="sm:p-1 p-2" dontSort="sm:p-1 p-2" />;


================================================
FILE: tests/fixtures/custom-jsx/output.jsx
================================================
const a = sortMeFn("p-2 sm:p-1");
const b = sortMeFn({
  foo: "p-2 sm:p-1",
});

const c = dontSortFn("sm:p-1 p-2");
const d = sortMeTemplate`p-2 sm:p-1`;
const e = dontSortMeTemplate`sm:p-1 p-2`;
const f = tw.foo`p-2 sm:p-1`;
const g = tw.foo.bar`p-2 sm:p-1`;
const h = no.foo`sm:p-1 p-2`;
const i = no.tw`sm:p-1 p-2`;
const k = tw.foo("p-2 sm:p-1");
const l = tw.foo.bar("p-2 sm:p-1");
const m = no.foo("sm:p-1 p-2");
const n = no.tw("sm:p-1 p-2");
const o = tw(Foo)`p-2 sm:p-1`;
const p = tw(Foo)(Bar)`p-2 sm:p-1`;
const q = no(Foo)`sm:p-1 p-2`;
const r = no.tw(Foo)`sm:p-1 p-2`;
const s = tw(Foo)("p-2 sm:p-1");
const t = tw(Foo)(Bar)("p-2 sm:p-1");
const u = no(Foo)("sm:p-1 p-2");
const v = no.tw(Foo)("sm:p-1 p-2");
const w = tw.div(Foo)`p-2 sm:p-1`;
const x = tw(Foo).div`p-2 sm:p-1`;
const y = no.tw(Foo)`sm:p-1 p-2`;
const z = no(Foo).tw`sm:p-1 p-2`;

const A = (props) => <div className={props.sortMe} />;
const B = () => <A sortMe="p-2 sm:p-1" dontSort="sm:p-1 p-2" />;


================================================
FILE: tests/fixtures/custom-jsx/prettier.config.js
================================================
module.exports = {
  tailwindFunctions: ['sortMeFn', 'sortMeTemplate', 'tw'],
  tailwindAttributes: ['sortMe'],
};


================================================
FILE: tests/fixtures/custom-jsx/tailwind.config.js
================================================
module.exports = {
  theme: {},
}


================================================
FILE: tests/fixtures/custom-pkg-name-v3/config.js
================================================
module.exports = {
  theme: {
    extend: {
      colors: {
        'tomato': 'tomato',
      }
    }
  }
}


================================================
FILE: tests/fixtures/custom-pkg-name-v3/index.html
================================================
<div class="sm:bg-tomato bg-red-500"></div>


================================================
FILE: tests/fixtures/custom-pkg-name-v3/output.html
================================================
<div class="bg-red-500 sm:bg-tomato"></div>


================================================
FILE: tests/fixtures/custom-pkg-name-v3/package.json
================================================
{
  "dependencies": {
    "tailwindcss-v3": "npm:tailwindcss@^3.4.17"
  },
  "prettier": {
    "tailwindPackageName": "tailwindcss-v3",
    "tailwindConfig": "./config.js"
  }
}


================================================
FILE: tests/fixtures/custom-pkg-name-v4/app.css
================================================
@import 'tailwindcss-v4';

@theme {
  --color-tomato: tomato;
}


================================================
FILE: tests/fixtures/custom-pkg-name-v4/index.html
================================================
<div class="sm:bg-tomato bg-red-500"></div>


================================================
FILE: tests/fixtures/custom-pkg-name-v4/output.html
================================================
<div class="bg-red-500 sm:bg-tomato"></div>


================================================
FILE: tests/fixtures/custom-pkg-name-v4/package.json
================================================
{
  "dependencies": {
    "tailwindcss-v4": "npm:tailwindcss@^4.1.7"
  },
  "prettier": {
    "tailwindPackageName": "tailwindcss-v4",
    "tailwindStylesheet": "./app.css"
  }
}


================================================
FILE: tests/fixtures/custom-vue/index.vue
================================================
<script setup>
  let a = sortMeFn("sm:p-1 p-2");
  let b = sortMeFn({"sm:p-1 p-2": true});
  let c = dontSortFn("sm:p-1 p-2");
  let d = sortMeTemplate`sm:p-1 p-2`;
  let e = dontSortMeTemplate`sm:p-1 p-2`;
</script>
<template>
  <div class="sm:p-1 p-2" sortMe="sm:p-1 p-2" dontSortMe="sm:p-1 p-2"></div>
  <div :class="{'sm:p-1 p-2': true}"></div>
  <div :sortMe="{'sm:p-1 p-2': true}"></div>
</template>


================================================
FILE: tests/fixtures/custom-vue/output.vue
================================================
<script setup>
let a = sortMeFn("p-2 sm:p-1");
let b = sortMeFn({ "p-2 sm:p-1": true });
let c = dontSortFn("sm:p-1 p-2");
let d = sortMeTemplate`p-2 sm:p-1`;
let e = dontSortMeTemplate`sm:p-1 p-2`;
</script>
<template>
  <div class="p-2 sm:p-1" sortMe="p-2 sm:p-1" dontSortMe="sm:p-1 p-2"></div>
  <div :class="{ 'p-2 sm:p-1': true }"></div>
  <div :sortMe="{ 'p-2 sm:p-1': true }"></div>
</template>


================================================
FILE: tests/fixtures/custom-vue/prettier.config.js
================================================
module.exports = {
  tailwindFunctions: ['sortMeFn', 'sortMeTemplate'],
  tailwindAttributes: ['sortMe'],
};


================================================
FILE: tests/fixtures/custom-vue/tailwind.config.js
================================================
module.exports = {
  theme: {},
}


================================================
FILE: tests/fixtures/esm/index.html
================================================
<div class="sm:bg-hotpink bg-red-500"></div>


================================================
FILE: tests/fixtures/esm/output.html
================================================
<div class="bg-red-500 sm:bg-hotpink"></div>


================================================
FILE: tests/fixtures/esm/prettier.config.js
================================================
module.exports = {};


================================================
FILE: tests/fixtures/esm/tailwind.config.mjs
================================================
export default {
  theme: {
    extend: {
      colors: {
        hotpink: "hotpink",
      },
    },
  },
};


================================================
FILE: tests/fixtures/esm-explicit/config.mjs
================================================
export default {
  theme: {
    extend: {
      colors: {
        hotpink: "hotpink",
      },
    },
  },
};


================================================
FILE: tests/fixtures/esm-explicit/index.html
================================================
<div class="sm:bg-hotpink bg-red-500"></div>


================================================
FILE: tests/fixtures/esm-explicit/output.html
================================================
<div class="bg-red-500 sm:bg-hotpink"></div>


================================================
FILE: tests/fixtures/esm-explicit/prettier.config.js
================================================
module.exports = {
  tailwindConfig: './config.mjs'
};


================================================
FILE: tests/fixtures/monorepo/.prettierrc
================================================
{
  "tailwindFunctions": ["tw"],
  "overrides": [
    {
      "files": "package-1/**",
      "options": {
        "tailwindStylesheet": "./package-1/app.css"
      }
    },
    {
      "files": "package-2/**",
      "options": {}
    }
  ]
}


================================================
FILE: tests/fixtures/monorepo/package-1/app.css
================================================
@import 'tailwindcss';

@theme {
  --color-tomato: tomato;
}


================================================
FILE: tests/fixtures/monorepo/package-1/index.jsx
================================================
const a = tw`sm:bg-tomato bg-red-500`;


================================================
FILE: tests/fixtures/monorepo/package-1/output.jsx
================================================
const a = tw`bg-red-500 sm:bg-tomato`;


================================================
FILE: tests/fixtures/monorepo/package-1/package.json
================================================
{
  "dependencies": {
    "tailwindcss": "^4.0.0"
  }
}


================================================
FILE: tests/fixtures/monorepo/package-2/index.jsx
================================================
const a = tw`sm:bg-tomato bg-red-500`;


================================================
FILE: tests/fixtures/monorepo/package-2/output.jsx
================================================
const a = tw`bg-red-500 sm:bg-tomato`;


================================================
FILE: tests/fixtures/monorepo/package-2/package.json
================================================
{
  "dependencies": {
    "tailwindcss": "^3.4.17"
  }
}


================================================
FILE: tests/fixtures/monorepo/package-2/tailwind.config.js
================================================
module.exports = { theme: { extend: { colors: { tomato: 'tomato' } } } }


================================================
FILE: tests/fixtures/monorepo/package.json
================================================
{}


================================================
FILE: tests/fixtures/no-local-version/app.css
================================================
@import "tailwindcss";

@theme {
  --color-tomato: tomato;
}


================================================
FILE: tests/fixtures/no-local-version/index.html
================================================
<div class="sm:bg-tomato bg-red-500"></div>


================================================
FILE: tests/fixtures/no-local-version/output.html
================================================
<div class="bg-red-500 sm:bg-tomato"></div>


================================================
FILE: tests/fixtures/no-local-version/package.json
================================================
{
  "prettier": {
    "tailwindStylesheet": "./app.css"
  }
}


================================================
FILE: tests/fixtures/no-prettier-config/index.html
================================================
<div class="sm:bg-tomato bg-red-500"></div>


================================================
FILE: tests/fixtures/no-prettier-config/output.html
================================================
<div class="bg-red-500 sm:bg-tomato"></div>


================================================
FILE: tests/fixtures/no-prettier-config/tailwind.config.js
================================================
module.exports = {
  theme: {
    extend: {
      colors: {
        tomato: 'tomato',
      },
    },
  },
}


================================================
FILE: tests/fixtures/no-stylesheet-given/index.html
================================================
<div class="sm:bg-red-500 bg-red-500"></div>


================================================
FILE: tests/fixtures/no-stylesheet-given/output.html
================================================
<div class="bg-red-500 sm:bg-red-500"></div>


================================================
FILE: tests/fixtures/package.json
================================================
{
  "type": "commonjs"
}


================================================
FILE: tests/fixtures/plugins/index.html
================================================
<div class="sm:bar foo uppercase"></div>


================================================
FILE: tests/fixtures/plugins/output.html
================================================
<div class="uppercase foo sm:bar"></div>


================================================
FILE: tests/fixtures/plugins/prettier.config.js
================================================
const prettier = require("prettier")

module.exports = {
  plugins: ['../../../dist/index.mjs'],
}


================================================
FILE: tests/fixtures/plugins/tailwind.config.js
================================================
const plugin = require("tailwindcss-v3/plugin");

module.exports = {
  plugins: [
    plugin(function ({ addUtilities }) {
      addUtilities({
        ".foo": { color: "red" },
        ".bar": { color: "blue" },
      });
    }),
  ],
};


================================================
FILE: tests/fixtures/ts/index.html
================================================
<div class="sm:bg-hotpink bg-red-500"></div>


================================================
FILE: tests/fixtures/ts/output.html
================================================
<div class="bg-red-500 sm:bg-hotpink"></div>


================================================
FILE: tests/fixtures/ts/prettier.config.js
================================================
module.exports = {};


================================================
FILE: tests/fixtures/ts/tailwind.config.ts
================================================
import type { Config } from "tailwindcss";

export default {
  content: ["index.html"],
  theme: {
    extend: {
      colors: {
        hotpink: "hotpink",
      },
    },
  },
} satisfies Config;


================================================
FILE: tests/fixtures/ts-explicit/config.ts
================================================
import type { Config } from "tailwindcss";

export default {
  content: ["index.html"],
  theme: {
    extend: {
      colors: {
        hotpink: "hotpink",
      },
    },
  },
} satisfies Config;


================================================
FILE: tests/fixtures/ts-explicit/index.html
================================================
<div class="sm:bg-hotpink bg-red-500"></div>


================================================
FILE: tests/fixtures/ts-explicit/output.html
================================================
<div class="bg-red-500 sm:bg-hotpink"></div>


================================================
FILE: tests/fixtures/ts-explicit/prettier.config.js
================================================
module.exports = {
  tailwindConfig: './config.ts'
};


================================================
FILE: tests/fixtures/v3-2/index.html
================================================
<div class="sm:bg-tomato bg-red-500"></div>


================================================
FILE: tests/fixtures/v3-2/output.html
================================================
<div class="bg-red-500 sm:bg-tomato"></div>


================================================
FILE: tests/fixtures/v3-2/package.json
================================================
{
  "dependencies": {
    "tailwindcss": "3.2.7"
  }
}


================================================
FILE: tests/fixtures/v3-2/prettier.config.js
================================================
module.exports = {};


================================================
FILE: tests/fixtures/v3-2/tailwind.config.js
================================================
module.exports = {
  theme: {
    extend: {
      colors: {
        tomato: 'tomato',
      },
    },
  },
}


================================================
FILE: tests/fixtures/v3-jiti-reexport/index.html
================================================
<div class="sm:p-0 p-0"></div>


================================================
FILE: tests/fixtures/v3-jiti-reexport/output.html
================================================
<div class="p-0 sm:p-0"></div>


================================================
FILE: tests/fixtures/v3-jiti-reexport/package.json
================================================
{
  "private": true,
  "dependencies": {
    "@repo/tailwind-config": "file:./packages/tailwind-config"
  }
}


================================================
FILE: tests/fixtures/v3-jiti-reexport/packages/tailwind-config/dist/index.mjs
================================================
import { createJiti } from 'jiti'

const jiti = createJiti(import.meta.url, {
  interopDefault: true,
})

const _module = await jiti.import(new URL('../src/index.ts', import.meta.url).href, {
  default: true,
})

export default _module?.default ?? _module


================================================
FILE: tests/fixtures/v3-jiti-reexport/packages/tailwind-config/package.json
================================================
{
  "name": "@repo/tailwind-config",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.mjs",
  "module": "./dist/index.mjs",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.mjs",
      "default": "./dist/index.mjs"
    }
  },
  "dependencies": {
    "jiti": "^2.6.1"
  }
}


================================================
FILE: tests/fixtures/v3-jiti-reexport/packages/tailwind-config/src/index.ts
================================================
export default {
  content: ['./index.html'],
}


================================================
FILE: tests/fixtures/v3-jiti-reexport/tailwind.config.mjs
================================================
export { default } from '@repo/tailwind-config'


================================================
FILE: tests/fixtures/v4/basic/app.css
================================================
@import 'tailwindcss';

@theme {
  --color-tomato: tomato;
}


================================================
FILE: tests/fixtures/v4/basic/index.html
================================================
<div class="sm:bg-tomato bg-red-500"></div>


================================================
FILE: tests/fixtures/v4/basic/output.html
================================================
<div class="bg-red-500 sm:bg-tomato"></div>


================================================
FILE: tests/fixtures/v4/basic/package.json
================================================
{
  "dependencies": {
    "tailwindcss": "^4.0.0"
  },
  "prettier": {
    "tailwindStylesheet": "./app.css"
  }
}


================================================
FILE: tests/fixtures/v4/css-loading-js/app.css
================================================
@import 'tailwindcss';

/* Load ESM versions */
@config './esm/my-config.mjs';
@plugin './esm/my-plugin.mjs';

/* Load Common JS versions */
@config './cjs/my-config.cjs';
@plugin './cjs/my-plugin.cjs';

/* Load TypeScript versions */
@config './ts/my-config.ts';
@plugin './ts/my-plugin.ts';

/* Attempt to load files that do not exist */
@config './missing-confg.mjs';
@plugin './missing-plugin.mjs';

@theme {
  --color-tomato: tomato;
}


================================================
FILE: tests/fixtures/v4/css-loading-js/cjs/my-config.cjs
================================================
module.exports = {
  theme: {
    extend: {
      colors: {
        'cjs-from-config': 'black',
      },
    },
  },
}


================================================
FILE: tests/fixtures/v4/css-loading-js/cjs/my-plugin.cjs
================================================
const plugin = require('tailwindcss/plugin')

module.exports = plugin(
  ({ addUtilities }) => {
    addUtilities({
      '.utility-cjs-from-plugin': {
        color: 'black'
      },
      '.utility-cjs-from-plugin-2': {
        width: '100%',
        height: '100%',
      },
    })
  },
  {
    theme: {
      extend: {
        colors: {
          'cjs-from-plugin': 'black',
        },
      },
    },
  },
)


================================================
FILE: tests/fixtures/v4/css-loading-js/esm/my-config.mjs
================================================
export default {
  theme: {
    extend: {
      colors: {
        'esm-from-config': 'black',
      },
    },
  },
}


================================================
FILE: tests/fixtures/v4/css-loading-js/esm/my-plugin.mjs
================================================
import plugin from 'tailwindcss/plugin'

export default plugin(
  ({ addUtilities }) => {
    addUtilities({
      '.utility-esm-from-plugin': {
        color: 'black'
      },
      '.utility-esm-from-plugin-2': {
        width: '100%',
        height: '100%',
      },
    })
  },
  {
    theme: {
      extend: {
        colors: {
          'esm-from-plugin': 'black',
        },
      },
    },
  },
)


================================================
FILE: tests/fixtures/v4/css-loading-js/index.html
================================================
<div
  class="sm:bg-tomato sm:utility-cjs-from-plugin sm:utility-cjs-from-plugin-2 sm:utility-esm-from-plugin sm:utility-esm-from-plugin-2 sm:utility-ts-from-plugin sm:utility-ts-from-plugin-2 sm:bg-cjs-from-config sm:bg-cjs-from-plugin sm:bg-esm-from-config sm:bg-esm-from-plugin sm:bg-ts-from-config sm:bg-ts-from-plugin bg-red-500 utility-cjs-from-plugin utility-esm-from-plugin utility-ts-from-plugin"
></div>


================================================
FILE: tests/fixtures/v4/css-loading-js/output.html
================================================
<div
  class="bg-red-500 utility-cjs-from-plugin utility-esm-from-plugin utility-ts-from-plugin sm:utility-cjs-from-plugin-2 sm:utility-esm-from-plugin-2 sm:utility-ts-from-plugin-2 sm:bg-cjs-from-config sm:bg-cjs-from-plugin sm:bg-esm-from-config sm:bg-esm-from-plugin sm:bg-tomato sm:bg-ts-from-config sm:bg-ts-from-plugin sm:utility-cjs-from-plugin sm:utility-esm-from-plugin sm:utility-ts-from-plugin"
></div>


================================================
FILE: tests/fixtures/v4/css-loading-js/package.json
================================================
{
  "dependencies": {
    "tailwindcss": "^4.0.0"
  },
  "prettier": {
    "tailwindStylesheet": "./app.css"
  }
}


================================================
FILE: tests/fixtures/v4/css-loading-js/ts/my-config.ts
================================================
import type { Config } from 'tailwindcss'

export default {
  theme: {
    extend: {
      colors: {
        'ts-from-config': 'black',
      },
    },
  },
} satisfies Config


================================================
FILE: tests/fixtures/v4/css-loading-js/ts/my-plugin.ts
================================================
import plugin from 'tailwindcss/plugin'

export default plugin(
  ({ addUtilities }) => {
    addUtilities({
      '.utility-ts-from-plugin': {
        color: 'black'
      },
      '.utility-ts-from-plugin-2': {
        width: '100%',
        height: '100%',
      },
    })
  },
  {
    theme: {
      extend: {
        colors: {
          'ts-from-plugin': 'black',
        },
      },
    },
  },
)


================================================
FILE: tests/fixtures/v4/subpath-imports/app.css
================================================
@import 'tailwindcss';
@import '#theme';


================================================
FILE: tests/fixtures/v4/subpath-imports/index.html
================================================
<div class="sm:bg-tomato bg-red-500"></div>


================================================
FILE: tests/fixtures/v4/subpath-imports/output.html
================================================
<div class="bg-red-500 sm:bg-tomato"></div>


================================================
FILE: tests/fixtures/v4/subpath-imports/package.json
================================================
{
  "dependencies": {
    "tailwindcss": "^4.0.0"
  },
  "prettier": {
    "tailwindStylesheet": "./app.css"
  },
  "imports": {
    "#theme": "./theme.css"
  }
}


================================================
FILE: tests/fixtures/v4/subpath-imports/theme.css
================================================
@theme {
  --color-tomato: tomato;
}


================================================
FILE: tests/fixtures.test.ts
================================================
import { exec } from 'node:child_process'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { afterAll, beforeAll, describe, test } from 'vitest'
import { format, pluginPath } from './utils'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const execAsync = promisify(exec)

let fixtures = [
  {
    name: 'no prettier config',
    dir: 'no-prettier-config',
    ext: 'html',
  },
  {
    name: 'no local install of Tailwind CSS (uses v4)',
    dir: 'no-local-version',
    ext: 'html',
  },
  {
    name: 'no stylesheet given (uses v4)',
    dir: 'no-stylesheet-given',
    ext: 'html',
  },
  {
    name: 'inferred config path',
    dir: 'basic',
    ext: 'html',
  },
  {
    name: 'inferred config path (.cjs)',
    dir: 'cjs',
    ext: 'html',
  },
  {
    name: 'using esm config',
    dir: 'esm',
    ext: 'html',
  },
  {
    name: 'using esm config (explicit path)',
    dir: 'esm-explicit',
    ext: 'html',
  },
  {
    name: 'using ts config',
    dir: 'ts',
    ext: 'html',
  },
  {
    name: 'using ts config (explicit path)',
    dir: 'ts-explicit',
    ext: 'html',
  },
  {
    name: 'using v3.2.7',
    dir: 'v3-2',
    ext: 'html',
  },
  {
    name: 'v3: re-exported config with jiti',
    dir: 'v3-jiti-reexport',
    ext: 'html',
  },
  {
    name: 'plugins',
    dir: 'plugins',
    ext: 'html',
  },
  {
    name: 'customizations: js/jsx',
    dir: 'custom-jsx',
    ext: 'jsx',
  },

  {
    name: 'v4: basic formatting',
    dir: 'v4/basic',
    ext: 'html',
  },
  {
    name: 'v4: configs and plugins',
    dir: 'v4/css-loading-js',
    ext: 'html',
  },
  {
    name: 'v4: subpath imports',
    dir: 'v4/subpath-imports',
    ext: 'html',
  },
  {
    name: 'custom npm package name: v3',
    dir: 'custom-pkg-name-v3',
    ext: 'html',
  },
  {
    name: 'custom npm package name: v4',
    dir: 'custom-pkg-name-v4',
    ext: 'html',
  },

  {
    name: 'monorepo / v4',
    dir: 'monorepo/package-1',
    ext: 'jsx',
  },
  {
    name: 'monorepo / v3',
    dir: 'monorepo/package-2',
    ext: 'jsx',
  },
]

let configs = [
  {
    from: __dirname + '/../.prettierignore',
    to: __dirname + '/../.prettierignore.testing',
  },
  {
    from: __dirname + '/../prettier.config.js',
    to: __dirname + '/../prettier.config.js.testing',
  },
]

test.concurrent('explicit config path', async ({ expect }) => {
  expect(
    await format('<div class="sm:bg-tomato bg-red-500"></div>', {
      tailwindConfig: path.resolve(__dirname, 'fixtures/basic/tailwind.config.js'),
    }),
  ).toEqual('<div class="bg-red-500 sm:bg-tomato"></div>')
})

describe('fixtures', () => {
  // Temporarily move config files out of the way so they don't interfere with the tests
  beforeAll(async () => {
    await Promise.all(configs.map(({ from, to }) => fs.rename(from, to)))
  })

  afterAll(async () => {
    await Promise.all(configs.map(({ from, to }) => fs.rename(to, from)))
  })

  let binPath = path.resolve(__dirname, '../node_modules/.bin/prettier')

  for (const { ext, dir, name } of fixtures) {
    let fixturePath = path.resolve(__dirname, `fixtures/${dir}`)
    let inputPath = path.resolve(fixturePath, `index.${ext}`)
    let outputPath = path.resolve(fixturePath, `output.${ext}`)
    let cmd = `${binPath} ${inputPath} --plugin ${pluginPath}`

    test.concurrent(name, async ({ expect }) => {
      let results = await execAsync(cmd)
      let formatted = results.stdout.replace(/\r\n/g, '\n')
      let expected = await fs.readFile(outputPath, 'utf-8').then((c) => c.replace(/\r\n/g, '\n'))

      expect(formatted.trim()).toEqual(expected.trim())
    })
  }
})


================================================
FILE: tests/format.test.ts
================================================
import { describe, test } from 'vitest'
import { tests } from './tests.js'
import { format } from './utils.js'

describe('parsers', async () => {
  for (let parser in tests) {
    test(parser, async ({ expect }) => {
      for (let [input, expected, options] of tests[parser]) {
        expect(await format(input, { ...options, parser })).toEqual(expected)
      }
    })
  }
})

describe('other', () => {
  test('non-tailwind classes are sorted to the front', async ({ expect }) => {
    let result = await format('<div class="sm:lowercase uppercase potato text-sm"></div>')

    expect(result).toEqual('<div class="potato text-sm uppercase sm:lowercase"></div>')
  })

  test('parasite utilities (v3)', async ({ expect }) => {
    let result = await format('<div class="group peer unknown-class p-0 container"></div>', {
      tailwindPackageName: 'tailwindcss-v3',
    })

    expect(result).toEqual('<div class="unknown-class group peer container p-0"></div>')
  })

  test('parasite utilities (v4)', async ({ expect }) => {
    let result = await format('<div class="group peer unknown-class p-0 container"></div>', {
      tailwindPackageName: 'tailwindcss-v4',
    })

    expect(result).toEqual('<div class="group peer unknown-class container p-0"></div>')
  })

  test('parasite utilities (no install == v4)', async ({ expect }) => {
    let result = await format('<div class="group peer unknown-class p-0 container"></div>')

    expect(result).toEqual('<div class="group peer unknown-class container p-0"></div>')
  })
})

describe('whitespace', () => {
  test('class lists containing interpolation are ignored', async ({ expect }) => {
    let result = await format('<div class="{{ this is ignored }}"></div>')

    expect(result).toEqual('<div class="{{ this is ignored }}"></div>')
  })

  test('whitespace can be preserved around classes', async ({ expect }) => {
    let result = await format(`;<div className={' underline text-red-500  flex '}></div>`, {
      parser: 'babel',
      tailwindPreserveWhitespace: true,
    })

    expect(result).toEqual(`;<div className={' flex text-red-500  underline '}></div>`)
  })

  test('whitespace can be collapsed around classes', async ({ expect }) => {
    let result = await format('<div class=" underline text-red-500  flex "></div>')

    expect(result).toEqual('<div class="flex text-red-500 underline"></div>')
  })

  test('whitespace is collapsed but not trimmed when ignored', async ({ expect }) => {
    let result = await format(';<div className={`underline text-red-500 ${foo}-bar flex`}></div>', {
      parser: 'babel',
    })

    expect(result).toEqual(';<div className={`text-red-500 underline ${foo}-bar flex`}></div>')
  })

  test('whitespace is not trimmed inside concat expressions', async ({ expect }) => {
    let result = await format(";<div className={a + ' p-4 ' + b}></div>", {
      parser: 'babel',
    })

    expect(result).toEqual(";<div className={a + ' p-4 ' + b}></div>")
  })

  test('whitespace is not trimmed inside concat expressions (angular)', async ({ expect }) => {
    let result = await format(
      `<ul [class]="'pagination' + (size ? ' pagination-' + size : '')"></ul>`,
      {
        parser: 'angular',
      },
    )

    expect(result).toEqual(`<ul [class]="'pagination' + (size ? ' pagination-' + size : '')"></ul>`)
  })

  test('whitespace is not trimmed inside adjacent-before/after template expressions', async ({
    expect,
  }) => {
    let result = await format(
      ";<div className={`header${isExtendable ? ' header-extendable' : ''}`} />",
      {
        parser: 'babel',
      },
    )

    expect(result).toEqual(
      ";<div className={`header${isExtendable ? ' header-extendable' : ''}`} />",
    )
  })

  test('whitespace is not trimmed before template literal quasis without leading space', async ({
    expect,
  }) => {
    let result = await format(";<div className={`${foo ? 'sm:p-0 p-0 ' : ''}header`}></div>", {
      parser: 'babel',
    })

    expect(result).toEqual(";<div className={`${foo ? 'p-0 sm:p-0 ' : ''}header`}></div>")
  })

  test('duplicate classes are dropped', async ({ expect }) => {
    let result = await format('<div class="underline line-through underline flex"></div>')

    expect(result).toEqual('<div class="flex line-through underline"></div>')
  })
})

describe('errors', () => {
  test('when the given JS config does not exist', async ({ expect }) => {
    let result = format('<div></div>', {
      tailwindConfig: 'i-do-not-exist.js',
      tailwindPackageName: 'tailwindcss-v3',
    })

    await expect(result).rejects.toThrowError(/Cannot find module/)
  })

  test('when the given stylesheet does not exist', async ({ expect }) => {
    let result = format('<div></div>', {
      tailwindStylesheet: 'i-do-not-exist.css',
      tailwindPackageName: 'tailwindcss-v4',
    })

    await expect(result).rejects.toThrowError(/no such file or directory/)
  })

  test('when using a stylesheet and the local install is not v4', async ({ expect }) => {
    let result = format('<div></div>', {
      tailwindStylesheet: 'i-do-not-exist.css',
      tailwindPackageName: 'tailwindcss-v3',
    })

    await expect(result).rejects.toThrowError(/no such file or directory/)
  })
})

describe('regex matching', () => {
  test('attribute name exact matches', async ({ expect }) => {
    let result = await format('<div myClass="sm:p-0 p-0"></div>', {
      tailwindAttributes: ['myClass'],
    })

    expect(result).toEqual('<div myClass="p-0 sm:p-0"></div>')
  })

  test('function name exact matches', async ({ expect }) => {
    let result = await format('let classList = tw`sm:p-0 p-0`', {
      parser: 'babel',
      tailwindFunctions: ['tw'],
    })

    expect(result).toEqual('let classList = tw`p-0 sm:p-0`')
  })

  test('attribute name regex matches', async ({ expect }) => {
    let result = await format(
      `<div data-class="sm:p-0 p-0" data-classes="sm:p-0 p-0" data-style="sm:p-0 p-0"></div>`,
      {
        tailwindAttributes: ['/data-.*/'],
      },
    )

    expect(result).toEqual(
      `<div data-class="p-0 sm:p-0" data-classes="p-0 sm:p-0" data-style="p-0 sm:p-0"></div>`,
    )
  })

  test('function name regex matches', async ({ expect }) => {
    let result = await format(
      'let classList1 = twClasses`sm:p-0 p-0`\nlet classList2 = myClasses`sm:p-0 p-0`',
      {
        parser: 'babel',
        tailwindFunctions: ['/.*Classes/'],
      },
    )

    expect(result).toEqual(
      'let classList1 = twClasses`p-0 sm:p-0`\nlet classList2 = myClasses`p-0 sm:p-0`',
    )
  })

  test('regex flags are supported', async ({ expect }) => {
    let result = await format(`;<div MyClass="sm:p-0 p-0" data-other={MyFn('sm:p-0 p-0')} />`, {
      parser: 'babel',
      tailwindAttributes: ['/myclass/i'],
      tailwindFunctions: ['/myfn/i'],
    })

    expect(result).toEqual(`;<div MyClass="p-0 sm:p-0" data-other={MyFn('p-0 sm:p-0')} />`)
  })

  test('anchors are supported', async ({ expect }) => {
    let result = await format(
      `;<div classList="sm:p-0 p-0" styleList="sm:p-0 p-0" otherList="sm:p-0 p-0" data-other-1={styleList('sm:p-0 p-0')} data-other-2={classList('sm:p-0 p-0')} />`,
      {
        parser: 'babel',
        tailwindAttributes: ['/.*List$/'],
        tailwindFunctions: ['/.*List$/'],
      },
    )

    expect(result).toEqual(
      `;<div classList="p-0 sm:p-0" styleList="p-0 sm:p-0" otherList="p-0 sm:p-0" data-other-1={styleList('p-0 sm:p-0')} data-other-2={classList('p-0 sm:p-0')} />`,
    )
  })

  test('works with Vue dynamic bindings', async ({ expect }) => {
    let result = await format('<div :data-classes="`sm:p-0 p-0`"></div>', {
      parser: 'vue',
      tailwindAttributes: ['/data-.*/'],
    })

    expect(result).toEqual('<div :data-classes="`p-0 sm:p-0`"></div>')
  })

  test('works with Angular property bindings', async ({ expect }) => {
    let result = await format('<div [dataClasses]="`sm:p-0 p-0`"></div>', {
      parser: 'angular',
      tailwindAttributes: ['/data.*/i'],
    })

    expect(result).toEqual('<div [dataClasses]="`p-0 sm:p-0`"></div>')
  })

  test('invalid regex patterns do nothing', async ({ expect }) => {
    let result = await format('<div data-test="sm:p-0 p-0"></div>', {
      tailwindAttributes: ['/data-[/'],
    })

    expect(result).toEqual('<div data-test="sm:p-0 p-0"></div>')
  })

  test('dynamic attributes are not matched as static attributes', async ({ expect }) => {
    let result = await format(`<div :custom-class="['sm:p-0 flex underline p-0']"></div>`, {
      parser: 'vue',
      tailwindAttributes: ['/.*-class/'],
    })

    expect(result).toEqual(`<div :custom-class="['flex p-0 underline sm:p-0']"></div>`)
  })

  test('dynamic attributes are not matched as static attributes (2)', async ({ expect }) => {
    let result = await format(`<div :custom-class="['sm:p-0 flex underline p-0']"></div>`, {
      parser: 'vue',
      tailwindAttributes: ['/:custom-class/'],
    })

    expect(result).toEqual(`<div :custom-class="['sm:p-0 flex underline p-0']"></div>`)
  })

  // These tests pass but that is a side-effect of the implementation
  // If these change in the future to no longer pass that is a good thing
  describe('dynamic attribute matching quirks', () => {
    test('Vue', async ({ expect }) => {
      let result = await format('<div ::data-classes="`sm:p-0 p-0`"></div>', {
        parser: 'vue',
        tailwindAttributes: ['/:data-.*/'],
      })

      expect(result).toEqual('<div ::data-classes="`p-0 sm:p-0`"></div>')
    })

    test('Angular', async ({ expect }) => {
      let result = await format('<div [[dataClasses]]="`sm:p-0 p-0`"></div>', {
        parser: 'angular',
        tailwindAttributes: ['/\\[data.*\\]/i'],
      })

      expect(result).toEqual('<div [[dataClasses]]="`p-0 sm:p-0`"></div>')
    })
  })
})


================================================
FILE: tests/plugins.test.ts
================================================
import { createRequire } from 'node:module'
import dedent from 'dedent'
import { test } from 'vitest'
import { javascript } from './tests.js'
import type { TestEntry } from './utils.js'
import { format, no, pluginPath, t, yes } from './utils.js'

const require = createRequire(import.meta.url)

interface PluginTest {
  plugins: string[]
  options?: Record<string, any>
  tests: Record<string, TestEntry[]>
}

let tests: PluginTest[] = [
  {
    plugins: ['@trivago/prettier-plugin-sort-imports'],
    options: {
      importOrder: ['^@one/(.*)$', '^@two/(.*)$', '^[./]'],
      importOrderSortSpecifiers: true,
    },
    tests: {
      babel: [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import '@one/file'\nimport '@two/file'\nimport './three'`,
        ],
      ],
      typescript: [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import '@one/file'\nimport '@two/file'\nimport './three'`,
        ],
      ],

      // This plugin does not support babel-ts
      'babel-ts': [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
        ],
      ],
    },
  },
  {
    plugins: ['@ianvs/prettier-plugin-sort-imports'],
    options: {
      importOrder: ['^@tailwindcss/(.*)$', '^@babel/(.*)$', '^[./]'],
      importOrderSortSpecifiers: true,
    },
    tests: {
      babel: [
        [
          `import './i-haz-side-effects'\nimport i3 from './three'\nimport i2 from '@two/file'\nimport i1 from '@one/file'`,
          `import './i-haz-side-effects'\nimport i1 from '@one/file'\nimport i2 from '@two/file'\nimport i3 from './three'`,
        ],
      ],
      typescript: [
        [
          `import './i-haz-side-effects'\nimport i3 from './three'\nimport i2 from '@two/file'\nimport i1 from '@one/file'`,
          `import './i-haz-side-effects'\nimport i1 from '@one/file'\nimport i2 from '@two/file'\nimport i3 from './three'`,
        ],
      ],

      // This plugin does not support babel-ts
      'babel-ts': [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
        ],
      ],
    },
  },
  {
    plugins: ['prettier-plugin-sort-imports'],
    options: {
      sortingMethod: 'alphabetical',
    },
    tests: {
      babel: [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import './three'\nimport '@one/file'\nimport '@two/file'`,
        ],
      ],
      typescript: [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import './three'\nimport '@one/file'\nimport '@two/file'`,
        ],
      ],

      // This plugin does not support babel-ts
      'babel-ts': [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
        ],
      ],
    },
  },
  {
    plugins: ['prettier-plugin-multiline-arrays'],
    tests: {
      babel: [[`const array = [\n'one']`, `const array = [\n  'one',\n]`]],
      typescript: [[`const array = [\n'one']`, `const array = [\n  'one',\n]`]],
      'babel-ts': [[`const array = [\n'one']`, `const array = [\n  'one',\n]`]],
    },
  },
  {
    plugins: ['prettier-plugin-organize-imports'],
    options: {},
    tests: {
      babel: [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import '@one/file'\nimport '@two/file'\nimport './three'`,
        ],
      ],
      typescript: [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import '@one/file'\nimport '@two/file'\nimport './three'`,
        ],
      ],
      'babel-ts': [
        [
          `import './three'\nimport '@two/file'\nimport '@one/file'`,
          `import '@one/file'\nimport '@two/file'\nimport './three'`,
        ],
      ],
    },
  },
  {
    plugins: ['@zackad/prettier-plugin-twig'],
    options: {
      twigAlwaysBreakObjects: false,
      tailwindFunctions: ['addClass', 'tw'],
    },
    tests: {
      twig: [
        [
          `<section class="{{ {base:css.prices}|classes }}"></section>`,
          `<section class="{{ { base: css.prices }|classes }}"></section>`,
        ],
        t`<section class="${yes}"></section>`,

        t`<section class="${yes} text-{{ i }}"></section>`,
        t`<section class="${yes} {{ i }}-text"></section>`,
        t`<section class="text-{{ i }} ${yes}"></section>`,
        t`<section class="{{ i }}-text ${yes}"></section>`,

        // text-center is used because it's placed between p-0 and sm:p-0
        t`<section class="${yes} text-center{{ i }}"></section>`,
        t`<section class="${yes} {{ i }}text-center"></section>`,
        t`<section class="text-center{{ i }} ${yes}"></section>`,
        t`<section class="{{ i }}text-center ${yes}"></section>`,

        [
          `<div class=" sm:flex   underline  block"></div>`,
          `<div class="block underline sm:flex"></div>`,
        ],
        [
          `<div class="{{ ' flex ' + ' underline ' + ' block ' }}"></div>`,
          `<div class="{{ 'flex ' + ' underline' + ' block' }}"></div>`,
        ],

        // Function call tests
        t`<div {{ tw('${yes}') }}></div>`,
        t`<div {{ attributes.addClass('${yes}') }}></div>`,

        t`{{ tw('${yes}') }}`,
        t`{{ attributes.addClass('${yes}') }}`,

        t`{{ tw('${yes}').tw('${yes}').tw('${yes}') }}`,
        t`{{ attributes.addClass('${yes}').addClass('${yes}').addClass('${yes}') }}`,

        t`{% set className = '${no}' %} {{ attributes.addClass(className) }}`,
        [
          `{{ attributes.addClass("sm:p-0 " ~ variant ~ " p-0") }}`,
          `{{ attributes.addClass('sm:p-0 ' ~ variant ~ ' p-0') }}`,
        ],
      ],
    },
  },
  {
    plugins: ['@prettier/plugin-hermes'],
    tests: {
      hermes: javascript,
    },
  },
  {
    plugins: ['@prettier/plugin-oxc'],
    tests: {
      oxc: javascript,
      'oxc-ts': javascript,
    },
  },
  {
    plugins: ['@prettier/plugin-pug'],
    tests: {
      pug: [
        [
          `a(class='md:p-4 sm:p-0 p-4 bg-blue-600' href='//example.com') Example`,
          `a.bg-blue-600.p-4(class='sm:p-0 md:p-4', href='//example.com') Example`,
        ],
        [
          `a.p-4.bg-blue-600(class='sm:p-0 md:p-4', href='//example.com') Example`,
          `a.bg-blue-600.p-4(class='sm:p-0 md:p-4', href='//example.com') Example`,
        ],

        [
          `a.p-4.bg-blue-600(class=' sm:p-0     md:p-4 ', href='//example.com') Example`,
          `a.bg-blue-600.p-4(class='sm:p-0 md:p-4', href='//example.com') Example`,
        ],

        // These two tests show how our sorting the two class lists separately is suboptimal
        // Two consecutive saves will result in different output
        // Where the second save is the most correct
        [
          `a.p-4(class='bg-blue-600 sm:p-0 md:p-4', href='//example.com') Example`,
          `a.p-4.bg-blue-600(class='sm:p-0 md:p-4', href='//example.com') Example`,
        ],
        [
          `a.p-4.bg-blue-600(class='sm:p-0 md:p-4', href='//example.com') Example`,
          `a.bg-blue-600.p-4(class='sm:p-0 md:p-4', href='//example.com') Example`,
        ],
      ],
    },
  },
  {
    plugins: ['prettier-plugin-jsdoc'],
    tests: {
      babel: [
        [
          `/**\n             * @param {  string   }    param0 description\n             */\n            export default function Foo(param0) { return <div className="sm:p-0 p-4"></div> }`,
          `/** @param {string} param0 Description */\nexport default function Foo(param0) {\n  return <div className="p-4 sm:p-0"></div>\n}`,
        ],
      ],
    },
  },
  {
    plugins: ['prettier-plugin-css-order'],
    tests: {
      css: [
        [
          `.foo {\n  color: red;\n  background-color: blue;\n  @apply sm:p-0 p-4 bg-blue-600;\n}`,
          `.foo {\n  background-color: blue;\n  color: red;\n  @apply bg-blue-600 p-4 sm:p-0;\n}`,
        ],
      ],
    },
  },
  {
    plugins: ['prettier-plugin-organize-attributes'],
    tests: {
      html: [
        [
          `<a href="https://www.example.com" class="sm:p-0 p-4">Example</a>`,
          `<a class="p-4 sm:p-0" href="https://www.example.com">Example</a>`,
        ],
      ],
    },
  },
  {
    plugins: ['@shopify/prettier-plugin-liquid'],
    tests: {
      'liquid-html': [
        t`<a class='${yes}' href='https://www.example.com'>Example</a>`,
        t`{% if state == true %}\n  <a class='{{ "${yes}" | escape }}' href='https://www.example.com'>Example</a>\n{% endif %}`,
        t`{%- capture class_ordering -%}<div class="${yes}"></div>{%- endcapture -%}`,
        t`{%- capture class_ordering -%}<div class="foo1 ${yes}"></div><div class="foo2 ${yes}"></div>{%- endcapture -%}`,
        t`{%- capture class_ordering -%}<div class="foo1 ${yes}"><div class="foo2 ${yes}"></div></div>{%- endcapture -%}`,
        t`<p class='${yes} {{ some.prop | prepend: 'is-' }} '></p>`,
        t`<div class='${yes} {% render 'some-snippet', settings: section.settings %}'></div>`,
        t`<div class='${yes} {{ foo }}'></div>`,
        t`<div class='${yes} {% render 'foo' %}'></div>`,
        t`<div class='${yes} {% render 'foo', bar: true %}'></div>`,
        t`<div class='${yes} {% include 'foo' %}'></div>`,
        t`<div class='${yes} {% include 'foo', bar: true %}'></div>`,
        t`<div class='${yes} foo--{{ id }}'></div>`,
        t`<div class='${yes} {{ id }}'></div>`,

        // Whitespace removal is disabled for Liquid
        // due to the way Liquid prints the AST
        // (the length of the output MUST NOT change)
        [
          `<div class=' sm:flex   underline  block'></div>`,
          `<div class=' block   underline  sm:flex'></div>`,
        ],
        [
          `<div class='{{ ' flex ' + ' underline ' + ' block ' }}'></div>`,
          `<div class='{{ ' flex ' + ' underline ' + ' block ' }}'></div>`,
        ],
      ],
    },
  },
  {
    plugins: ['prettier-plugin-marko'],
    tests: {
      marko: [
        t`<div class='${yes}'/>`,
        t`<!-- <div class='${no}'/> -->`,
        t`<div not-class='${no}'/>`,
        t`<div class/>`,
        t`<div class=''/>`,
        t`<div>
  <h1 class='${yes}'/>
</div>`,
        t`style {
  h1 {
    @apply ${yes};
  }
}`,
        t`<div class=[
  '${yes}',
  'w-full',
  someVariable,
  {
    a: true,
  },
  null,
  '${yes}',
]/>`,
        t`<div class=['${yes}', 'underline', someVariable]/>`,

        [`<div class=' sm:flex   underline  block'/>`, `<div class='block underline sm:flex'/>`],

        // TODO: An improvement to the plugin would be to remove the whitespace
        // in this scenario:
        [
          `<div class=[' flex ' + ' underline ' + ' block ']/>`,
          `<div class=[' flex ' + ' underline ' + ' block ']/>`,
        ],
      ],
    },
  },
  {
    plugins: ['prettier-plugin-astro'],
    options: {
      tailwindAttributes: ['/(styles|classes)/'],
    },
    tests: {
      astro: [
        // ...html, // TODO:
        [
          '<div styles="sm:p-0 p-0" classes="sm:p-0 p-0" other="sm:p-0 p-0"></div>',
          '<div styles="p-0 sm:p-0" classes="p-0 sm:p-0" other="sm:p-0 p-0"></div>',
        ],

        [
          `{<div class="p-20 bg-red-100 w-full"></div>}`,
          `{(<div class="w-full bg-red-100 p-20" />)}`,
        ],
        [
          `<style>
  h1 {
    @apply bg-fuchsia-50 p-20 w-full;
}
</style>`,
          `<style>
  h1 {
    @apply w-full bg-fuchsia-50 p-20;
  }
</style>`,
        ],
        t`---
import Layout from '../layouts/Layout.astro'
import Custom from '../components/Custom.astro'
---

<Layout>
  <main class="${yes}"></main>
  <my-element class="${yes}"></my-element>
  <Custom class="${yes}" />
</Layout>`,
        t`<div>
  <span class:list={['${yes}', { '${yes}': '${yes}' }, new Set(['${yes}'])]}></span>
</div>`,
        t`<div>
  <span class:list={[\`${yes}\`, \`\${'${yes}'}\`, \`\${\`${yes}\`}\`, \`\${\`\${'${yes}'}\`}\`]}></span>
</div>`,
        t`<MyReactComponent className="${yes}" />`,
        t`<MyReactComponent className={'${yes}'} />`,

        [
          `<div class=" sm:flex   underline  block"></div>`,
          `<div class="block underline sm:flex"></div>`,
        ],
        [
          `<div class:list={[' flex ' + ' underline ' + ' block ']}></div>`,
          `<div class:list={['flex ' + ' underline' + ' block']}></div>`,
        ],
      ],
    },
  },
  {
    plugins: ['prettier-plugin-svelte'],
    tests: {
      svelte: [
        t`<div class="${yes}" />`,
        t`<div class />`,
        t`<div class="" />`,
        t`<div class="${yes} {someVar}" />`,
        t`<div class="{someVar} ${yes}" />`,
        t`<div class="${yes} {someVar} ${yes}" />`,
        t`<div class={'${yes}'} />`,
        t`<div class={'${yes}' + '${yes}'} />`,
        t`<div class={\`${yes}\`} />`,
        t`<div class={\`${yes} \${'${yes}' + \`${yes}\`} ${yes}\`} />`,
        t`<div class={\`${no}\${someVar}${no}\`} />`,
        t`<div class="${yes} {\`${yes}\`}" />`,
        t`<div let:class={clazz} class="${yes} {clazz}" />`,
        t`{#if something} <div class="${yes}" /> {:else} <div class="${yes}" /> {/if}`,
        [
          `<div class="sm:block uppercase flex{someVar}" />`,
          `<div class="uppercase sm:block flex{someVar}" />`,
        ],
        [
          `<div class="{someVar}sm:block md:inline flex" />`,
     
Download .txt
gitextract_s76hpf38/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.md
│   │   └── config.yml
│   └── workflows/
│       ├── ci.yml
│       ├── prepare-release.yml
│       ├── release-insiders.yml
│       └── release.yml
├── .gitignore
├── .oxlintrc.json
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── knip.json
├── package.json
├── prettier.config.js
├── scripts/
│   ├── copy-licenses.js
│   ├── install-fixture-deps.js
│   ├── release-channel.js
│   └── release-notes.js
├── src/
│   ├── config.ts
│   ├── console.ts
│   ├── create-plugin.ts
│   ├── expiring-map.ts
│   ├── index.ts
│   ├── internal.d.ts
│   ├── options.ts
│   ├── resolve.ts
│   ├── sorter.ts
│   ├── sorting.ts
│   ├── transform.ts
│   ├── types.ts
│   ├── utils.bench.ts
│   ├── utils.test.ts
│   ├── utils.ts
│   └── versions/
│       ├── assets.ts
│       ├── v3.ts
│       └── v4.ts
├── tests/
│   ├── fixtures/
│   │   ├── basic/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── cjs/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.cjs
│   │   ├── custom-jsx/
│   │   │   ├── index.jsx
│   │   │   ├── output.jsx
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── custom-pkg-name-v3/
│   │   │   ├── config.js
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── package.json
│   │   ├── custom-pkg-name-v4/
│   │   │   ├── app.css
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── package.json
│   │   ├── custom-vue/
│   │   │   ├── index.vue
│   │   │   ├── output.vue
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── esm/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.mjs
│   │   ├── esm-explicit/
│   │   │   ├── config.mjs
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── prettier.config.js
│   │   ├── monorepo/
│   │   │   ├── .prettierrc
│   │   │   ├── package-1/
│   │   │   │   ├── app.css
│   │   │   │   ├── index.jsx
│   │   │   │   ├── output.jsx
│   │   │   │   └── package.json
│   │   │   ├── package-2/
│   │   │   │   ├── index.jsx
│   │   │   │   ├── output.jsx
│   │   │   │   ├── package.json
│   │   │   │   └── tailwind.config.js
│   │   │   └── package.json
│   │   ├── no-local-version/
│   │   │   ├── app.css
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── package.json
│   │   ├── no-prettier-config/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── tailwind.config.js
│   │   ├── no-stylesheet-given/
│   │   │   ├── index.html
│   │   │   └── output.html
│   │   ├── package.json
│   │   ├── plugins/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── ts/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.ts
│   │   ├── ts-explicit/
│   │   │   ├── config.ts
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   └── prettier.config.js
│   │   ├── v3-2/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── package.json
│   │   │   ├── prettier.config.js
│   │   │   └── tailwind.config.js
│   │   ├── v3-jiti-reexport/
│   │   │   ├── index.html
│   │   │   ├── output.html
│   │   │   ├── package.json
│   │   │   ├── packages/
│   │   │   │   └── tailwind-config/
│   │   │   │       ├── dist/
│   │   │   │       │   └── index.mjs
│   │   │   │       ├── package.json
│   │   │   │       └── src/
│   │   │   │           └── index.ts
│   │   │   └── tailwind.config.mjs
│   │   └── v4/
│   │       ├── basic/
│   │       │   ├── app.css
│   │       │   ├── index.html
│   │       │   ├── output.html
│   │       │   └── package.json
│   │       ├── css-loading-js/
│   │       │   ├── app.css
│   │       │   ├── cjs/
│   │       │   │   ├── my-config.cjs
│   │       │   │   └── my-plugin.cjs
│   │       │   ├── esm/
│   │       │   │   ├── my-config.mjs
│   │       │   │   └── my-plugin.mjs
│   │       │   ├── index.html
│   │       │   ├── output.html
│   │       │   ├── package.json
│   │       │   └── ts/
│   │       │       ├── my-config.ts
│   │       │       └── my-plugin.ts
│   │       └── subpath-imports/
│   │           ├── app.css
│   │           ├── index.html
│   │           ├── output.html
│   │           ├── package.json
│   │           └── theme.css
│   ├── fixtures.test.ts
│   ├── format.test.ts
│   ├── plugins.test.ts
│   ├── sorter.test.ts
│   ├── tests.ts
│   └── utils.ts
├── tsconfig.json
├── tsdown.config.ts
└── vitest.config.ts
Download .txt
SYMBOL INDEX (117 symbols across 20 files)

FILE: src/config.ts
  function resolvePrettierConfigDir (line 13) | async function resolvePrettierConfigDir(
  function getTailwindConfig (line 46) | async function getTailwindConfig(options: ParserOptions): Promise<Unifie...

FILE: src/console.ts
  function log (line 3) | function log(key: string, arg: unknown, ...args: unknown[]) {
  function warn (line 9) | function warn(key: string, arg: unknown, ...args: unknown[]) {
  function error (line 15) | function error(key: string, arg: unknown, ...args: unknown[]) {

FILE: src/create-plugin.ts
  function createPlugin (line 9) | function createPlugin(transforms: TransformOptions<any>[]) {
  function createParser (line 55) | async function createParser({
  function createPrinter (line 108) | function createPrinter({
  function loadPlugins (line 145) | async function loadPlugins<T>(fns: PluginLoad[]) {
  constant EMPTY_PLUGIN (line 167) | const EMPTY_PLUGIN: Plugin<any> = {
  function loadPlugin (line 175) | async function loadPlugin(source: PluginLoad): Promise<Plugin<any>> {
  function normalizePlugin (line 183) | function normalizePlugin(source: unknown): Plugin<any> {
  function findEnabledPlugin (line 190) | function findEnabledPlugin(options: ParserOptions<any>, name: string) {
  function loadTailwindCSS (line 215) | async function loadTailwindCSS<T = any>({
  function transformAst (line 244) | function transformAst<T = any>({

FILE: src/expiring-map.ts
  type ExpiringMap (line 1) | interface ExpiringMap<K, V> {
  function expiringMap (line 7) | function expiringMap<K, V>(duration: number): ExpiringMap<K, V> {

FILE: src/index.ts
  constant ESCAPE_SEQUENCE_PATTERN (line 16) | const ESCAPE_SEQUENCE_PATTERN = /\\(['"\\nrtbfv0-7xuU])/g
  function tryParseAngularAttribute (line 17) | function tryParseAngularAttribute(value: string, env: TransformerEnv) {
  function transformDynamicAngularAttribute (line 27) | function transformDynamicAngularAttribute(attr: any, env: TransformerEnv) {
  function transformDynamicJsAttribute (line 93) | function transformDynamicJsAttribute(attr: any, env: TransformerEnv) {
  function transformHtml (line 223) | function transformHtml(ast: any, env: TransformerEnv) {
  function transformGlimmer (line 248) | function transformGlimmer(ast: any, env: TransformerEnv) {
  function transformLiquid (line 308) | function transformLiquid(ast: any, env: TransformerEnv) {
  function sortStringLiteral (line 406) | function sortStringLiteral(
  function isStringLiteral (line 460) | function isStringLiteral(node: any) {
  function sortTemplateLiteral (line 466) | function sortTemplateLiteral(
  function isSortableTemplateExpression (line 523) | function isSortableTemplateExpression(
  function isSortableCallExpression (line 530) | function isSortableCallExpression(
  function isSortableExpression (line 539) | function isSortableExpression(
  function canCollapseWhitespaceIn (line 559) | function canCollapseWhitespaceIn(
  function transformJavaScript (line 613) | function transformJavaScript(ast: import('@babel/types').Node, env: Tran...
  function transformCss (line 684) | function transformCss(ast: any, env: TransformerEnv) {
  function transformAstro (line 752) | function transformAstro(ast: any, env: TransformerEnv) {
  function transformMarko (line 777) | function transformMarko(ast: any, env: TransformerEnv) {
  function transformTwig (line 819) | function transformTwig(ast: any, env: TransformerEnv) {
  function transformPug (line 881) | function transformPug(ast: any, env: TransformerEnv) {
  function transformSvelte (line 940) | function transformSvelte(ast: any, env: TransformerEnv) {
  type HtmlNode (line 1038) | type HtmlNode =
  type GlimmerNode (line 1058) | type GlimmerNode =
  type CssValueNode (line 1076) | type CssValueNode = { type: 'value-*'; name: string; params: string }
  type CssNode (line 1077) | type CssNode = {
  type SvelteNode (line 1165) | type SvelteNode = import('svelte/compiler').AST.SvelteNode & {
  method reprint (line 1183) | reprint(path, { options, changes }) {
  type AstroNode (line 1201) | type AstroNode =
  type MarkoNode (line 1234) | type MarkoNode = import('@marko/compiler').types.Node
  type TwigIdentifier (line 1247) | type TwigIdentifier = { type: 'Identifier'; name: string }
  type TwigMemberExpression (line 1249) | type TwigMemberExpression = {
  type TwigCallExpression (line 1254) | type TwigCallExpression = {
  type TwigNode (line 1259) | type TwigNode =
  type PugNode (line 1287) | interface PugNode {
  type LiquidNode (line 1303) | type LiquidNode =
  type PluginOptions (line 1339) | interface PluginOptions {

FILE: src/internal.d.ts
  type InternalOptions (line 3) | interface InternalOptions extends PluginOptions {
  type InternalPlugin (line 12) | interface InternalPlugin {
  type RequiredOptions (line 17) | interface RequiredOptions extends InternalOptions {}
  type ParserOptions (line 18) | interface ParserOptions extends InternalOptions {}
  type Plugin (line 19) | interface Plugin<T = any> extends InternalPlugin {}

FILE: src/options.ts
  type Matcher (line 64) | interface Matcher {
  function createMatcher (line 70) | function createMatcher(
  function nameFromDynamicAttr (line 147) | function nameFromDynamicAttr(name: string, parser: string) {
  function hasMatch (line 166) | function hasMatch(name: string, list: Set<string>, patterns: RegExp[]): ...
  function parseRegex (line 176) | function parseRegex(str: string): RegExp | null {

FILE: src/resolve.ts
  function maybeResolve (line 39) | function maybeResolve(name: string) {
  function loadIfExists (line 55) | async function loadIfExists<T>(name: string): Promise<T | null> {
  function resolveJsFrom (line 66) | function resolveJsFrom(base: string, id: string): string {
  function resolveCssFrom (line 74) | function resolveCssFrom(base: string, id: string) {

FILE: src/sorter.ts
  type SorterOptions (line 11) | interface SorterOptions {
  type Sorter (line 68) | interface Sorter {
  type TailwindConfigOptions (line 88) | type TailwindConfigOptions = {
  function resolveIfRelative (line 96) | function resolveIfRelative(base: string, filePath?: string) {
  function getTailwindConfig (line 107) | async function getTailwindConfig(options: TailwindConfigOptions): Promis...
  function resolveTailwindPath (line 206) | async function resolveTailwindPath(
  function resolveJsConfigPath (line 247) | function resolveJsConfigPath(configPath: string | null): string | null {
  function findClosestJsConfig (line 255) | function findClosestJsConfig(inputDir: string): string | null {
  function resolveStylesheet (line 284) | function resolveStylesheet(stylesheetPath: string | null, base: string):...
  function createSorter (line 338) | async function createSorter(opts: SorterOptions): Promise<Sorter> {

FILE: src/sorting.ts
  type SortOptions (line 4) | interface SortOptions {
  function sortClasses (line 11) | function sortClasses(
  function sortClassList (line 95) | function sortClassList({

FILE: src/transform.ts
  function defineTransform (line 4) | function defineTransform<T>(opts: TransformOptions<T>) {
  type LazyPluginLoad (line 8) | interface LazyPluginLoad {
  type PluginLoad (line 13) | type PluginLoad = LazyPluginLoad | Plugin<any>
  type TransformOptions (line 15) | interface TransformOptions<T> {

FILE: src/types.ts
  type TransformerMetadata (line 4) | interface TransformerMetadata {
  type Customizations (line 11) | interface Customizations {
  type UnifiedApi (line 20) | interface UnifiedApi {
  type TransformerEnv (line 24) | interface TransformerEnv {
  type StringChangePositional (line 31) | interface StringChangePositional {
  type StringChange (line 38) | interface StringChange {

FILE: src/utils.bench.ts
  function buildFixture (line 13) | function buildFixture(repeatCount: number, changeCount: number) {

FILE: src/utils.ts
  function loadIfExists (line 5) | function loadIfExists(name: string): any {
  type PathEntry (line 15) | interface PathEntry<T, Meta> {
  type Path (line 23) | type Path<T, Meta> = PathEntry<T, Meta>[]
  type Visitor (line 25) | type Visitor<T, Meta extends Record<string, unknown>> = (
  type Visitors (line 31) | type Visitors<T, Meta extends Record<string, unknown>> = Record<string, ...
  function isNodeLike (line 33) | function isNodeLike(value: any): value is { type: string } {
  function visit (line 38) | function visit<T extends {}, Meta extends Record<string, unknown>>(
  function spliceChangesIntoString (line 110) | function spliceChangesIntoString(str: string, changes: StringChange[]) {
  function bigSign (line 143) | function bigSign(bigIntValue: bigint) {
  function cacheForDirs (line 163) | function cacheForDirs<V>(

FILE: src/versions/v3.ts
  type LegacyTailwindContext (line 15) | interface LegacyTailwindContext {
  type GenerateRules (line 27) | interface GenerateRules {
  function prefixCandidate (line 31) | function prefixCandidate(context: LegacyTailwindContext, selector: strin...
  function loadV3 (line 36) | async function loadV3(pkgDir: string | null, jsConfig: string | null): P...

FILE: src/versions/v4.ts
  type DesignSystem (line 10) | interface DesignSystem {
  type LoadOptions (line 14) | interface LoadOptions {
  type ApiV4 (line 38) | interface ApiV4 {
  function loadV4 (line 42) | async function loadV4(mod: ApiV4 | null, stylesheet: string | null): Pro...
  function createLoader (line 174) | function createLoader<T>({

FILE: tests/plugins.test.ts
  type PluginTest (line 10) | interface PluginTest {

FILE: tests/utils.ts
  type TestEntry (line 14) | type TestEntry = [
  function t (line 23) | function t(strings: TemplateStringsArray, ...values: string[]): TestEntry {
  function format (line 42) | async function format(str: string, options: prettier.Options = {}) {

FILE: tsdown.config.ts
  function patchJiti (line 8) | function patchJiti(): Rolldown.Plugin {
  function inlineCssImports (line 30) | function inlineCssImports(): Rolldown.Plugin {

FILE: vitest.config.ts
  method resolveId (line 13) | resolveId(id) {
Condensed preview — 142 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (225K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 44,
    "preview": "custom: ['https://tailwindcss.com/sponsor']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "chars": 1406,
    "preview": "---\nname: Bug report\nabout: If you've already asked for help with a problem and confirmed something is broken with prett"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 498,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Get Help\n    url: https://github.com/tailwindlabs/tailwindcss/discu"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 893,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\npermissions:\n  contents: read\n\nconcurre"
  },
  {
    "path": ".github/workflows/prepare-release.yml",
    "chars": 1295,
    "preview": "name: Prepare Release\n\non:\n  workflow_dispatch:\n  push:\n    tags:\n      - 'v*'\n\nenv:\n  CI: true\n  NODE_VERSION: 22\n\nperm"
  },
  {
    "path": ".github/workflows/release-insiders.yml",
    "chars": 1341,
    "preview": "name: Release Insiders\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  id-token: write\n\nconcurrency:\n  group:"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1124,
    "preview": "name: Release\n\non:\n  release:\n    types: [published]\n\npermissions:\n  contents: read\n  id-token: write\n\nconcurrency:\n  gr"
  },
  {
    "path": ".gitignore",
    "chars": 19,
    "preview": "node_modules\n/dist\n"
  },
  {
    "path": ".oxlintrc.json",
    "chars": 168,
    "preview": "{\n  \"$schema\": \"https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json\",\n  \"rules\":"
  },
  {
    "path": ".prettierignore",
    "chars": 40,
    "preview": "tests/fixtures\n.github/\ndist/\nREADME.md\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 22389,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) Tailwind Labs Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 10429,
    "preview": "<img src=\"https://raw.githubusercontent.com/tailwindlabs/prettier-plugin-tailwindcss/main/.github/banner.jpg\" alt=\"prett"
  },
  {
    "path": "knip.json",
    "chars": 933,
    "preview": "{\n  \"$schema\": \"https://unpkg.com/knip@5/schema.json\",\n  \"entry\": [\"scripts/*.js\"],\n  \"project\": [\"src/**/*.ts\", \"script"
  },
  {
    "path": "package.json",
    "chars": 4926,
    "preview": "{\n  \"type\": \"module\",\n  \"name\": \"prettier-plugin-tailwindcss\",\n  \"version\": \"0.7.2\",\n  \"description\": \"A Prettier plugin"
  },
  {
    "path": "prettier.config.js",
    "chars": 241,
    "preview": "/** @type {import('prettier').Config} */\nexport default {\n  semi: false,\n  singleQuote: true,\n  trailingComma: 'all',\n  "
  },
  {
    "path": "scripts/copy-licenses.js",
    "chars": 1050,
    "preview": "import * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport"
  },
  {
    "path": "scripts/install-fixture-deps.js",
    "chars": 685,
    "preview": "import { exec } from 'node:child_process'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimp"
  },
  {
    "path": "scripts/release-channel.js",
    "chars": 736,
    "preview": "// Given a version, figure out what the release channel is so that we can publish to the correct\n// channel on npm.\n//\n/"
  },
  {
    "path": "scripts/release-notes.js",
    "chars": 874,
    "preview": "// Given a version, figure out what the release notes are so that we can use this to pre-fill the\n// relase notes on a G"
  },
  {
    "path": "src/config.ts",
    "chars": 3025,
    "preview": "// @ts-check\nimport * as path from 'node:path'\nimport prettier from 'prettier'\nimport type { ParserOptions } from 'prett"
  },
  {
    "path": "src/console.ts",
    "chars": 467,
    "preview": "let seen = new Set<string>()\n\nexport function log(key: string, arg: unknown, ...args: unknown[]) {\n  if (seen.has(key)) "
  },
  {
    "path": "src/create-plugin.ts",
    "chars": 6832,
    "preview": "import { isAbsolute } from 'path'\nimport type { Parser, ParserOptions, Plugin, Printer } from 'prettier'\nimport { getTai"
  },
  {
    "path": "src/expiring-map.ts",
    "chars": 955,
    "preview": "interface ExpiringMap<K, V> {\n  get(key: K): V | undefined\n  remember(key: K, factory: () => V): V\n  set(key: K, value: "
  },
  {
    "path": "src/index.ts",
    "chars": 37726,
    "preview": "// @ts-ignore\nimport type * as Liquid from '@shopify/prettier-plugin-liquid/dist/types.js'\n// @ts-ignore\nimport lineColu"
  },
  {
    "path": "src/internal.d.ts",
    "chars": 468,
    "preview": "import type { PluginOptions } from '.'\n\nexport interface InternalOptions extends PluginOptions {\n  printer: Printer<any>"
  },
  {
    "path": "src/options.ts",
    "chars": 5313,
    "preview": "import type { RequiredOptions, SupportOption } from 'prettier'\nimport type { Customizations } from './types'\n\nexport con"
  },
  {
    "path": "src/resolve.ts",
    "chars": 2118,
    "preview": "import fs from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { CachedInputFileSystem, ResolverFactory } from"
  },
  {
    "path": "src/sorter.ts",
    "chars": 11666,
    "preview": "import * as path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport escalade from 'escalade/sync'\nimport *"
  },
  {
    "path": "src/sorting.ts",
    "chars": 3573,
    "preview": "import type { TransformerEnv, UnifiedApi } from './types'\nimport { bigSign } from './utils'\n\nexport interface SortOption"
  },
  {
    "path": "src/transform.ts",
    "chars": 1876,
    "preview": "import type { AstPath, Plugin } from 'prettier'\nimport type { TransformerEnv } from './types'\n\nexport function defineTra"
  },
  {
    "path": "src/types.ts",
    "chars": 952,
    "preview": "import type { ParserOptions } from 'prettier'\nimport type { Matcher } from './options'\n\nexport interface TransformerMeta"
  },
  {
    "path": "src/utils.bench.ts",
    "chars": 1752,
    "preview": "import { bench, describe } from 'vitest'\nimport type { StringChange } from './types'\nimport { spliceChangesIntoString } "
  },
  {
    "path": "src/utils.test.ts",
    "chars": 951,
    "preview": "import { describe, test } from 'vitest'\nimport type { StringChange } from './types'\nimport { spliceChangesIntoString } f"
  },
  {
    "path": "src/utils.ts",
    "chars": 4674,
    "preview": "import * as path from 'node:path'\nimport type { StringChange } from './types'\n\n// For loading prettier plugins only if t"
  },
  {
    "path": "src/versions/assets.ts",
    "chars": 616,
    "preview": "// @ts-ignore\nimport index from 'tailwindcss-v4/index.css'\n// @ts-ignore\nimport preflight from 'tailwindcss-v4/preflight"
  },
  {
    "path": "src/versions/v3.ts",
    "chars": 3762,
    "preview": "// @ts-check\nimport * as path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport clearModule from 'clear-m"
  },
  {
    "path": "src/versions/v4.ts",
    "chars": 4989,
    "preview": "import * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport"
  },
  {
    "path": "tests/fixtures/basic/index.html",
    "chars": 44,
    "preview": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/basic/output.html",
    "chars": 44,
    "preview": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/basic/prettier.config.js",
    "chars": 21,
    "preview": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/basic/tailwind.config.js",
    "chars": 109,
    "preview": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        tomato: 'tomato',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/cjs/index.html",
    "chars": 45,
    "preview": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/cjs/output.html",
    "chars": 45,
    "preview": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/cjs/prettier.config.js",
    "chars": 21,
    "preview": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/cjs/tailwind.config.cjs",
    "chars": 111,
    "preview": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        hotpink: 'hotpink',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/custom-jsx/index.jsx",
    "chars": 982,
    "preview": "const a = sortMeFn(\"sm:p-1 p-2\");\nconst b = sortMeFn({\n  foo: \"sm:p-1 p-2\",\n});\n\nconst c = dontSortFn(\"sm:p-1 p-2\");\ncon"
  },
  {
    "path": "tests/fixtures/custom-jsx/output.jsx",
    "chars": 982,
    "preview": "const a = sortMeFn(\"p-2 sm:p-1\");\nconst b = sortMeFn({\n  foo: \"p-2 sm:p-1\",\n});\n\nconst c = dontSortFn(\"sm:p-1 p-2\");\ncon"
  },
  {
    "path": "tests/fixtures/custom-jsx/prettier.config.js",
    "chars": 115,
    "preview": "module.exports = {\n  tailwindFunctions: ['sortMeFn', 'sortMeTemplate', 'tw'],\n  tailwindAttributes: ['sortMe'],\n};\n"
  },
  {
    "path": "tests/fixtures/custom-jsx/tailwind.config.js",
    "chars": 34,
    "preview": "module.exports = {\n  theme: {},\n}\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v3/config.js",
    "chars": 108,
    "preview": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        'tomato': 'tomato',\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v3/index.html",
    "chars": 44,
    "preview": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v3/output.html",
    "chars": 44,
    "preview": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v3/package.json",
    "chars": 178,
    "preview": "{\n  \"dependencies\": {\n    \"tailwindcss-v3\": \"npm:tailwindcss@^3.4.17\"\n  },\n  \"prettier\": {\n    \"tailwindPackageName\": \"t"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v4/app.css",
    "chars": 64,
    "preview": "@import 'tailwindcss-v4';\n\n@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v4/index.html",
    "chars": 44,
    "preview": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v4/output.html",
    "chars": 44,
    "preview": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v4/package.json",
    "chars": 179,
    "preview": "{\n  \"dependencies\": {\n    \"tailwindcss-v4\": \"npm:tailwindcss@^4.1.7\"\n  },\n  \"prettier\": {\n    \"tailwindPackageName\": \"ta"
  },
  {
    "path": "tests/fixtures/custom-vue/index.vue",
    "chars": 406,
    "preview": "<script setup>\n  let a = sortMeFn(\"sm:p-1 p-2\");\n  let b = sortMeFn({\"sm:p-1 p-2\": true});\n  let c = dontSortFn(\"sm:p-1 "
  },
  {
    "path": "tests/fixtures/custom-vue/output.vue",
    "chars": 402,
    "preview": "<script setup>\nlet a = sortMeFn(\"p-2 sm:p-1\");\nlet b = sortMeFn({ \"p-2 sm:p-1\": true });\nlet c = dontSortFn(\"sm:p-1 p-2\""
  },
  {
    "path": "tests/fixtures/custom-vue/prettier.config.js",
    "chars": 109,
    "preview": "module.exports = {\n  tailwindFunctions: ['sortMeFn', 'sortMeTemplate'],\n  tailwindAttributes: ['sortMe'],\n};\n"
  },
  {
    "path": "tests/fixtures/custom-vue/tailwind.config.js",
    "chars": 34,
    "preview": "module.exports = {\n  theme: {},\n}\n"
  },
  {
    "path": "tests/fixtures/esm/index.html",
    "chars": 45,
    "preview": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/esm/output.html",
    "chars": 45,
    "preview": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/esm/prettier.config.js",
    "chars": 21,
    "preview": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/esm/tailwind.config.mjs",
    "chars": 110,
    "preview": "export default {\n  theme: {\n    extend: {\n      colors: {\n        hotpink: \"hotpink\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "tests/fixtures/esm-explicit/config.mjs",
    "chars": 110,
    "preview": "export default {\n  theme: {\n    extend: {\n      colors: {\n        hotpink: \"hotpink\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "tests/fixtures/esm-explicit/index.html",
    "chars": 45,
    "preview": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/esm-explicit/output.html",
    "chars": 45,
    "preview": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/esm-explicit/prettier.config.js",
    "chars": 55,
    "preview": "module.exports = {\n  tailwindConfig: './config.mjs'\n};\n"
  },
  {
    "path": "tests/fixtures/monorepo/.prettierrc",
    "chars": 242,
    "preview": "{\n  \"tailwindFunctions\": [\"tw\"],\n  \"overrides\": [\n    {\n      \"files\": \"package-1/**\",\n      \"options\": {\n        \"tailw"
  },
  {
    "path": "tests/fixtures/monorepo/package-1/app.css",
    "chars": 61,
    "preview": "@import 'tailwindcss';\n\n@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-1/index.jsx",
    "chars": 39,
    "preview": "const a = tw`sm:bg-tomato bg-red-500`;\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-1/output.jsx",
    "chars": 39,
    "preview": "const a = tw`bg-red-500 sm:bg-tomato`;\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-1/package.json",
    "chars": 56,
    "preview": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-2/index.jsx",
    "chars": 39,
    "preview": "const a = tw`sm:bg-tomato bg-red-500`;\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-2/output.jsx",
    "chars": 39,
    "preview": "const a = tw`bg-red-500 sm:bg-tomato`;\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-2/package.json",
    "chars": 57,
    "preview": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"^3.4.17\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-2/tailwind.config.js",
    "chars": 73,
    "preview": "module.exports = { theme: { extend: { colors: { tomato: 'tomato' } } } }\n"
  },
  {
    "path": "tests/fixtures/monorepo/package.json",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "tests/fixtures/no-local-version/app.css",
    "chars": 61,
    "preview": "@import \"tailwindcss\";\n\n@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures/no-local-version/index.html",
    "chars": 44,
    "preview": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-local-version/output.html",
    "chars": 44,
    "preview": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-local-version/package.json",
    "chars": 62,
    "preview": "{\n  \"prettier\": {\n    \"tailwindStylesheet\": \"./app.css\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/no-prettier-config/index.html",
    "chars": 44,
    "preview": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-prettier-config/output.html",
    "chars": 44,
    "preview": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-prettier-config/tailwind.config.js",
    "chars": 109,
    "preview": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        tomato: 'tomato',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/no-stylesheet-given/index.html",
    "chars": 45,
    "preview": "<div class=\"sm:bg-red-500 bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-stylesheet-given/output.html",
    "chars": 45,
    "preview": "<div class=\"bg-red-500 sm:bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/package.json",
    "chars": 25,
    "preview": "{\n  \"type\": \"commonjs\"\n}\n"
  },
  {
    "path": "tests/fixtures/plugins/index.html",
    "chars": 41,
    "preview": "<div class=\"sm:bar foo uppercase\"></div>\n"
  },
  {
    "path": "tests/fixtures/plugins/output.html",
    "chars": 41,
    "preview": "<div class=\"uppercase foo sm:bar\"></div>\n"
  },
  {
    "path": "tests/fixtures/plugins/prettier.config.js",
    "chars": 99,
    "preview": "const prettier = require(\"prettier\")\n\nmodule.exports = {\n  plugins: ['../../../dist/index.mjs'],\n}\n"
  },
  {
    "path": "tests/fixtures/plugins/tailwind.config.js",
    "chars": 239,
    "preview": "const plugin = require(\"tailwindcss-v3/plugin\");\n\nmodule.exports = {\n  plugins: [\n    plugin(function ({ addUtilities })"
  },
  {
    "path": "tests/fixtures/ts/index.html",
    "chars": 45,
    "preview": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/ts/output.html",
    "chars": 45,
    "preview": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/ts/prettier.config.js",
    "chars": 21,
    "preview": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/ts/tailwind.config.ts",
    "chars": 198,
    "preview": "import type { Config } from \"tailwindcss\";\n\nexport default {\n  content: [\"index.html\"],\n  theme: {\n    extend: {\n      c"
  },
  {
    "path": "tests/fixtures/ts-explicit/config.ts",
    "chars": 198,
    "preview": "import type { Config } from \"tailwindcss\";\n\nexport default {\n  content: [\"index.html\"],\n  theme: {\n    extend: {\n      c"
  },
  {
    "path": "tests/fixtures/ts-explicit/index.html",
    "chars": 45,
    "preview": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/ts-explicit/output.html",
    "chars": 45,
    "preview": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/ts-explicit/prettier.config.js",
    "chars": 54,
    "preview": "module.exports = {\n  tailwindConfig: './config.ts'\n};\n"
  },
  {
    "path": "tests/fixtures/v3-2/index.html",
    "chars": 44,
    "preview": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/v3-2/output.html",
    "chars": 44,
    "preview": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/v3-2/package.json",
    "chars": 55,
    "preview": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"3.2.7\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/v3-2/prettier.config.js",
    "chars": 21,
    "preview": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/v3-2/tailwind.config.js",
    "chars": 109,
    "preview": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        tomato: 'tomato',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/index.html",
    "chars": 31,
    "preview": "<div class=\"sm:p-0 p-0\"></div>\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/output.html",
    "chars": 31,
    "preview": "<div class=\"p-0 sm:p-0\"></div>\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/package.json",
    "chars": 110,
    "preview": "{\n  \"private\": true,\n  \"dependencies\": {\n    \"@repo/tailwind-config\": \"file:./packages/tailwind-config\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/packages/tailwind-config/dist/index.mjs",
    "chars": 256,
    "preview": "import { createJiti } from 'jiti'\n\nconst jiti = createJiti(import.meta.url, {\n  interopDefault: true,\n})\n\nconst _module "
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/packages/tailwind-config/package.json",
    "chars": 334,
    "preview": "{\n  \"name\": \"@repo/tailwind-config\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.mjs\",\n  \"module\":"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/packages/tailwind-config/src/index.ts",
    "chars": 48,
    "preview": "export default {\n  content: ['./index.html'],\n}\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/tailwind.config.mjs",
    "chars": 48,
    "preview": "export { default } from '@repo/tailwind-config'\n"
  },
  {
    "path": "tests/fixtures/v4/basic/app.css",
    "chars": 61,
    "preview": "@import 'tailwindcss';\n\n@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures/v4/basic/index.html",
    "chars": 44,
    "preview": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/v4/basic/output.html",
    "chars": 44,
    "preview": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/v4/basic/package.json",
    "chars": 115,
    "preview": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"^4.0.0\"\n  },\n  \"prettier\": {\n    \"tailwindStylesheet\": \"./app.css\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/app.css",
    "chars": 441,
    "preview": "@import 'tailwindcss';\n\n/* Load ESM versions */\n@config './esm/my-config.mjs';\n@plugin './esm/my-plugin.mjs';\n\n/* Load C"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/cjs/my-config.cjs",
    "chars": 119,
    "preview": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        'cjs-from-config': 'black',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/cjs/my-plugin.cjs",
    "chars": 413,
    "preview": "const plugin = require('tailwindcss/plugin')\n\nmodule.exports = plugin(\n  ({ addUtilities }) => {\n    addUtilities({\n    "
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/esm/my-config.mjs",
    "chars": 117,
    "preview": "export default {\n  theme: {\n    extend: {\n      colors: {\n        'esm-from-config': 'black',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/esm/my-plugin.mjs",
    "chars": 406,
    "preview": "import plugin from 'tailwindcss/plugin'\n\nexport default plugin(\n  ({ addUtilities }) => {\n    addUtilities({\n      '.uti"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/index.html",
    "chars": 414,
    "preview": "<div\n  class=\"sm:bg-tomato sm:utility-cjs-from-plugin sm:utility-cjs-from-plugin-2 sm:utility-esm-from-plugin sm:utility"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/output.html",
    "chars": 414,
    "preview": "<div\n  class=\"bg-red-500 utility-cjs-from-plugin utility-esm-from-plugin utility-ts-from-plugin sm:utility-cjs-from-plug"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/package.json",
    "chars": 115,
    "preview": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"^4.0.0\"\n  },\n  \"prettier\": {\n    \"tailwindStylesheet\": \"./app.css\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/ts/my-config.ts",
    "chars": 176,
    "preview": "import type { Config } from 'tailwindcss'\n\nexport default {\n  theme: {\n    extend: {\n      colors: {\n        'ts-from-co"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/ts/my-plugin.ts",
    "chars": 403,
    "preview": "import plugin from 'tailwindcss/plugin'\n\nexport default plugin(\n  ({ addUtilities }) => {\n    addUtilities({\n      '.uti"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/app.css",
    "chars": 41,
    "preview": "@import 'tailwindcss';\n@import '#theme';\n"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/index.html",
    "chars": 44,
    "preview": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/output.html",
    "chars": 44,
    "preview": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/package.json",
    "chars": 163,
    "preview": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"^4.0.0\"\n  },\n  \"prettier\": {\n    \"tailwindStylesheet\": \"./app.css\"\n  },\n  \"imp"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/theme.css",
    "chars": 37,
    "preview": "@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures.test.ts",
    "chars": 3749,
    "preview": "import { exec } from 'node:child_process'\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimpor"
  },
  {
    "path": "tests/format.test.ts",
    "chars": 9871,
    "preview": "import { describe, test } from 'vitest'\nimport { tests } from './tests.js'\nimport { format } from './utils.js'\n\ndescribe"
  },
  {
    "path": "tests/plugins.test.ts",
    "chars": 17773,
    "preview": "import { createRequire } from 'node:module'\nimport dedent from 'dedent'\nimport { test } from 'vitest'\nimport { javascrip"
  },
  {
    "path": "tests/sorter.test.ts",
    "chars": 6526,
    "preview": "import * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { describe, expect, test } from 'vites"
  },
  {
    "path": "tests/tests.ts",
    "chars": 11000,
    "preview": "import type { TestEntry } from './utils.js'\nimport { no, t, yes } from './utils.js'\n\nlet html: TestEntry[] = [\n  t`<div "
  },
  {
    "path": "tests/utils.ts",
    "chars": 1399,
    "preview": "import * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport * as prettier from 'prettier'\n\nconst _"
  },
  {
    "path": "tsconfig.json",
    "chars": 578,
    "preview": "{\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleDetect"
  },
  {
    "path": "tsdown.config.ts",
    "chars": 2027,
    "preview": "import { readFile } from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { defineConfig, Rolldown } from 'ts"
  },
  {
    "path": "vitest.config.ts",
    "chars": 372,
    "preview": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    testTimeout: 10000,\n    css: t"
  }
]

About this extraction

This page contains the full source code of the tailwindlabs/prettier-plugin-tailwindcss GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 142 files (202.0 KB), approximately 61.4k tokens, and a symbol index with 117 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!